从零开始,打造一个属于自己的react脚手架

1. 准备

首先,需要确保你已经安装了 node,然后我们来初始化一个项目

bash 复制代码
mkdir mycli-demo & cd mycli-demo
npm init -y

2. 添加一个简单的命令

在 package.json 中添加 bin 配置,mycli是我们 cli 工具提供的命令名称,对应的值是文件入口,我们这里指向bin/cli.js文件

diff 复制代码
{
  "name": "mycli-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
+ "bin":{
+   "mycli": "./bin/cli.js"
+ },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

在项目中创建bin/cli.js,并打印一段文字

js 复制代码
#!/usr/bin/env node

console.log("i'm a cli");

第一行很重要,用来指明运行环境

此时我们可以再控制台运行mycli

你可能会有错误:控制台找不到我们的cli工具,这是因为我们在使用一个工具的时候必须安装它,例如你没有安装npm的时候去使用也会遇到同样的错误。

由于我们的mycli并没有发布,因此可以借助npm link或者yarn link选择本地安装,执行:

bash 复制代码
npm link

yarn link

如果你觉得 npm link 和 yarn link 比较繁琐,你也可以使用yalc 进行本地调试

然后再执行mycli,就可以看到控制台输出了bin/cli.js中打印的内容

至此,我们已经了解cli工具是如何作用的,下面我们在此基础上做一些改进,让他可以处理参数,彩色打印,显示加载中等等功能

3. 处理参数

很多时候我们需要在运行cli工具时携带参数,那么如何获取到这个参数呢?

node程序中,通过process.argv可获取到命令的参数,以数组返回

可以看到其实我们已经拿到各个参数,但是这样的获取方式不太直观,所以我们引入一个第三方 npm 包帮我们处理这部分功能:commander,参考文档:commander 文档

bash 复制代码
npm i commander -S

我们在bin/cli.js添加如下代码

diff 复制代码
#!/usr/bin/env node
+ const program = require('commander')

console.log("i'm a cli")

// 打印参数
console.log(process.argv)

+ program
+  .command('create <projectName>')
+  .description('create a new project')
+  .alias('c')
+  .option('-u, --umi', 'umi react template')
+  .option('-v, --vite', 'vite react template')
+  .option('-w, --webpack', 'webpack react template')
+  .action((projectName, options) => {
+    console.log(projectName, options)
+ })
+ program.version('1.0.0').parse(process.argv)

到目前为止,我们可以直观的获取到命令的参数,你可以在控制台尝试一下

4. 交互式命令

有的时候我们可能需要在命令行工具中融入一些交互,根据用户的输入或者选择生成一些东西或者做相应的操作。我们可以引入一个npm包来帮我们实现:inquirer

bash 复制代码
npm i inquirer -S

我们在bin/cli.js添加如下代码

diff 复制代码
#!/usr/bin/env node
const program = require('commander')
+ const inquirer = require('inquirer')

console.log("i'm a cli")

// 打印参数
console.log(process.argv)

program
  .command('create <projectName>')
  .description('create a new project')
  .alias('c')
  .option('-u, --umi', 'umi react template')
  .option('-v, --vite', 'vite react template')
  .option('-w, --webpack', 'webpack react template')
  .action((projectName, options) => {
    console.log(projectName, options)
+   inquirer
+     .prompt([
+       {
+         type: 'list',
+         name: 'frameTemplate',
+         message: '请选择框架类型',
+         choices: ['vite react', 'umi react', 'webpack react']
+       }
+     ])
+     .then((answer) => {
+       console.log(answer)
+     })
  })
program.version('1.0.0').parse(process.argv)

我们在控制台运行

bash 复制代码
mycli create test -r

至此,我们完成了交互式命令,下面我将完成一个模板的下载,一起动手吧!

5. 完成一个模板下载

在前面的步骤中,我们发现我们的日志打印不是很友好,我们可以通过log-symblos chalk ora 帮我们做一些提示信息的优化

npm 包名称 作用 官网 备注
chalk 修改控制台中字符串的样式 chalk 字体样式、颜色、背景颜色
log-symbols 各种日志级别的彩色符号 log-symbols 从 5 版本开始使用 ESMRelease v5.0.0
ora 终端加载效果 ora

由于 log-symbols 从 5 版本开始使用 ESM,所以我们这里使用 4 版本

bash 复制代码
npm i chalk log-symbols@4 ora -S

要下载一个 github 仓库的代码,我们需要引入download-git-repo, download-git-repo - npm

bash 复制代码
npm i download-git-repo -S

我们来实现一个简单的下载功能

diff 复制代码
#!/usr/bin/env node
const program = require('commander')
const inquirer = require('inquirer')
+ const ora = require('ora')
+ const download = require('download-git-repo')
+ const { errLog, successLog } = require('../src/utils/log.js')

console.log("i'm a cli")

// 打印参数
console.log(process.argv)

program
  .command('create <projectName>')
  .description('create a new project')
  .alias('c')
  .option('-r, --react', 'react template')
  .option('-v, --vue', 'vue template')
  .option('-v2, --vue2', 'vue2 template')
  .option('-v3, --vue3', 'vue3 template')
  .action((projectName, options) => {
    console.log(projectName, options)
    inquirer
      .prompt([
        {
          type: 'list',
          name: 'frameTemplate',
          message: '请选择框架类型',
          choices: ['Vue3', 'Vue2', 'React']
        }
      ])
      .then((answer) => {
        console.log(answer)
+       const spinner = ora()
+       spinner.text = '正在下载模板...'
+       spinner.start()
+       download(
+         '',
+         projectName,
+         { clone: true },
+         function (err) {
+           if (err) {
+             spinner.fail('模板下载失败')
+             errLog(err)
+           } else {
+             spinner.succeed('模板下载成功')
+             successLog('项目初始化完成')
+           }
+         }
+       )
      })
  })
program.version('1.0.0').parse(process.argv)

在控制台输入

bash 复制代码
 mycli create test -r

可以看到模板下载成功

如果下载不成功,请详细查看download-git-repo的文档:download-git-repo - npm

至此,我们已经实现了一个简单的模板仓库下载功能;其中项目的下载地址可以去设置为一个或多个git仓库,更多的功能大家可以自行尝试。

6. 项目优化

如果我们有多个命令,那么我们就需要写多个

js 复制代码
program
  .command("")
  .description("")
  .alias("")
  .option("")
  .action(() => {
    // 命令处理
  });
  1. 因为这部分代码被重复使用,我们自然而然想到了遍历,首先声明一个 list 变量用来维护我们的命令配置,在 src 新建 command-config.js 文件,该文件导出配置
js 复制代码
const COMMAND_LIST = [
  {
    command: "create <projectName>",
    description: "create a new project",
    alias: "c",
    options:  ['-u, --umi', 'umi react template'],
      ['-v, --vite', 'vite react template'],
      ['-w, --webpack', 'webpack react template'],,
    action: require("./commandHandler/create"),
    examples: ['-u', '--umi', '-v', '--vite', '-w, --webpack'].map((v) => `create projectName ${v}`),
  },
];

module.exports = COMMAND_LIST;

修改bin/cli.js

js 复制代码
/**
 * 注册option
 * @param {Object} commander commander实例
 * @param {Object} option 每个命令配置对象
 * @returns commander
 */
const registerOption = (commander, option) => {
  return option && option.length ? commander.option(...option) : commander;
};
/**
 * 注册action
 * @param {Object} commander commander实例
 * @param {Object} commandEle 每个命令配置对象
 * @returns commander
 */
const registerAction = (commander, commandEle) => {
  const { command, description, alias, options, action } = commandEle;
  const c = commander
    .command(command) // 命令的名称
    .description(description) // 命令的描述
    .alias(alias);
  // 循环options
  options && options.reduce(registerOption, c);
  c.action(action);
  return commander;
};

// 循环创建命令
COMMAND_LIST.reduce(registerAction, program);
  1. 由于命令处理部分代码量较大,所以我们考虑把命令处理的函数提取在一个文件夹下,我们在 src 下新建 commandHandler 目录,并新建一个 create.js,把 create 命令的处理代码放进 create.js

  2. 为了更方便使用,我们改写 mycli --help 命令

js 复制代码
// help命令 把example显示出去
const help = () => {
  console.log("\n");
  console.log(chalk.green("如何使用:"));
  COMMAND_LIST.forEach((command, index) => {
    console.log(
      "  ",
      chalk.keyword("orange")(index + 1),
      `${command.command}命令`
    );
    command.examples.forEach((example) => {
      console.log(`     - mycli ${example}`);
    });
  });
};

program.on("-h", help);
program.on("--help", help);
  1. 为了更方便使用,我们加入对 npx 的支持
diff 复制代码
{
  "name": "mycli-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin":{
     "mycli": "./bin/cli.js"
  },
  "scripts": {
+   "start": "./bin/cli.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

5.优化我们的下载函数

javascript 复制代码
/**
 * 下载函数,用于下载模板并安装依赖
 * @param {string} answer - 用户输入答案
 * @param {string} projectName - 项目名称
 * @returns {Promise<void>} 无返回值
 */
const downloadFn = async (answer, projectName) => {
  const url = SOURCESURL[answer?.frameTemplate];
  const spinner = ora()
  spinner.text = '正在下载模板...'
  spinner.start()
  try {
    await new Promise((resolve, reject) => {
      download(`direct:${url}`, projectName, { clone: true }, (err) => {
        if (err) {
          reject(err);
        } else {
          resolve();
        }
      });
    });

    spinner.text = '模板下载成功,正在安装依赖'

    // 进入目录
    process.chdir(projectName);

    // 执行npm install命令
    const { stdout, stderr } = await exec('npm install');
    console.log(stdout);
    spinner.succeed('依赖完成')
    successLog('项目初始化完成')
    return
  } catch (err) {
    spinner.fail('模板下载失败')
    console.error(err);
    errLog(err)
  }
}

至此我们完成了一个简易版 cli 工具的开发,后续优化版本将会直接发布至 npm。

另附:

github地址:github.com/Shen-zhihao...

gitee地址:gitee.com/laborer996/...

npm项目地址:www.npmjs.com/package/cre...

相关推荐
小公主1 分钟前
如何利用闭包封装私有变量?掌握防抖、节流与 this 问题的巧妙解决方案
前端
烛阴4 分钟前
JavaScript 的动态魔法:使用 constructor 动态创建函数
前端·javascript
前端 贾公子15 分钟前
tailwind ( uni ) === 自定义主题
前端
独立开阀者_FwtCoder23 分钟前
大制作!在线 CSS 动效 编辑神器!太炫酷了!
前端·javascript·github
白白李媛媛25 分钟前
vue3中实现echarts打印功能
前端·vue.js·echarts
尘心cx27 分钟前
前端-HTML-day2
前端·html
歘chua28 分钟前
组件封装:封装一个可复用的crud界面的思路
前端
徐小夕29 分钟前
牛!用vue3实现的多维表格编辑器,小白也能轻松构建复杂数据报表!
前端·javascript·vue.js
爱编程的喵31 分钟前
CSS动画实战:从零打造一个超萌的小球亲亲动画
前端·css
zmirror32 分钟前
Webpack5 多页面实践
前端