从 0 实现一个前端项目提效脚手架

简介: 从 0 实现一个前端项目提效脚手架

1 功能演示

1.1 脚手架安装

npm i @coderyjw/cli -g

输入 jw-cli -h 查询帮助

image.png

1.2 init 命令演示:下载项目模板

1. 输入 jw-cli init -h 查询帮助

image.png

2. 输入 jw-cli init初始化项目,选择类型:项目

image.png

3. 输入项目名称: react-app

image.png

4. 选择项目模板:react模板

image.png

5. 下载成功

image.png

1.3 install 命令演示:源码下载

1. 输入 jw-cli install,选择 Gitee

image.png

2. 输入 token

image.png

3. 输入关键词: vue-admin-template

image.png

4. 输入开发语言(可选择不输入)

image.png

5. 选择项目:vue-admin-template

image.png

6. 选择版本: 4.3.1

image.png

image.png

image.png

7. 下载成功,启动项目

image.png

1. 脚手架整体框架搭建

1.1 脚手架入口文件开发

1. 使用 Lerna 创建项目

Lerna 是一个用于管理包含多个包的 JavaScript 项目的管理工具。

npm i lerna -g
mkdir jw-cli
cd jw-cli
lerna init

此时我们的目录结构应该是这样的:

image.png

2. 通过 Lerna 创建 cli package

lerna create cli

3. 创建 packages/cli/bin/cli.js,通常来说脚手架文件都会放在 bin 目录下

4. 将 packages/cli/lib/cli.js 修改为 packages/cli/lib/index.js。这个就是我们未来要下载和安装的入口文件

5. 到 npm 上创建对应的组织

image.png

image.png
6. 修改 packages/cli/package.json

image.png

注意上面填入的 package name(@coderyjw/cli) 要填入你自己在 npm 上创建的组织,

7. 让入口文件生效

cd packages/cli
npm link

8. 修改 bin/cli.jslib/index.js 测试是否生效

bin/cli.js

#!/usr/bin/env node

import entry from "../lib/index.js";

entry(process.argv.slice(2));

lib/index.js

export default function (args) {
  console.log("this is entry", args);
}

执行 jw-cli,控制台打印如下内容说明入口文件已生效

image.png

至此我们脚手架的入口就已经开发好了。

1.2 脚手架注册 + 命令注册

1. 在 packages/cli 目录下安装 commander 插件

lerna add commander packages/cli

commander 是一个很好用的 node.js 命令行解决方案,这里就不详细讲了。

2. 创建 packages/cli/lib/createCLI.js 文件用于封装脚手架注册逻辑

import fse from "fs-extra";
import path from "path";
import { dirname } from "dirname-filename-esm";

const __dirname = dirname(import.meta);
const pkgPath = path.resolve(__dirname, "../package.json");
const pkg = fse.readJsonSync(pkgPath);

export default function createCLI(program) {

  // 命令注册
  program
    .name(Object.keys(pkg.bin)[0])
    .usage("<command> [options]")
    .version(pkg.version, "-v, --version", "输出当前版本号")
    .option("-d, --debug", "是否开启调试模式", false)
    .helpOption("-h, --help", "命令显示帮助")
    .addHelpCommand("help [command]", "命令显示帮助");

  // 监听未知的命令
  program.on("command:*", function (obj) {
    console.error("未知的命令:" + obj[0]);
  });

  // 监听 debug 选项
  program.on("option:debug", function () {
    if (program.opts().debug) {
      console.log("开启调试模式");
    }
  });
}

3. 在 packages/cli/lib/index.js 中引入 createCLI

import { program } from "commander";
import createCLI from "./createCLI.js";

export default function (args) {
  createCLI(program);
  program.parse(process.argv);
}

这里需要引入两个包:

  1. fs-extra:它可以看做是 nodefs 模块的升级版,比 fs 更加的好用
  2. dirname-filename-esm:因为在 esm 中,是没有 cjs 中的 __dirname 变量的,但是我们可以通以下下方法获得 __dirname
import { dirname } from "dirname-filename-esm";
const __dirname = dirname(import.meta);

所以我们要先安装以下这两个包

lerna add dirname-filename-esm  packages/cli
lerna add fs-extra  packages/cli

4. 最后测试能出现下面的效果则说明我们已经成功注册了命令

image.png

1.3 log 日志功能封装

在上面的代码中,我们使用的是 cosole.log 进行日志打印,实际上我们有更好的选择,那就是 npmlog,虽然它的功能已经非常完善了,但是我们最好对它再进行一层封装。

1. 创建 utils 文件夹作为我们的一个工具包,后续我们所有的工具类都放在这个包下

lerna create utils

此时的目录结构

image.png

2. 打开 packages/utils/packages.json 修改

image.png

3. 将 packages/utils/utils.js 改为 packages/utils/index.js

4. 安装 npmlog 依赖

leran add npmlog pacakges/utils

5. 创建 packages/utils/log.js 文件并做如下配置

import log from "npmlog";
import isDebug from "./isDebug.js";

if (isDebug()) {
  log.level = "verbose";
} else {
  log.level = "info";
}

log.heading = "jw-cli";

log.addLevel("success", 2000, { fg: "green", bold: true });

export default log;

6. 创建 packages/utils/isDebug.js

/**
 * 判断是否是 debug 模式
 * @returns boolean
 */
export default function isDebug() {
  return process.argv.includes("--debug") || process.argv.includes("-d");
}

7. 在 packages/utils/index.js 导入并导出这两个模块

import isDebug from "./isDebug.js";
import log from "./log.js";

export { log, isDebug };

8. 在 cli 包中引入 utils

lerna add @coderyjw/utils packages/cli

9. 修改 packages/cli/bin/createCLI.js,引入 log 模块,并将 console 改为 log

