如何开发一个cli工具

5,767 阅读5分钟

脚手架可以帮助我们通过简单的命令来快速构建内容,目前比较常见的脚手架有vue-cli、create-react-app等,它是团队提升效率的重要手段。我们可以结合公司的业务来开发自己的脚手架工具。下面我将一步一步的开发一个脚手架工具,跟着我一起吧!

1. 准备

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

mkdir mycli-demo & cd mycli-demo
npm init -y

image-20210616162547762.png

2. 添加一个简单的命令

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

{
  "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,并打印一段文字

#!/usr/bin/env node

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

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

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

image-20210616161849805.png

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

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

npm link

yarn link

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

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

image-20210616164824467.png

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

3. 处理参数

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

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

image-20210616165851713.png

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

npm i commander -S

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

#!/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('-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)
+ })
+ program.version('1.0.0').parse(process.argv)

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

4. 交互式命令

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

npm i inquirer -S

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

#!/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('-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)
+     })
  })
program.version('1.0.0').parse(process.argv)

我们在控制台运行

mycli create test -r

得到:

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

5. 完成一个模板下载

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

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

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

npm i chalk log-symbols@4 ora -S

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

npm i download-git-repo -S

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

#!/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)

在控制台输入

 mycli create test -r

可以看到模板下载成功

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

至此,我们已经实现了一个简单的模板仓库下载功能。更多的功能大家可以自行尝试。

如果我们想让其他小伙伴使用,我们需要把这个cli发布到npm,如何发布不在此篇文章展开叙述。

6. 项目优化

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

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

module.exports = COMMAND_LIST

修改bin/cli.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命令


// 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)

此时我们输入mycli --help

image.png

循环部分可自行再进行优化

至此我们完成了一个简易版cli工具的开发

源码:github.com/enbrands/st…

main分支只git init该项目,其他分支每个分支完成一个步骤

作者:李云飞