当前位置: 首页 > 工具软件 > Parse CLI > 使用案例 >

从0构建一个cli

欧阳博超
2023-12-01

前言

其实很早我就想了解vue-cli react-cli 的工作原理了,想了解本质是什么,作用又是什么,运用了啥。其实使用这一些工具不难,但是内心总会有疑惑,不理解其原理,总感觉自己有点慌,不知道大家有没有这样的感悟。正好最近在实习的公司接触到他们自己搭建的脚手架和内部封装的基础项目模版,所以最近就自己看博客,边理解,自己跟着写了一个cli,功能不多,但是一个cli应该拥有的功能都实现了。自己想的有一些功能还没有弄上去,所以这一篇文章先用来记录所用到的功能库和具体步骤,防止自己忘记,好记性不如烂笔头。

脚手架作用是什么?

想必大家也有想过这个问题吧,说实话,没去认真搜过几篇文章,还真不知道cli是干什么的。vue项目文件是怎么来的了?为啥输入命令我们就能创建一个自己命名的项目文件了?我们所敲的命令是怎么形成的了?总不会空穴来风吧,哈。

cli的作用:
其实cli脚手架就是从远端库(GitHub)里面拉取(请求)我们需要的模版文件,根据用户在命令行中的选项,生成对应的项目,然后返回给用户,通过相应的js库文件,在本地创建项目。

cli中用到的基本js库

1, commander 用来创建命令行命令
2, inquirer 用来创建命令行选择,并获得用户的选择结果
3, chalk 命令行美化库
4, cross-spawn 跨系统运行
5, axios 请求文件库
6, ora 控制台动画效果
7, download-git-repo git拉取文件
8, fs-extra fs模块的扩展

接下来通过一个实例cli来介绍这一些库

一,配置项目的运行环境

1,创建一个文件夹
2,npm init 实例化一个package.json 文件(这里确保自己电脑是安装了node)
3,然后在package.json 文件中加入

“bin":{
	"song":"./bin/cli.js"//这里运行的命令随意,我就以song为命令
}

4,在根目录创建对应的文件bin>cli.js
5, cli文件中添加node环境运行的代码(没必要手动 node + 文件)

#! /usr/bin/env node

在文件中可以试着答应一个hello world,然后保存

6,使用npm link 链接全局,bin目录下的文件为运行文件
npm link 的作用:作用

7,然后在命令行中运行 song 就可以看到运行结果。

二,搭建命令行命令

通过第三方库进行创建

npm install commander --save

因为运行的文件是cli,所以命令行命令的创建理所当然的在cli文件中编写。

#! /usr/bin/env node

const program = require('commander')

program
  // 定义命令和参数
  .command('create <app-name>')
  .description('create a new project')
  // -f or --force 为强制创建,如果创建的目录存在则直接覆盖
  .option('-f, --force', 'overwrite target directory if it exist')
  .action((name, options) => {
    // 打印执行结果
    console.log('name:',name,'options:',options)
  })
  
program
   // 配置版本号信息
  .version(`v${require('../package.json').version}`)
  .usage('<command> [option]')
  
// 解析用户执行命令传入参数
program.parse(process.argv);

在命令行运行song 进行查看

其实写到这里,感觉创建命令不难,只是使用了特定的库文件。

三,封装一个文件用来执行命令

在根目录创建一个lib 文件夹,新建一个create.js 文件,然后在cli.js全局执行文件中引入,在命令的action执行函数中执行这个create.js文件。

思考:
在创建项目目录文件时,思考有没有重名的文件夹已经存在,是创建过另外一个名字的文件夹,还是覆盖,还是取消。
当 { force: true } 时,直接移除原来的目录,直接创建
当 { force: false } 时 询问用户是否需要覆盖

对文件进行操作,那么就得引入文件操作模块fs-extra
fs-extra是node fs模块的升级版本,这里用它来判别是否已经存在文件
node fs不支持promise,fs-extra支持

npm  install fs-extra --save

修改create.js 文件

// lib/create.js

const path = require('path')
const fs = require('fs-extra')

module.exports = async function (name, options) {
  // 执行创建命令

  // 当前命令行选择的目录
  const cwd  = process.cwd();
  // 需要创建的目录地址
  const targetAir  = path.join(cwd, name)

  // 目录是否已经存在?
  if (fs.existsSync(targetAir)) {

    // 是否为强制创建?
    if (options.force) {
      await fs.remove(targetAir)
    } else {
      // TODO:询问用户是否确定要覆盖
    }
  }
}