import { log } from "@coderyjw/utils";
...
export default function createCLI(program) {
  ...
  // 监听未知的命令
  program.on("command:*", function (obj) {
    log.error("未知的命令:" + obj[0]);
  });

  // 监听 debug 选项
  program.on("option:debug", function () {
    if (program.opts().debug) {
      log.verbose("debug", "开启调试模式");
    }
  });

10. 测试成功

image.png

1.4 Commander 类封装

log 一样,虽然 commander 已经很好用了,但是我们还是希望能对其进行封装,这样对我们后续的命令注册也会非常的方便。对于 Commander 类的封装,这里提供给大家一种思路。

1. 创建 command 一个新的 package

lerna create command

2. 修改 packages/command/package.json

image.png

3. 将 packages/command/command.js 改为 packages/command/index.js,修改 index.js

class Command {
  constructor(instance) {
    if (!instance) {
      throw new Error("command instance must not be null!");
    }

    this.program = instance;

    const cmd = this.program.command(this.command);
    cmd.description(this.description);
    cmd.hook("preAction", () => {
      this.preAction();
    });
    cmd.hook("postAction", () => {
      this.postAction();
    });
    if (this.options?.length > 0) {
      this.options.forEach((option) => {
        cmd.option(...option);
      });
    }
    cmd.action((...params) => {
      this.action(params);
    });
  }

  get command() {
    throw new Error("command must be implements");
  }

  get description() {
    throw new Error("description must be implements");
  }

  get options() {
    return [];
  }

  action() {
    throw new Error("action must be implements");
  }

  preAction() {
    // empty
  }

  postAction() {
    // empty
  }
}

export default Command;

在上面的代码中,我们封装了一个 Command 类最后将其导出,在 Command 类的构造函数中,我们主要做了以下几件事:

  1. 通过 program.command 注册命令
  2. 添加 description 命令描述信息
  3. 添加了命令生命周期 preActionpostAction 两个 hook,这两个函数分别会在 action 的前后触发
  4. 调用 command.action() 触发我们命令的逻辑

之后的所有命令只要继承该类即可

1.5 init 命令封装

接下来我们终于可以开始我们的第一个命令的注册了。

1. 创建 init

lerna create init

2. 修改 packages/init/package.json

image.png

3. 将 packages/init/init.js 改为 packages/init/index.js,修改 index.js

import Command from "@coderyjw/command";
import { log } from '@coderyjw/utils'

class InitCommand extends Command {
  get command() {
    return "init [name]";
  }

  get description() {
    return "项目初始化";
  }

  get options() {
    return [
      ["-f, --force", "是否强制更新", false],
      ["-t, --type <type>", "项目类型(project/page)"],
      ["-tp, --template <template>", "模版名称"],
    ];
  }

  async action([name, opts]) {
    log.verbose("init", name, opts);
  }

  preAction() {}

  postAction() {}
}

function Init(instance) {
  return new InitCommand(instance);
}

export default Init;

在上面的代码中我们封装的 InitCommand 继承自 Command 类,最后导出了一个创建 InitCommand 类的函数,这样,我们只要到 packages.cli/lib/index.js 脚手架入口文件中调用这个函数即可

4. 修改 packages.cli/lib/index.js

import { program } from "commander";
import createCLI from "./createCLI.js";
import createInitCommand from "@coderyjw/init";

export default function (args) {
  createCLI(program);

  createInitCommand(program)
  
  // 解析配置
  program.parse(process.argv);
}

5. 当然我们还要导入一下所需的依赖

lerna add @coderyjw/command packages/init
lerna add @coderyjw/utils packages/init
lerna add @coderyjw/init packages/cli

6. 测试成功

image.png

1.6 node 最低版本检查功能

到现在我们脚手架的基本逻辑其实已经差不多了,我们可以再回来优化一下启动逻辑,比如检查 Node 版本,这个启动的逻辑可以放在 commander 的钩子函数 preAction 中来实现

1. 修改 packages/cli/bin/createCLI.js

import semver from "semver";
import chalk from "chalk";
...
...
const LOWEST_NODE_VERSION = "14.0.0";

const checkNodeVersion = () => {
  log.verbose("node version", process.version);
  // 当前 node 版本低于 LOWEST_NODE_VERSION 时 抛出错误
  if (!semver.gte(process.version, LOWEST_NODE_VERSION)) {
    throw new Error(
      chalk.red(`jw-cli 需要安装 ${LOWEST_NODE_VERSION} 以上版本的 Node.js`)
    );
  }
};

const preAction = () => {
  // 检查 Node 版本
  checkNodeVersion();
};
export default function createCLI(program) {
  // 命令注册
  program
    .name(Object.keys(pkg.bin)[0])
    .usage("<command> [options]")
    .version(pkg.version, "-v, --version", "输出当前版本号")
    .option("-d, --debug", "是否开启调试模式", false)
    .helpOption("-h, --help", "命令显示帮助")
    .addHelpCommand("help [command]", "命令显示帮助")
    .hook("preAction", preAction);
...
...
  • 在上面的代码中:

    1. 我们首先定义了一个 preAction 函数,后续如果还有什么启动逻辑需要做的都可以放在这里,
    2. 然后封装了一个 checkNodeVersion,在这个方法中我们用到了两个第三方包

      1. 一个是 semver: 这个包是专门用来比较两个版本的
      2. 另一个是 chalk:这个包是专门用来修改终端字符串样式的

2. 接下来我们安装下这两个包

lerna add chalk packages/cli 
lerna add semver packages/cli 

3. 分别修改 LOWEST_NODE_VERSION 的值为 17.0.014.0.0 进行测试,测试成功

image.png

image.png

1.7 添加异常监听

到现在为止,我们已经基本把脚手架的框架维护好了,最后再做一些收尾工作,比如异常监听。

Node.js 程序运行在单进程上,应用开发时一个难免遇到的问题就是异常处理,

关于异常监听,我们可以这样优化,判断如果当前是调试模式时再打印出函数栈,如果不是就仅打印出 message 即可

1. 修改 packages/utils/lib/log.js,封装 printErrorLog 方法,之后如果我们主动捕捉到的异常时都可以用这个方法打印日志

export function printErrorLog(e, type) {
  if (isDebug()) {
    log.error(type, e);
  } else {
    log.error(type, e.message);
  }
}

2. 修改在 packages/utils/lib/index.js , 导出 printErrorLog

import isDebug from "./isDebug.js";
import log, { printErrorLog } from "./log.js";

export { log, isDebug, printErrorLog };

3. 另外除了主动捕捉到的异常,还有一些可能漏掉的异常,可能导致程序退出,我们创建 packages/cli/lib/exception.js

import { printErrorLog } from "@coderyjw/utils";

process.on("uncaughtException", (e) => printErrorLog(e, "error"));

process.on("unhandledRejection", (e) => printErrorLog(e, "promise"));

4. 在 packages/cli/index.js 中引入 exception.js

import "./exception.js";

5. 我们以上一小节的报错为例进行测试

image.png

可以看到加上 -d 的有报错的函数调用栈,而没加的就只打印出了报错 message

2. 项目创建脚手架开发

  • 我们为什么要开发创建项目的脚手架?为什么不直接使用 vue-cli 或者 create-react-app

    1. 因为项目在迭代过程中会添加很多本土化的元素,如:H5 兼容接口请求埋点上报组件封装通用方法等,甚至会对整块业务逻辑进行复用,比如登录;
    2. 而且每次在创建项目的时候都要重新填写这些代码是非常耗时的,而且无法对每个团队成员进行复用,可以利用脚手架来完成项目模板的沉淀和标准化建设。

2.1 项目模板开发

1. 新建一个 jw-cli-template 项目,并按如下目录结构创建文件夹

image.png

2. 修改分别修改各个模板的 package.json

react 模板

{
  "name": "@coderyjw/template-react18",
  "version": "1.0.0",
  "description": "jw-cli react18 template",
  "author": "yejw6",
  "license": "ISC",
  "publishConfig": {
    "access": "public"
  }
}

vue 模板

{
  "name": "@coderyjw/template-vue3",
  "version": "1.0.0",
  "description": "jw-cli vue3 template",
  "author": "yejw6",
  "license": "ISC",
  "publishConfig": {
    "access": "public"
  }
}

vue-element-admin 模板

{
  "name": "@coderyjw/template-vue-element-admin",
  "version": "1.0.0",
  "description": "jw-cli vue-element-admin template",
  "main": "index.js",
  "author": "yejw6",
  "license": "ISC",
  "publishConfig": {
    "access": "public"
  }
}

3. 这三个模板的 template 就是存放我们最终代码的位置,关于模板大家可以按照自己的沉淀配置,这里我分享一下我 react模板的配置

4. 通过 npm 依次发布模板

npm publish 

5. 可以在 npm 仓库中看到我们的三个模板

image.png

2.2 整体逻辑搭建

有了模板之后我们就可以开始写代码去下载模板了。

  • 我们的这个脚手架最主要的逻辑应该包含以下三条:

    1. 我们应该有一个命令行来让我们选择生成哪份模板,甚至还可以选择生成一个页面模块(包含页面、状态、路由、网络请求等等)
    2. 选择完之后最好是能缓存到本地硬盘上,这样下次再去生成时就不要再去下载了
    3. 最后从缓存中下载到本地目录,提示命令运行项目

1. 创建 packages/init/lib/createTemplate.js

import { log } from "@coderyjw/utils";

export default async function createTemplate(name, opts) {
  log.verbose("createTemplate", "选择项目模板,生成项目信息");

  return null
}

2. 创建 packages/init/lib/downloadTemplate.js

import { log } from "@coderyjw/utils";

export default async function downloadTemplate(selectedTemplate) {
  log.verbose("downloadTemplate", "下载项目模板值缓存目录");
  log.verbose("template", selectedTemplate);
}

3. 创建 packages/init/lib/installTemplate.js

import { log } from "@coderyjw/utils";

export default async function installTemplate(name, opts) {
  log.verbose("installTemplate", "安装项目模板至目录");
}

上面创建的三份文件分别对应我们上面说的三个逻辑,目前还是空的需要我们去完善代码。

4. 修改 packages/init/lib/index.js

import createTemplate from "./createTemplate.js";
import downloadTemplate from "./downloadTemplate.js";
import installTemplate from "./installTemplate.js";
...
...

  async action([name, opts]) {
    // 1. 选择项目模板,生成项目信息
    const selectedTemplate = await createTemplate(name, opts);
    // 2. 下载项目模板值缓存目录
    await downloadTemplate(selectedTemplate);
    // 3. 安装项目模板至目录
    await installTemplate(selectedTemplate, opts);
  }

至此我们整体的代码已经创建好了,接下来就是去实现各个逻辑了

2.3 选择项目模板,生成项目信息

要想在命令行进行选择模板,就要用到 inquirer 了,inquirer 拥有一组通用的交互式命令行用户界面,支持常见的 input 输入、单选、多选、是/否等常见提问类型

1. 创建 packages/utils/inquirer.js,分别对其以上能力进行封装

import inquirer from "inquirer";

function make({
  choices,
  defaultValue,
  message = "请选择",
  type = "list",
  require = true,
  mask = "*",
  validate,
  pageSize,
  loop,
}) {
  const options = {
    name: "name",
    default: defaultValue,
    message,
    type,
    require,
    mask,
    validate,
    pageSize,
    loop,
  };

  if (type === "list") {
    options.choices = choices;
  }

  return inquirer.prompt(options).then((answer) => answer.name);
}

export function makeList(params) {
  return make({ ...params });
}

export function makeInput(params) {
  return make({ type: "input", ...params });
}

export function makePassword(params) {
  return make({ type: "password", ...params });
}

2. 在 package/utils/lib/index.js 中导出

import isDebug from "./isDebug.js";
import log, { printErrorLog } from "./log.js";
import { makeList, makeInput, makePassword } from "./inquirer.js";

export { log, isDebug, printErrorLog, makeList, makeInput, makePassword };

3. 安装 inquirer

lerna add inquirer packages/utils

4. 修改 package/init/lib/createTemplate.js 文件

import { log, makeList, makeInput } from "@coderyjw/utils";
const ADD_TYPE_PROJECT = "project";
const ADD_TYPE_PAGE = "page";

const ADD_TYPE = [
  { name: "项目", value: ADD_TYPE_PROJECT },
  { name: "页面", value: ADD_TYPE_PAGE },
];

const GLOBAL_ADD_TEMPLATE = [
  {
    name: "vue3 项目模板",
    value: "template-vue3",
    npmName: "@coderyjw/template-vue3",
    version: "1.0.1",
  },
  {
    name: "react18 项目模板",
    value: "template-react18",
    npmName: "@coderyjw/template-react18",
    version: "1.0.0",
  },
  {
    name: "vue-element-admin 项目模板",
    value: "template-vue-element-admin",
    npmName: "@coderyjw/template-vue-element-admin",
    version: "1.0.0",
  },
];

// 获取创建类型
function getAddType() {
  return makeList({
    choices: ADD_TYPE,
    message: "请选择初始化类型",
    defaultValue: ADD_TYPE_PROJECT,
  });
}

// 获取项目名称
function getAddName() {
  return makeInput({
    message: "请输入项目的名称",
    defaultValue: "",
    validate(name) {
      if (name.length > 0) return true;
      return "项目名称不能为空";
    },
  });
}

// 选择项目模版
function getAddTemplate(ADD_TEMPLATE) {
  return makeList({
    choices: ADD_TEMPLATE,
    message: "请选择项目模版",
  });
}

export default async function createTemplate(name, opts) {
  log.verbose("createTemplate", "选择项目模板,生成项目信息");

  const ADD_TEMPLATE = GLOBAL_ADD_TEMPLATE;

  // 项目类型,项目名称,项目模版
  let addType, addName, addTemplate;
  addType = await getAddType();
  log.verbose("addType", addType);

  if (addType === ADD_TYPE_PROJECT) {
    addName = await getAddName();
    log.verbose("addName", addName);
    addTemplate = await getAddTemplate(ADD_TEMPLATE);
    log.verbose("addTemplate", addTemplate);

    const selectedTemplate = ADD_TEMPLATE.find((_) => _.value === addTemplate);
    if (!selectedTemplate) throw new Error(`项目模版 ${template} 不存在!`);
    log.verbose("selectedTemplate", selectedTemplate);

    return {
      type: addType,
      name: addName,
      template: selectedTemplate,
    };
  } else {
    throw new Error(`抱歉,创建的项目类型 ${addType} 暂不支持!`);
  }
}

在上面的代码中我们分别封装 getAddName getAddType getAddTemplate 三个方法,用于获取要创建的类型、名称和模板

5. 测试

image.png

image.png

image.png

image.png

不出意外的话就会生成我们选中模板的信息了。

6. 修改 package/init/lib/createTemplate.js,优化代码,使之能通过一条命令直接生成模板

+  const { type, template } = opts;
   // 项目类型,项目名称,项目模版
   let addType, addName, addTemplate;
-  addType = await getAddType()
+  if (type) {
+    addType = type;
+  } else {
+    addType = await getAddType();
+  }
...
-    addName = await getAddName();
+    if (name) {
+      addName = name;
+    } else {
+      addName = await getAddName();
+    }
-    addTemplate = await getAddTemplate(ADD_TEMPLATE);
+    if (template) {
+      addTemplate = template;
+    } else {
+      addTemplate = await getAddTemplate(ADD_TEMPLATE);
+    }

7. 测试成功

image.png

上面的代码还会有一个小问题,就是生成的模板信息中 version 一直是 1.0.0 固定,我们知道我们的版本是会更新的,如果每次更新都要重新改一下脚手架的代码是会十分麻烦的,那么应该如何解决呢?我这里的提供一种方法是每次都去下载最新的版本,或者你也可以做一个功能下载指定版本的模板。

npm 给我们提供了一个 api 让我们可以获取到一个包的所有信息,我们只需要通过请求 https://registry.npmjs.org/ + 包名 即可

image.png

8. npm API 接入,创建 packages/utils/npm.js

import urlJoin from "url-join";
import axios from "axios";
import { log } from "./log.js";

function getNpmInfo(npmName) {
  // npm API 地址 https://registry.npmjs.org/
  // 如果 npm 过慢,可以使用淘宝镜像 https://registry.npmmirror.com/
  const registry = "https://registry.npmmirror.com/";
  const url = urlJoin(registry, npmName);
  return axios.get(url).then((res) => {
    try {
      return res.data;
    } catch (e) {
      Promise.reject(e);
    }
  });
}

export default async function getLatestVersion(npmNmae) {
  const result = await getNpmInfo(npmNmae);
  if (result?.["dist-tags"]?.["latest"]) {
    return result["dist-tags"]["latest"];
  }
  log.error("没有 latest 版本号");
  return Promise.reject("没有 latest 版本号");
}

9. 修改 packages/utils/index.js 导出 getLatestVersion 方法

import isDebug from "./isDebug.js";
import log, { printErrorLog } from "./log.js";
import { makeList, makeInput, makePassword } from "./inquirer.js";
import getLatestVersion from "./npm.js";
export {
  log,
  isDebug,
  printErrorLog,
  makeList,
  makeInput,
  makePassword,
  getLatestVersion,
};

10. 最后不要忘了安装 axiosurl-join 两个包

lerna add axios packages/utils
lerna add url-join packages/utils

11. 在 packages/init/lib/createTemplate.js 中引入 getLatestVersion,获取最新版本

// 获取最新的版本
const latestVersion = await getLatestVersion(selectedTemplate.npmName);
log.verbose("latestVersion", latestVersion);
selectedTemplate.version = latestVersion;

12. 最后因为我们之后要下载模板,最好将缓存目录获取的逻辑在这一步给实现了传过去

import path from "path";
import { homedir } from "os";

const TEMP_HOME = ".jw-cli";
...
// 安装缓存目录
function makeTargetPath() {
  return path.resolve(`${homedir()}/${TEMP_HOME}`, "addTemplate");
}
...
const targetPath = makeTargetPath();
log.verbose("targetPath", targetPath);

return {
  type: addType,
  name: addName,
  template: selectedTemplate,
  targetPath,
};

2.4 下载项目模板至缓存目录

接下来我们就来到了第二步逻辑:下载项目模板至缓存目录

1. 修改 packages/init/downloadTemplate.js

import path from "node:path";
import { pathExistsSync } from "path-exists";
import fse from "fs-extra";
import ora from "ora";
import { execa } from "execa";
import { printErrorLog, log } from "@yejiwei/utils";

function getCacheDir(targetPath) {
  return path.resolve(targetPath, "node_modules");
}

function makeCacheDir(targetPath) {
  const cacheDir = getCacheDir(targetPath);
  if (!pathExistsSync(cacheDir)) {
    fse.mkdirpSync(cacheDir);
  }
}

async function downloadAddTemplate(targetPath, selectedTemplate) {
  const { npmName, version } = selectedTemplate;
  const installCommand = "npm";
  const installArgs = ["install", `${npmName}@${version}`];
  const cwd = targetPath;
  const subprocess = execa(installCommand, installArgs, { cwd });
  await subprocess;
}

export default async function downloadTemplate(selectedTemplate) {
  const { targetPath, template } = selectedTemplate;
  makeCacheDir(targetPath);
  const spinner = ora("正在下载模版....").start();

  try {
    await downloadAddTemplate(targetPath, template);
    spinner.stop();
    log.success("模版下载成功");
  } catch (e) {
    spinner.stop();
    printErrorLog(e);
  }
}
  • 在上面的代码中:

    1. 我们主要通过封装了 downloadAddTemplate 来下载模板
    2. 引入 execa模块 来执行 npm install 包名的方式将模板下载至缓存目录中
    3. 另外还引入了 ora 模块做了一个下载中的动画

    image.png

2. 引入这些依赖

lerna add path-exists packages/init
lerna add execa packages/init
lerna add ora packages/init

3. 输入命令测试

image.png

image.png

可以看到命令行显示模板下载成功,并且模板代码已经成功下载至我们的路径 C:\Users\yjw\.jw-cli\addTemplate\node_modules\@coderyjw\template-react18 下面

2.5 安装项目模板至目录

接下来我们终于来到第三步逻辑了:安装项目模板至目录

1. 修改 packages/init/lib/installTemplate.js

import fse from "fs-extra";
import path from "path";
import ora from "ora";
import { log } from "@coderyjw/utils";

function getCacheFilePath(targetPath, template) {
  return path.resolve(targetPath, "node_modules", template.npmName, "template");
}

function copyFile(targetPath, template, installDir) {
  const originFilePath = getCacheFilePath(targetPath, template);
  log.verbose("originFilePath", originFilePath);
  const fileList = fse.readdirSync(originFilePath);
  log.verbose("fileList", fileList);
  const spinner = ora("正在拷贝文件...").start();
  fileList.map((file) => {
    fse.copySync(`${originFilePath}/${file}`, `${installDir}/${file}`);
  });
  spinner.stop();
  log.success("模版拷贝成功");
}

export default async function installTemplate(selectedTemplate, opts) {
  log.verbose("installTemplate", "安装项目模板至目录");
  const { force = false } = opts;
  const { targetPath, template, name } = selectedTemplate;
  const rootDir = process.cwd();
  fse.ensureDirSync(targetPath);
  const installDir = path.resolve(`${rootDir}/${name}`);
  fse.ensureDirSync(installDir);

  copyFile(targetPath, template, installDir);
  log.info(`输入以下命令运行项目`)
  log.info(`cd ${name}`)
  log.info(`npm install`)
  log.info(`npm start`)
}

在上面的代码中我们主要封装了 copyFile 方法用来从缓存目录中拷贝文件到当前目录下,但是有一个小问题就是,如果当前目录下我们填入 name 的文件夹已存在时就会有点问题,这里我们可以通过添加一个 option 让用户选择是否强制更新

image.png

2. 代码逻辑优化

+   import { pathExistsSync } from "path-exists";
...
+   const { force = false } = opts;
...
-  fse.ensureDirSync(installDir);
+  if (pathExistsSync(installDir)) {
+    if (!force) {
+      log.error(`当前目录已存在 ${installDir} 文件夹`);
+      return;
+    } else {
+      fse.removeSync(installDir);
+      fse.ensureDirSync(installDir);
+    }
+  } else {
+    fse.ensureDirSync(installDir);
+  }

3. 输入 jw-cli init react-app -t project -tp template-react18 -d 命令测试

image.png
模板成功安装

再次输入命令显示

image.png

输入 jw-cli init react-app -t project -tp template-react18 -d -f 命令强制更新

image.png

至此,我们的项目创建脚手架的开发就已经完成了。

3. 源码下载脚手架开发

在上一章中,我们完成了项目创建脚手架的开发,代码全部放置 init文件夹下,从本章开始我们将进行源码下载器的开发。

  • 为什么要实现源码下载器?

    1. 因为 npm 只能下载 npm registry 中的包,而对于未上传 npm 的包则无法下载
    2. 对于 githubgitee 的项目或包如果手动下载效率低下,则可以通过脚手架开发前端项目下载器提升效率
  • 那么,实现一个源码下载器需要怎么样的流程呢?
  1. 首先我们需要掌握 github APIgitee API的使用方法
  2. 通过 githubgitee 实现根据仓库名称搜索项目的能力(github 能力更强,甚至可以根据源码进行搜索)
  3. 存储 githubgiteetoken ,具备 github APIgitee API的调用条件
  4. 通过搜索获取仓库信息,选取指定版本(实现翻页功能)
  5. 将指定版本的源码拉取到本地
  6. 在本地安装依赖,安装可执行文件,启动项目

3.1 整理逻辑搭建

1. 创建 install 文件夹作为源码下载器的包

lerna create install

2. 修改 packages/install/package.json

{
  "name": "@coderyjw/install",
  "version": "0.0.0",
  "description": "jw-cli脚手架 install 命令",
  "author": "叶继伟 <yejw6@asiainfo.com>",
  "homepage": "",
  "license": "ISC",
  "main": "lib/index.js",
  "directories": {
    "lib": "lib",
    "test": "__tests__"
  },
  "files": [
    "lib"
  ],
  "type": "module",
  "scripts": {
    "test": "node ./__tests__/install.test.js"
  }
}

3. 修改 packages/install/lib/install.jspackages/install/lib/index.js

import Command from "@coderyjw/command";


class InstallCommand extends Command {
  get command() {
    return "install";
  }

  get description() {
    return "项目下载、安装依赖、启动项目";
  }

  get options() {
    return [["-c, --clear", "清空缓存", false]];
  }

  async action() {}
}

function Install(instance) {
  return new InstallCommand(instance);
}

export default Install;

4. 在 packages/cli/lib/index.js 中引入 install

import { program } from "commander";
import createCLI from "./createCLI.js";
import createInitCommand from "@coderyjw/init";
import createInstall from "@coderyjw/install";
import "./exception.js";

export default function (args) {
  createCLI(program);

  createInitCommand(program);

  createInstall(program)

  // 解析配置
  program.parse(process.argv);
}

5. 安装依赖

lerna add @coderyjw/command packages/install
lerna add @coderyjw/install packages/cli

6. 运行 jw-cli,可以看到此时我们的脚手架已经有了 install 命令

image.png

3.2 平台选择 + token 缓存

1. 创建 packages/utils/git/GitServer.js 文件

import { homedir } from "os";
import path from "path";
import { pathExistsSync } from "path-exists";
import fse from "fs-extra";
import { makePassword } from "../lib/inquirer.js";
import { log } from "../lib/index.js";

const TEMP_HOME = ".jw-cli";
const TEMP_GITHUB_TOEN = ".github-token";
const TEMP_GITEE_TOEN = ".gitee-token";

function createTokenPath(platForm) {
  if (platForm === "github") {
    return path.resolve(homedir(), TEMP_HOME, TEMP_GITHUB_TOEN);
  } else {
    return path.resolve(homedir(), TEMP_HOME, TEMP_GITEE_TOEN);
  }
}

export default class GitServer {
  constructor() {}

  async init(platForm) {
    // 判断 token 是否录入
    const tokenPath = createTokenPath(platForm);
    if (pathExistsSync(tokenPath)) {
      this.token = fse.readFileSync(tokenPath).toString();
    } else {
      this.token = await this.getToken();
      fse.writeFileSync(tokenPath, this.token);
    }
    log.verbose("token", this.token);
  }

  async getToken() {
    return await makePassword({ message: "请输入 token 信息" });
  }
}

2. 创建 packages/utils/git/Gitee.js 文件

import GitServer from "./GitServer.js";
import { log } from "../lib/index.js";

const BASE_URL = "https://gitee.com/api/v5";

export default class Gitee extends GitServer {
  constructor() {
    super();
  }
}

3. 创建 packages/utils/git/Github.js 文件

import GitServer from "./GitServer.js";
import { log } from "../lib/index.js";

const BASE_URL = "https://api.github.com";

export default class GitHub extends GitServer {
  constructor() {
    super();
  }
}

4. 创建 packages/utils/git/GitUtils.js 文件

import { makeList, log } from "../lib/index.js";
import Github from "./Github.js";
import Gitee from "./Gitee.js";

export async function chooseGitPlatForm() {
  let platForm = await makeList({
    message: "请选择 Git 平台",
    choices: [
      {
        name: "Github",
        value: "github",
      },
      { name: "Gitee", value: "gitee" },
    ],
  });
  log.verbose("platForm", platForm);

  return platForm;
}

export async function initGitServer(platForm) {
  let gitAPI;
  if (platForm === "github") {
    gitAPI = new Github();
  } else if (platForm === "gitee") {
    gitAPI = new Gitee();
  }
  await gitAPI.init(platForm);
  return gitAPI;
}

5. 在 packages/utils/index.js 中导入并导出

+  import Github from "../git/Github.js";
+  import Gitee from "../git/Gitee.js";
+  import { chooseGitPlatForm, initGitServer } from "../git/GitUtils.js";
export {
  log,
  isDebug,
  printErrorLog,
  makeList,
  makeInput,
  makePassword,
  getLatestVersion,
+  Github,
+  Gitee,
+  chooseGitPlatForm,
+  initGitServer
};

在上面的代码中我们分别封装了三个类 GitSeverGithubGitee,后两者继承自前者,我们之后会使用这两个类来接入 giteegithubapi 来完成源码的查询与下载,但是,我们当前的目标还是先进行平台(github/gitee)的选择和 token 的缓存,因为调用这两个平台的 api 都是需要提供它们的 token 的,所以我们最好在第一次输入之后缓存一下,之后就不用再输入了。

6. 修改 packages/install/lib/index.js

import { chooseGitPlatForm, initGitServer } from "@coderyjw/utils";
...

  async action() {
    await this.generateGitAPI();
  }

  async generateGitAPI() {
    this.platForm = await chooseGitPlatForm();

    this.gitAPI = await initGitServer(this.platForm);
  }
...

7. 安装依赖

lerna add path-exists packages/utils
lerna add fs-extra packages/utils
lerna add @coderyjw/utils packages/install

8. 测试

image.png

image.png

我们到 C:\Users\yjw\.jw-cli 目录下可以看到两个平台的 token 数据都已经缓存到了本地

image.png

  1. 关于 Github token 如何获取 token 可以查看 文档
  2. Gitee 创建 token

3.3 仓库搜索 + 翻页功能开发

1. Github / Gitee Search API 接入,分别修改 packages/utils/git/Github.jspackages/utils/git/Gitee.js

Github.js

import axios from "axois";
import GitServer from "./GitServer.js";
import { log } from "../lib/index.js";

const BASE_URL = "https://api.github.com";

export default class GitHub extends GitServer {
  constructor() {
    super();
    this.service = axios.create({
      baseURL: BASE_URL,
      timeout: 50000,
    });

    this.service.interceptors.request.use(
      (config) => {
        config.headers["Authorization"] = `Bearer ${this.token}`;
        config.headers["Accept"] = "application/vnd.github+json";
        return config;
      },
      (error) => {
        return Promise.reject(error);
      }
    );

    this.service.interceptors.response.use(
      (response) => {
        return response.data;
      },
      (err) => {
        return Promise.reject(err);
      }
    );
  }

  get(url, params, headers) {
    return this.service({
      url,
      params,
      method: "GET",
      headers,
    });
  }

  post(url, data, headers) {
    return this.service({
      url,
      data,
      params: {
        access_token: this.token,
      },
      method: "post",
      headers,
    });
  }

  searchRepositories(params) {
    return this.get("search/repositories", params);
  }

  searchCode(params) {
    return this.get("search/code", params);
  }
}

Gitee.js

import axios from "axois";
import GitServer from "./GitServer.js";
import { log } from "../lib/index.js";

const BASE_URL = "https://gitee.com/api/v5";

export default class Gitee extends GitServer {
  constructor() {
    super();
    this.service = axios.create({
      baseURL: BASE_URL,
      timeout: 5000,
    });

    this.service.interceptors.response.use(
      (response) => {
        return response.data;
      },
      (err) => {
        return Promise.reject(err);
      }
    );
  }

  get(url, params, headers) {
    return this.service({
      url,
      params: {
        ...params,
        access_token: this.token,
      },
      method: "GET",
      headers,
    });
  }

  post(url, data, headers) {
    return this.service({
      url,
      data,
      params: {
        access_token: this.token,
      },
      method: "post",
      headers,
    });
  }

  searchRepositories(params) {
    return this.get("search/repositories", params);
  }
}

2. 安装 axios

lerna add axios packages/utils

3. 修改 packages/install/lib/index.js

import Command from "@coderyjw/command";
import {
  chooseGitPlatForm,
  initGitServer,
  makeList,
  makeInput,
} from "@coderyjw/utils";
const PREV_PAGE = "prev_page";
const NEXT_PAGE = "next_page";
const SEARCH_MODE_REPO = "search_mode_repo";
const SEARCH_MODE_CODE = "search_mode_code";
...
  async action() {
    await this.generateGitAPI();

    await this.searchGitAPI();
    log.verbose("selectedProject", this.selectedProject);
  }
  ...
  async searchGitAPI() {
    log.verbose("this.platForm", this.platForm);
    if (this.platForm === "github") {
      this.mode = await makeList({
        message: "请选择搜索模式",
        choices: [
          { name: "仓库名称", value: SEARCH_MODE_REPO },
          { name: "源码", value: SEARCH_MODE_CODE },
        ],
      });
    }

    // 1. 收集搜索关键词和开发语言
    this.q = await makeInput({
      message: "请输入搜索关键词",
      validate(value) {
        if (value) {
          return true;
        } else {
          return "请输入搜索关键词";
        }
      },
    });

    this.language = await makeInput({
      message: "请输入开发语言",
    });

    this.keywords =
      this.q + (this.language ? `+language:${this.language}` : "");

    log.verbose("search keywords", this.keywords, this.platForm);

    this.page = 1;
    this.perPage = 10;

    await this.doSearch();
  }

  async doSearch() {
    // 2. 根据平台生成搜索参数
    let params;
    let count = 0;
    let list;
    let searchResult;

    if (this.platForm === "github") {
      params = {
        q: this.keywords,
        order: "desc",
        per_page: this.perPage,
        page: this.page,
      };

      log.verbose("search project params", params);
      log.verbose("mode", this.mode);
      if (this.mode === SEARCH_MODE_REPO) {
        searchResult = await this.gitAPI.searchRepositories(params);
        list = searchResult.items.map((item) => ({
          name: `${item.full_name}(${item.description})`,
          value: item.full_name,
        }));
      } else if (this.mode === SEARCH_MODE_CODE) {
        searchResult = await this.gitAPI.searchCode(params);
        list = searchResult.map((item) => ({
          name:
            item.repository.full_name +
            (item.repository.description &&
              `(${item.repository.description})`),
          value: item.repository.full_name,
        }));
      }

      count = searchResult.total_count; // 整体数据量
    } else if (this.platForm === "gitee") {
      params = {
        q: this.q,
        order: "desc",
        per_page: this.perPage,
        page: this.page,
      };
      if (this.language) {
        params.language = this.language; // 注意输入格式: JavaScript
      }
      log.verbose("search params", params);
      searchResult = await this.gitAPI.searchRepositories(params);

      count = 99999;

      list = searchResult.map((item) => ({
        name: `${item.full_name}(${item.description})`,
        value: item.full_name,
      }));
    }
    // 判断当前页面,已经是否达到最大页数
    if (
      (this.platForm === "github" && this.page * this.perPage < count) ||
      (this.platForm === "gitee" && list?.length > 0)
    ) {
      list.push({
        name: "下一页",
        value: NEXT_PAGE,
      });
    }

    if (this.page > 1) {
      list.unshift({
        name: "上一页",
        value: PREV_PAGE,
      });
    }

    if (count > 0) {
      const selectedProject = await makeList({
        message:
          this.platForm === "github"
            ? `请选择要下载的项目(共 ${count} 条数据)`
            : "请选择要下载的项目",
        choices: list,
      });

      if (selectedProject === NEXT_PAGE) {
        await this.nextPage();
      } else if (selectedProject === PREV_PAGE) {
        await this.prevPage();
      } else {
        // 选中项目 去查询 tag
        this.selectedProject = selectedProject;
      }
    }
  }

  async nextPage() {
    this.page++;
    await this.doSearch();
  }

  async prevPage() {
    this.page--;
    await this.doSearch();
  }
  • 解释上面的代码:

    1. 我们首先调用了封装的 searchGitAPI 方法,这个方法是专门查询源码仓库的
    2. searchGitAPI 我们先是进行了平台的判断,因为 github 提供的 api 是可以通过源码查询仓库的
    3. 之后我们进行了一些数据的收集,包括搜索的关键字和开发语言,这些都是调用 github/gitee api 所需要的参数
    4. 然后调用了另一个封装的 doSearch 方法,封装这个方法的目的是为了翻页,因为在翻页的时候还需要再次做查询的操作,代码会重复。
    5. doSearch 方法中我们对不同平台(github/gitee)不同搜索模式(通过仓库名称搜索、通过源码搜索)进行了区分,它们会调用不同的 api
    6. 再拿到搜索到的数据之后,在命令行中以列表的形式显示出来让用户选择
    7. 最后分别封装了 nextPageprePage 进行翻页的操作

4. 我们通过 jw-cli install -d 命令进行测试

image.png

image.png

image.png

image.png

image.png

image.png

image.png

3.4 tag 选择 + 翻页功能开发

tag 的选择和翻页的功能和上一小节是相同的逻辑,这里就不再解释,直接看代码吧

1. 修改 packages/utils/Gitee.js,接入查询 giee tag 的 api

getTags(fullName) {
    return this.get(`/repos/${fullName}/tags`);
}

2. 修改 packages/utils/Github.js,接入查询 github tag 的 api

getTags(fullName, params) {
    return this.get(`/repos/${fullName}/tags`, params);
}

3. 修改 packages/install/lib/index.js

...
  async action() {
    await this.generateGitAPI();

    await this.searchGitAPI();
    log.verbose("selectedProject", this.selectedProject);
    log.verbose("selectedTag", this.selectedTag);
  }
  ...
  async searchGitAPI() {
      ...
      ...
      await this.selectTags();
  }
...
  async selectTags() {
    let tagsList;
    this.tagPage = 1;
    this.tagPerPage = 30;
    tagsList = await this.doSelectTags();
  }

  async doSelectTags() {
    let tagsListChoices = [];
    let tagsList
    if (this.platForm === "github") {
      const params = {
        page: this.tagPage,
        per_page: this.tagPerPage,
      };
      log.verbose("search tags params", this.selectedProject, params);
       tagsList = await this.gitAPI.getTags(this.selectedProject, params);
      tagsListChoices = tagsList.map((item) => ({
        name: item.name,
        value: item.name,
      }));
    } else {
      log.verbose("search tags params", this.selectedProject);
       tagsList = await this.gitAPI.getTags(this.selectedProject);
      tagsListChoices = tagsList.map((item) => ({
        name: item.name,
        value: item.name,
      }));
    }
    if (tagsList.length === 0) return;

    if (tagsList.length > 0) {
      tagsListChoices.push({
        name: "下一页",
        value: NEXT_PAGE,
      });
    }
    if (this.tagPage > 1) {
      tagsListChoices.unshift({
        name: "上一页",
        value: PREV_PAGE,
      });
    }
    const selectedTag = await makeList({
      message: "请选择tag",
      choices: tagsListChoices,
    });

    if (selectedTag === NEXT_PAGE) {
      await this.nextTags();
    } else if (selectedTag === PREV_PAGE) {
      await this.prevTags();
    } else {
      this.selectedTag = selectedTag;
    }
  }

  async prevTags() {
    this.tagPage--;
    await this.doSelectTags();
  }

  async nextTags() {
    this.tagPage++;
    await this.doSelectTags();
  }

4. 测试

image.png

3.5 下载指定分支源码

选择完仓库名和分支后就可以去下载仓库了,这个功能我们可以通过 execa 模块执行 git clone 命令实现

  1. 修改 packages/install/lib/index.js

  async action() {
    await this.generateGitAPI();

    await this.searchGitAPI();

    log.verbose("selectedProject", this.selectedProject);
    log.verbose("selectedTag", this.selectedTag);

+    await this.downloadRepo();
  }
  
  ...
+  async downloadRepo() {
+    const spinner = ora(
+      `正在下载:${this.selectedProject}` +
+        (this.selectedTag ? `(${this.selectedTag})` : "")
+    ).start();
+    try {
+      await this.gitAPI.cloneRepo(this.selectedProject, this.selectedTag);
+      spinner.stop();
+      log.success(
+        `下载模板成功:${this.selectedProject}` +
+          (this.selectedTag ? `(${this.selectedTag})` : "")
+      );
+    } catch (err) {
+      spinner.stop();
+      printErrorLog(err);
+    }
+  }
  1. 修改 packages/utlis/git/GitServer.js
+  cloneRepo(fullName, tag) {
+    if (tag) {
+      return execa("git", ["clone", this.getRepoUrl(fullName), "-b", tag]);
+    }
+    return execa("git", ["clone", this.getRepoUrl(fullName)]);
+  }
  1. 修改 packages/utlis/git/Github.js
+  getRepoUrl(fullName) {
+    return `https://github.com/${fullName}.git`;
+  }
  1. 修改 packages/utlis/git/Gitee.js
+  getRepoUrl(fullName) {
+    return `https://gitee.com/${fullName}.git`;
+  }
  1. 安装依赖
lerna add ora packages/install

3.6 自动安装依赖 + 启动项目

  1. 修改 packages/install/lib/index.js
  async downloadRepo() {
    const spinner = ora(
      `正在下载:${this.selectedProject}` +
        (this.selectedTag ? `(${this.selectedTag})` : "")
    ).start();
    try {
      await this.gitAPI.cloneRepo(this.selectedProject, this.selectedTag);
      spinner.stop();
      log.success(
        `下载模板成功:${this.selectedProject}` +
          (this.selectedTag ? `(${this.selectedTag})` : "")
      );
+      await this.installDependencies();
+      await this.runRepo();
    } catch (err) {
      spinner.stop();
      printErrorLog(err);
    }
  }
  
+  async installDependencies() {
+    const spinner = ora(
+      `正在安装依赖:${this.selectedProject}` +
+        (this.selectedTag ? `(${this.selectedTag})` : "")
+    ).start();
+    try {
+      const ret = await this.gitAPI.installDependencies(
+        process.cwd(),
+        this.selectedProject,
+        this.selectedTag
+      );
+      spinner.stop();
+      if (ret) {
+        log.success(
+          `依赖安装安装成功:${this.selectedProject}` +
+            (this.selectedTag ? `(${this.selectedTag})` : "")
+        );
+      } else {
+        log.error("依赖安装失败");
+      }
+    } catch (err) {
+      spinner.stop();
+      printErrorLog(err);
+    }
+  }

  async runRepo() {
    await this.gitAPI.runRepo(process.cwd(), this.selectedProject);
  }
  1. 修改 packages/utils/git/GitServer.js

function getProjectPath(cwd, fullName) {
  const projectName = fullName.split("/")[1]; // vuejs/vue => vue
  const projectPath = path.resolve(cwd, projectName);
  return projectPath;
}

function getPackageJson(cwd, fullName) {
  const projectPath = getProjectPath(cwd, fullName);
  const pkgPath = path.resolve(projectPath, "package.json");
  if (pathExistsSync(pkgPath)) {
    return fse.readJsonSync(pkgPath);
  } else {
    return null;
  }
}

...
export default class GitServer {
...
  installDependencies(cwd, fullName, tag) {
    const projectPath = getProjectPath(cwd, fullName);
    if (pathExistsSync(projectPath)) {
      return execa("npm", ["install", "--registry=https://registry.npmmirror.com"], { cwd: projectPath });
    }

    return null;
  }

  runRepo(cwd, fullName) {
    const projectPath = getProjectPath(cwd, fullName);
    const pkg = getPackageJson(cwd, fullName);
    if (pkg) {
      const { scripts, bin } = pkg;

      if (bin) {
        execa("npm", ["run", "-g", name, "--registry=https://registry.npmmirror.com"], {
          cwd: projectPath,
          stdout: "inherit",
        });
      }

      if (scripts && scripts.dev) {
        return execa("npm", ["run", "dev"], {
          cwd: projectPath,
          stdout: "inherit",
        });
      } else if (scripts && scripts.serve) {
        return execa("npm", ["run", "serve"], {
          cwd: projectPath,
          stdout: "inherit",
        });
      } else if (scripts && scripts.start) {
        return execa("npm", ["run", "start"], {
          cwd: projectPath,
          stdout: "inherit",
        });
      } else {
        log.warn("未找到启动命令");
      }
    } else {
    }
  }

那么至此我们的源码下载脚手架就已经开发完成了,最后我们用一张图总结所有流程

image.png

image.png

相关文章
|
5天前
|
JSON 前端开发 API
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
29 5
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
|
14天前
|
Dart 前端开发 Android开发
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
37 4
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
16天前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
121 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
24天前
|
Dart 前端开发 容器
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
75 18
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
26天前
|
缓存 前端开发 IDE
【06】flutter完成注册页面-密码登录-手机短信验证-找回密码相关页面-并且实现静态跳转打包demo做演示-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【06】flutter完成注册页面-密码登录-手机短信验证-找回密码相关页面-并且实现静态跳转打包demo做演示-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
27 0
【06】flutter完成注册页面-密码登录-手机短信验证-找回密码相关页面-并且实现静态跳转打包demo做演示-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
27天前
|
Dart 前端开发
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
116 75
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
29天前
|
缓存 前端开发 Android开发
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
79 12
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
|
1月前
|
前端开发 Java 开发工具
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
81 18
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
|
1月前
|
Dart 前端开发 Android开发
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
36 1
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
3月前
|
监控 前端开发 数据可视化
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
@icraft/player-react 是 iCraft Editor 推出的 React 组件库,旨在简化3D数字孪生场景的前端集成。它支持零配置快速接入、自定义插件、丰富的事件和方法、动画控制及实时数据接入,帮助开发者轻松实现3D场景与React项目的无缝融合。
280 8
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生

热门文章

最新文章

  • 1
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 2
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 3
    【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
  • 4
    详解智能编码在前端研发的创新应用
  • 5
    巧用通义灵码,提升前端研发效率
  • 6
    智能编码在前端研发的创新应用
  • 7
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
  • 8
    【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
  • 9
    抛弃node和vscode,如何用记事本开发出一个完整的vue前端项目
  • 10
    大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
  • 1
    以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
    29
  • 2
    大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
    51
  • 3
    【2025优雅草开源计划进行中01】-针对web前端开发初学者使用-优雅草科技官网-纯静态页面html+css+JavaScript可直接下载使用-开源-首页为优雅草吴银满工程师原创-优雅草卓伊凡发布
    26
  • 4
    巧用通义灵码,提升前端研发效率
    93
  • 5
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    141
  • 6
    详解智能编码在前端研发的创新应用
    96
  • 7
    智能编码在前端研发的创新应用
    83
  • 8
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    37
  • 9
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    121
  • 10
    【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
    75