前语

最近在写一个覆盖公司产品全站页面功用测验的项目,随着代码量逐步上升,
单纯的脚本履行的姿势,使保护成本上去了,代码散落各地,调用要打一大串。
所以直接搞成CLI改善运用体验和下降保护成本。

这篇文章只聚集CLI入口的姿势及经验共享。

成品图

yargs 简易指南:撸一个CLI


yargs 简易指南:撸一个CLI


yargs 简易指南:撸一个CLI

yargs 简易指南:撸一个CLI

资料及思路

搞CLI的前提便是正确的解析终端指令行传递的指令及参数,
node生态里边,有挺多解析库的, 比如commander,minimist 这些,
可是有一个是我之前调研过感觉不错的,刚好这次能够用上,便是yargs;

前置知识储藏

  • 知道linux风格的指令行规范,比如可选参数,子指令等
  • node解析参数的原理
    • process.argv

选用的解析库

yargs : API风格是链式调用,具有完善的command和复杂参数的组合,以及对应的hanlder【钩子呼应】,有完整的typescript提示,在写的过程中,直接跳转看类型界说能够减少看文档的次数。
这玩意让CLI代码的可读性直接拉升,哪怕你有同名参数,可是结合不同的commandhandler就能够很好的独立开来!

需求

  • 伪CLI风格调用
    • 不是真的发布CLI,结合package.json的scripts调用,适用于整个工程运用
  • 供给多功用指令【command】,独立不同功用
    • 指令及参数支撑别号,类型界说,是否必填,描绘等
    • 参数过错捕获
    • 参数的自界说校验及必填
  • 指令履行过程中止

代码完成

接下来请看代码注释,愈加清晰的介绍能够看下官网的API介绍,这儿不过多介绍!