四,创建其他命令

和上面cli 文件中的配置一样

五,命令行 代码美化

npm install chalk --save

chalk库提供了很多对应颜色的API,可以通过自己想要的颜色,设置对应的API就行

chalk官网:
chalk

六,询问用户,并获得用户的选择结果

这里在命令行创建选择行为和获得用户的结果使用inquirer库

npm install inquirer --save

例子:

// lib/create.js

const path = require('path')

// fs-extra 是对 fs 模块的扩展,支持 promise 语法
const fs = require('fs-extra')
const inquirer = require('inquirer')

module.exports = async function (name, options) {
  // 执行创建命令

  // 当前命令行选择的目录
  const cwd  = process.cwd();
  // 需要创建的目录地址
  const targetAir  = path.join(cwd, name)

  // 目录是否已经存在?
  if (fs.existsSync(targetAir)) {

    // 是否为强制创建?
    if (options.force) {
      await fs.remove(targetAir)
    } else {

      // 询问用户是否确定要覆盖
      let { action } = await inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: 'Target directory already exists Pick an action:',
          choices: [
            {
              name: 'Overwrite',
              value: 'overwrite'
            },{
              name: 'Cancel',
              value: false
            }
          ]
        }
      ])

      if (!action) {
        return;
      } else if (action === 'overwrite') {
        // 移除已存在的目录
        console.log(`\r\nRemoving...`)
        await fs.remove(targetAir)
      }
    }
  }
}

现在运行song,就可以看到命令行出现原则了

七,整合用户的选择结果

通过上一步的inquirer执行后,可以获得用户的选择结果,我们可以通过用户的选择结果,在进行处理

八,通过axios发送请求,获得想要的数据(非必需)

久,拉取远端库里面的项目模版

这里以git仓库为例子,首先确保自己按装了git环境,然后下载download-git-repo工具包,通过这个工具包就可以实现从远端拉取文件,并放置在我们创建好的项目文件夹下面。由于download-git-repo这个工具包不支持promise,所以通过node的util模块将它promise化。方便操作。

npm install download-git-repo --save

promise化

// lib/Generator.js

...
const util = require('util')
const path = require('path')
const downloadGitRepo = require('download-git-repo') // 不支持 Promise

// 添加加载动画
async function wrapLoading(fn, message, ...args) {
  ...
}

class Generator {
  constructor (name, targetDir){
    ...

    // 对 download-git-repo 进行 promise 化改造
    this.downloadGitRepo = util.promisify(downloadGitRepo);
  }
  ...
  
  // 下载远程模板
  // 1)拼接下载地址
  // 2)调用下载方法
  async download(repo, tag){

    // 1)拼接下载地址
    const requestUrl = `zhurong-cli/${repo}${tag?'#'+tag:''}`;

    // 2)调用下载方法
    await wrapLoading(
      this.downloadGitRepo, // 远程下载方法
      'waiting download template', // 加载提示信息
      requestUrl, // 参数1: 下载地址
      path.resolve(process.cwd(), this.targetDir)) // 参数2: 创建位置
  }

  // 核心创建逻辑
  // 1)获取模板名称
  // 2)获取 tag 名称
  // 3)下载模板到模板目录
  // 4)模板使用提示
  async create(){

    // 1)获取模板名称
    const repo = await this.getRepo()

    // 2) 获取 tag 名称
    const tag = await this.getTag(repo)

    // 3)下载模板到模板目录
    await this.download(repo, tag)
    
    // 4)模板使用提示
    console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)
    console.log(`\r\n  cd ${chalk.cyan(this.name)}`)
    console.log('  npm run dev\r\n')
  }
}

module.exports = Generator;

说明:通过之前的步骤我们是可以获得,模版项目的git仓库的地址的,所以通过只要将这个地址传入这个工具包实例就行。

十,cli创建完成,发布

通过上面的步骤就可以实现一个简单的cli,完善package.json文件之后,我们可以通过在npm官网上注册,然后npm publish 发布,

commander介绍

commander函数为全局提供了一个全局对象,通过这个对象去进行操作

const {program} = require('commander');
//也可以通过实例化获得
import {Command} from 'commander/esm.mjs';
const program = new Command()

全局对象的API

command 选项
option()

Commander 使用.option()方法来定义选项,同时可以附加选项的简介。每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(–后面接一个或多个单词),使用逗号、空格或|分隔。

