你也可以拥有自己的CLI

204 阅读4分钟

上文 从头开始构建你的开发生态系统:自定义脚手架的完整指南(一)已经讲解了cli相关的知识, 本文就着重讲解一下实操,手把手实现一个自己的cli脚手架,由于上篇文章已经对cli相关知识点讲解过了,本文就直接上代码了,不会太多原理解析

废话不多说,直接上干货.

准备工作

  1. 先初始化一个项目
  2. 然后新建bin目录,新增index.js文件
  3. package.json中新增bin配置
cd demo-cli
npm init -y

当前目录结构

    bin          
    └─ index.js  
    package.json

package.json

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

ps: bin中的那个key看自己喜好定义,可定义多个,在本文中定义为cli;

这个bin/index.js文件就是触发cli全局变量时会执行的文件, 添加简单代码

#!/usr/bin/env node

console.log('我是cli---');

然后执行npm link 然后进行测试,在控制台输入cli 回车,就会发现控制台会输出'我是cli---'

image.png 到这里 恭喜你,以及拥有一个属于自己的脚手架了, 哈哈


开个玩笑,继续上菜

解析命令行参数

当我们使用vue-cli的时候,会经常使用如下指令来创建一个项目,

vue create project-name
// or
vue create project-name --force

那么该怎么定义create project-name 或者 --force这些指令? 该怎么解析这些参数呢? 上面的准备工作中,我们已经拥有了cli这个全局指令, 那么我如果输入cli create name, 我怎么获取到这些cli后面的参数呢? 这里先采用原生的方法,process.argv 属性返回一个数组,这个数组包含了启动Node.js进程时的命令行参数, 我们现在bin/index.js中新增一行代码

#!/usr/bin/env node
let argv = process.argv
console.log(argv);

然后控制台输入 cli create name,回车后看控制台可以看到

image.png 可以看到,输入的参数是存在在数组的第二位及以后的,也就是说

let argv = process.argv.slice(2) // log [ 'create', 'name' ]

但是这样做,对参数的解析,校验要写很多的逻辑,有没有一个工具能让我们自定义指令和校验参数呢,答案是当然有的,commander 编写代码来描述你的命令行界面。 Commander 负责将参数解析为选项和命令参数,为问题显示使用错误,并实现一个有帮助的系统。

Commander 是严格的,并且会针对无法识别的选项显示错误。 两种最常用的选项类型是布尔选项,和从参数中获取值的选项。

然后让我们安装它 npm install commander -D 然后在bin/index.js中写入

#!/usr/bin/env node
const { Command } = require('commander')
const chalk = require('chalk')



const program = new Command()
const { version, name } = require('../package.json')
const log = console.log;
program.version(version).usage("command <options>");

program.on("--help", () => {
    log(`Run ${chalk.red(`${name} <command> --help`)} show details`);
});

program
    .command("create <project-name>")
    .description("创建一个项目")
    .option("-f --force", "overwrite target if it exists")
    .action((name, options) => {
        if (!name) {
            console.log(chalk.red('请输入项目名称'));
            return
        }
        console.log(options);
        log('执行create操作')
    });

program
    .command("add")
    .description("add a new template")
    .action(() => {
        log('执行add操作')
    });
program
    .command("list")
    .description("see template list")
    .action(() => {
        log('执行list操作')
    });
program.parse(process.argv);

if (!program.args.length) {
    program.help();
}




看到引用的包还会发现存在一个chalk的包

chalk 控制log在控制台输出的颜色

  • 控制log在控制台输出的颜色
  • "chalk": "2.4.2" 新版的是采用的ESM模式,如果在node中使用,就安装开头的这个版本

回归主线,看到上述代码. 表示注册了四个指令以及help帮助提示, 控制台输入如下

image.png

然后我们可以针对不同的指令编写不同的代码, 以add为例,为了代码好维护, add指令相关的代码会抽取到单独的文件去编写

// bin/index
  program
  .command("add")
  .description("add a new template")
  .action(() => {
    require('../libs/command/add')
  });

libs/command/add

#!/usr/bin/env node

// 交互式命令行
const inquirer = require("inquirer");
// 修改控制台字符串样式
const chalk = require("chalk");
const fs = require("fs");
// 读取根目录下的template.json
const tplObj = require(`${__dirname}/../../template`);

// 自定义交互式命令行的问题和简单校验

let question = [
  {
    name: "name",
    type: "input",
    message: "请输入模板名称",
    validate(val) {
      if (val === "") {
        return "请输入模板名称";
      } else if (tplObj[val]) {
        return "模板已存在!";
      } else {
        return true;
      }
    },
  },
  {
    name: "type",
    type: "rawlist",
    message: "请选择模板仓库类型",
    choices: [
      {
        name: "github:",
      },
      {
        name: "gitlab:",
      },
      {
        name: "Bitbucket:",
      },
    ],
  },
  {
    name: "url",
    type: "input",
    message: "请输入模板地址",
    validate(val) {
      if (val === "") return "请输入模板地址";
      return true;
    },
  },
];

inquirer.prompt(question).then((options) => {
  // options是用户输入的参数  是一个对象
  let { name, url, type } = options;
  // 过滤 unicode 字符
  tplObj[name] = type + url.replace(/[\u0000-\u0019]/g, "");
  // 把模板信息写入template.json文件中
  fs.writeFile(
    `${__dirname}/../../template.json`,
    JSON.stringify(tplObj),
    "utf-8",
    (error) => {
      if (error) console.log(error);
      console.log("\n");
      console.log(chalk.green("Added successfully!\n"));
      console.log(chalk.grey("The latest template list is: \n"));
      console.log(tplObj);
      console.log("\n");
    }
  );
});



上述add操作其实就是维护了一个模板列表, 进行新增和删除操作,提供多重模板, 还有另一个做法是采用git提供的接口, 把模板放在一个仓库中, 调用接口返回模板信息, 但我个人觉得自己维护一个template.json比较灵活,可以突破仓库的限制

create创建操作,可以简单的分为下面几步(更严谨的话步骤更多)

  • 获取当前工作目录
  • 判断当前目录下是否存在当前操作的目录名字
  • 让用户选择要使用的模板
  • 拉取当前组织下的模版
  • 下载当前的模版所需依赖
  • 给出相关成功提示

下面是部分代码示例, 后面会给出完整的

code.png

上面两步骤,一个增加模板,一个使用, 核心流程是完成的, 删除和list 其实就是对模板数据的操作, 这里不展示了. 其实到这里,一个简单版的cli就完成了

注意: 下载所需依赖的时候注意版本, 部分依赖的新版本号变成esm格式了,大家注意一下

仓库地址: 仓库地址