const yargs = require('yargs/yargs');
const process = require('process');
const { hideBin } = require('yargs/helpers');
const path = require('path');
const { measureRun, measureRunAllModule } = require('./run'); // 功用完成
const genData2File = require('./gen'); // 功用完成
const clean = require('./clean'); // 功用完成
// 检测终端输入的中止快捷键信号【cmd+c】,强行退出进程
process.on('SIGINT', () => {
  process.kill(process.pid);
});
const argv = yargs(hideBin(process.argv))
  .strict()// 严厉模式,参数过错直接抛出反常
  .command({
    // 供给子指令
    command: 'measure', // 子指令全称
    aliases: ['m'], // 子指令别号
    desc: '丈量文件', // 子指令描绘
    builder: function (yargs) {
      // 此处返回指令参数组合
      return yargs
        .check((argv) => {
          // .check支撑手动校验承受的参数
          if (argv.all) return true;
          if (typeof argv.sourceFile !== 'string' || !argv.sourceFile) {
            throw new Error('CLI出错啦,源文件途径必须是字符串且不能为空!\n');
          }
          return true;
        })
        .options({
          // 子指令参数
          sourceFile: {
            alias: 'path', // 子指令参数别号
            describe: '丈量途径', // 子指令参数描绘
            string: true, // 承受类型是字符串
          },
          recursive: {
            alias: 'r',
            describe: '递归查找文件,一般用于测验整个模块运用',
            boolean: true, // 承受类型是布尔值,默许会隐形转换
          },
          docker: {
            alias: 'd',
            describe: '该参数启用的话,丈量是根据docker运转【无头浏览器运转】,数据包含视频录制',
            boolean: false,
          },
          open: {
            alias: 'o',
            describe: '该参数启用的话,会尝试调用本地浏览器翻开丈量后的数据网页',
            boolean: false,
          },
          'allow-ignore': {
            alias: 'keep',
            describe: '该参数启用的话,疏忽文件机制会收效',
            boolean: true,
            default: true,
          },
          'all-module': {
            alias: 'all',
            describe: '该参数启用的话,会顺次履行一切模块,最终输出一切模块陈述',
            boolean: false,
          },
          'result-dir-name': {
            describe: '该参数能够把陈述数据源的归纳到[resultDirName参数]目录,一般结合all参数运用',
            string: '',
          },
          number: {
            alias: 'n',
            describe: '装备测验走几轮,精度会高一些,可是每轮测验的时长会增加',
            number: true,
            default: 1,
          },
        })
        .usage('$0 measure <path>') // 辅助攻略,终端输出的能够看到
        .usage('$0 measure [--path=<path>] [--docker=<true|false>]')
        .example([
          // 辅助攻略,终端输出的能够看到
          ['$0 measure  "src/pages/desk/measures.js"', '测验该丈量文件'],
          ['$0 m src/pages/desk/measures.js -r', '测验该丈量文件及基层文件【递归往下找】'],
          ['$0 m src/pages/project/components/sprint/plan/measure.js --allow-ignore=false', '强行丈量处于疏忽清单的文件'],
        ]);
    },
    handler: function (argv) {
      // 呼应句柄,这儿处理参数经过校验后接收到的目标,然后你自己丢到你自己完成的功用函数引证即可!
      // 支撑async await , promise这类异步调用
      if (argv.all) {
        measureRunAllModule();
      } else {
        const entryPath = path.resolve(process.cwd(), argv?.sourceFile);
        const arg = {
          sourceFile: entryPath,
          r: argv?.r ?? false,
          n: argv?.n,
          d: argv?.d,
          open: argv?.o,
          docker: argv?.docker,
          allowIgnore: argv?.keep,
          resultDirName: argv?.resultDirName,
        };
        measureRun(arg);
      }
    },
  })
  .command({
    command: 'generate',
    aliases: ['g'],
    desc: '生成数据',
    builder: function (yargs) {
      return yargs
        .options({
          format: {
            describe: '生成报表的数据格局',
            array: true,
            choices: ['json', 'excel'], // 数组支撑多个值
            default: 'excel', // 也能设置默许值
          },
          date: {
            describe: '指定日期的报表【检索数据源】返回,日期格局【2022-08-21 , 2022/08-21】都能够',
            string: true,
            default: null,
          },
          dateRangeType: {
            array: true,
            choices: ['d', 'M', 'w', 'y'],
            describe: '指定日期的报表检索区间,默许是传递日期的当天内\n M 是月份,w 是周,y是年,d是当天',
            default: 'd',
          },
          all: {
            alias: 'a',
            describe: '检索一切数据源并生成报表[优先级比指定日期低,全量资源开销大]',
            boolean: true,
            default: false,
          },
          list: {
            alias: 'l',
            describe: '检索一切数据源并在终端展示',
            boolean: true,
            default: false,
          },
        })
        .usage('$0 g [--date] [--format=<json|excel>]')
        .example([
          ['$0 generate', '履行数据源生成报表,默许便是格局便是excel,根据最近一周内最新的一份有用数据作为数据源'],
          ['$0 g --list', '检索一切数据源并在终端展示'],
          ['$0 g --format json  ', '履行数据源生成报表,格局为json'],
          ['$0 g --date 2022-08-22 ', '检索特定日期当天内的数据源并生成报表'],
          ['$0 g --date 2022-08-22 --dateRangeType m', '检索特定日期当月内的数据源并生成报表'],
          ['$0 g --all ', '检索一切数据源并生成报表'],
        ]);
    },
    handler: (argv) => {
      genData2File({
        date: argv?.date,
        all: argv?.all,
        format: argv?.format,
        dateRangeType: argv?.dateRangeType,
        list: argv?.l,
      });
    },
  })
  .command({
    command: 'clean',
    aliases: ['c'],
    desc: '删去数据源文件',
    builder: function (yargs) {
      return yargs
        .options({
          src: {
            alias: 'path',
            describe: '删去的资源途径',
            string: true,
          },
        })
        .demandOption(['src', 'path'], '请传入要删去的途径!相对于项目根途径,比如: sitespeed-result/docker-test/2022-08-22') // 强制某个参数为必填,不如check灵敏
        .usage('$0 clean --path ')
        .example([
          ['$0 clean --path sitespeed-result/docker-test/2022-08-18/12-25-46_1580d24f-4d01-4155-ad22-bd3b8305f73d', '清除指定的数据源'],
        ]);
    },
    handler: async (argv) => {
      clean(argv?.path);
    },
  })
  .fail(function (msg, err, yargs) {
    // 此处捕获yargs履行反常或许抛出的反常【throw new Error】
    console.error(yargs.help());
    console.error('\n\n\n=====指令履行过错,信息如下=====\n\n', msg);
    process.exit(1);
  })
  .showHelpOnFail(false, '指令指定 --help 查看有用的选项') // 当指令履行过错的时候,自动调用一下协助指令并输出到终端
  .version(false) // cli版别设置,此处关闭
  .wrap(null) // 设置为null,便是自适应,固宽的话,
  .locale('zh_CN') // yargs供给多言语支撑,装备对应区域,核心过错这些有对应的言语文本
  .help('help', '查看指令行协助').argv;
module.exports = argv;

官方还供给了从装备文件读取的操作,包括指令的智能推断等,这些没搞。
有爱好的能够官网自行了解,并不难。

  • yargs.js.org/docs/#api-r…
  • yargs.js.org/docs/#api-r…

package.json

  "scripts": {
    "prepare": "husky install",
    "commit": "git-cz",
    "help": "npm run cli -- --help",
    "cli": "node ./node-scripts/cli.js",
    "m:all": "npm run cli --  m --all",
    "m:dev": "npm run cli -- m --open ",
    "m:docker": "npm run cli -- m --open -d",
    "g": "npm run cli -- g ",
    "clean": "npm run cli -- c"
  },

这样,结合scripts就能到达类似CLI的效果

总结

yargs我用下来觉得最大的亮点便是组织性很强,这样指令的保护成本会很低。
比如minimist 这种就只有纯粹的指令解析,一切判定逻辑需求自己去兜住,校验,同名参数隔离等!
有不对之处请留言,会及时批改,谢谢阅读。