解析后的选项可以通过Command对象上的.opts()方法获取,同时会被传递给命令处理函数。可以使用.getOptionValue()和.setOptionValue()操作单个选项的值。

对于多个单词的长选项,选项名会转为驼峰命名法(camel-case),例如–template-engine选项可通过program.opts().templateEngine获取。

多个短选项可以合并简写,其中最后一个选项可以附加参数。 例如,-a -b -p 80也可以写为-ab -p80,甚至进一步简化为-abp80。

–可以标记选项的结束,后续的参数均不会被命令解释,可以正常使用。

默认情况下,选项在命令行中的顺序不固定,一个选项可以在其他参数之前或之后指定。

常用选项类型,boolean 型选项和带参数选项
有两种最常用的选项,一类是 boolean 型选项,选项无需配置参数,另一类选项则可以设置参数(使用尖括号声明在该选项后,如–expect )。如果在命令行中不指定具体的选项及参数,则会被定义为undefined。

program
  .option('-d, --debug', 'output extra debugging')
  .option('-s, --small', 'small pizza size')
  .option('-p, --pizza-type <type>', 'flavour of pizza');

program.parse(process.argv);

const options = program.opts();
//**用来处理对应命令使用的选项的回调函数**
if (options.debug) console.log(options);
console.log('pizza details:');
if (options.small) console.log('- small pizza size');
if (options.pizzaType) console.log(`- ${options.pizzaType}`);

requireOption(). // 用来设置必填选项

command()创建命令

program
	//接收两个参数,命令名称,参数
  .command('clone <source> [destination]')
  //命令的描述
  .description('clone a repository into a newly created directory')
  //命令执行的回调
  .action((source, destination) => {
    console.log('clone command called');
  });

command另外一种参数制定方式:
.argument(“1”,‘2’,’<3>’,’[4]’)
尖括号表示必须参数,方括号表示可选参数

.hook() 执行生命周期函数

program
  .option('-t, --trace', 'display trace statements for commands')
  .hook('preAction', (thisCommand, actionCommand) => {
    if (thisCommand.opts().trace) {
      console.log(`About to call action handler for subcommand: ${actionCommand.name()}`);
      console.log('arguments: %O', actionCommand.args);
      console.log('options: %o', actionCommand.opts());
    }
  });

preAction:在本命令或其子命令的处理函数执行前
postAction:在本命令或其子命令的处理函数执行后

command 的其他API
1,–help -h 是默认的回展示所有的信息
2,addHelpText 额外添加帮助信息,参数1:命令,参数2:描述
3,showHelpAfterError 命令行命令出错的处理
4,on 用来监听命令和选项

//监听选项
program.on('option:verbose', function () {
  process.env.VERBOSE = this.opts().verbose;
});
//监听命令
program.on('command:*', function (operands) {
  console.error(`error: unknown command '${operands[0]}'`);
  const availableCommands = program.commands.map(cmd => cmd.name());
  mySuggestBestMatch(operands[0], availableCommands);
  process.exitCode = 1;
});

5,version 用来制定npm包的版本

官方网址:
commander

inquirer介绍

inquirer是什么?
Inquirer是基于诺言的npm软件包,用于Node项目中,以创建用于基于查询的任务的CLI(命令行界面)工具。 询问用户问题,验证用户输入并根据给定的响应进行操作非常好。

例子:

// promise 风格
var inquirer = require('inquirer');
inquirer
  .prompt([
  //用来配置选择
    /* Pass your questions in here */
  ])
  .then((answers) => {
  //回调函数,可以获得用户的选择结果
    // Use user feedback for... whatever!!
  })
  .catch((error) => {
  // 处理错误
    if (error.isTtyError) {
      // Prompt couldn't be rendered in the current environment
    } else {
      // Something else went wrong
    }
  });

选择框配置这里就不细讲了,可以参考inquirer网站:
inquirer官网
对应中文博客

总结

通过上面的大致介绍(没有细致到每一步),我们应该可以大致了解cli创建的过程里吧。主要的思想就是,通过创建命令,然后执行(commander),通过inquirer库获得用户的选择结果,然后整合用户的选择,使用axios获得参数或者直接判断是拉取哪种模版,然后获得git的地址,最后通过download-git-repo拉取git上的代码。

参考文献:
从0手动构建自己的cli
仿vue-cli构建脚手架

 类似资料: