commander.js 原理解析

2,241 阅读5分钟

原文发表在 Github: hoperyy blog

概览

commander.js 7.0.0 版本的核心代码就一个文件 index.js,2200 多行代码,代码的注释比较丰富,代码可读性也是不错的,感兴趣的同学可以通读一下。

commander.js 包含以下类:

  • Option
  • Help
  • Command
  • CommandError
  • InvalidOptionArgumentError

如下图:

Option 类

Options 类的实例存储选项的各类信息:

  • 选项是否必填(requred)
  • 描述信息(description)
  • 可变参数(variadic)
  • 是否反向 boolean,也就是 --no-xx 类型的选项
  • 长名称/短名称,也就是 -c, --cheese
  • 参数的值
  • 其他

image

命令行实例执行 program.option("-c, --cheese", "add cheese"),会创建一个新的 Option 实例,存储该选项的各类信息。

Option 类内部通过各种正则和字符串的计算各类信息。

比如,Option 类接受参数的长短名称有三种写法:

  • "-c, --cheese"
  • "-c|--cheese"
  • "-c --cheese"

其解析参数的时候以正则 /[ |,]+/ 对字符串做分隔计算: flags.split(/[ |,]+/)

Help 类

Help 类主要负责帮助信息的展示、配置等工作。

比如执行命令行 node index.js -h 会打印出:

Usage: index [options]

Options:
    -v, --version           output the version number
    -d, --debug             output extra debugging
    -c, --cheese <type>     cheese type (default: "blue")
    -b, --banana [type]     banana type
    -i, --integer <number>  integer argument
    -h, --help              display help for command

Help 类中相对值得说的是 formatHelp 方法了。

formatHelp 接收当前命令行和 help 实例对象,返回格式化后的帮助信息。

在生成帮助信息的过程中,有几个小的编程技巧可以借鉴:

  • 日志信息字符串,通过数组形式组织,最终拼接为字符串

    该方法对比直接拼接字符串,代码可维护性更强。

  • 利用 String.prototype.padEnd 方法实现字符串补全

    日常工作中用到该方法的场景可能不多,容易遗漏。

    比如:'hello'.padEnd(7, '~') 的结果是 hello~~

    formatHelp 里用空格补全,用于字符串显示的格式化。

Command类

Command 类是 commander.js 的核心类,提供了命令行的各类方法。

下图是 Command 类使用时的主要流程:

我们简要介绍下其中的一些点:

  • version(str, flags, description)

    该方法注册了命令的版本信息,利用 createOption() 实现的一个快捷方法。

  • command(nameAndArgs, actionOptsOrExecDesc, execOpts)

    该方法注册子命令,有两种模式:

    • 绑定函数实现命令

      program
          .command('start')
          .action(function() {
              console.log('actor');
          });
      

      执行 node index start 的时候,会执行 action 注册的回调,打印 actor

    • 启动独立文件执行命令

      program.command('start', 'start runner');
      

      执行 node index start 的时候,会启动 index-start.js 文件。

    该方法内部通过是否含有描述信息判断是哪种模式。

  • 重复注册命令时,会使用第一个注册的命令

    比如:

    program
            .command('start')
            .action(function() {
                console.log('start 1');
            });
    program
            .command('start')
            .action(function() {
                console.log('start 2');
            });
    

    在执行 node index start 的时候,只会打印 start 1,因为内部找到匹配的命令的代码是:

    this.commands.find(cmd => cmd._name === name || cmd._aliases.includes(name));
    

    Array.prototype.find 方法会返回数组第一个匹配的元素。

  • EventEmitter 在 Command 类中的使用

    Node 内置模块 EventEmitter 提供了事件机制,最常见的 api 是 on/emit

    Command 类中几处利用事件机制的地方举例:

    • 注册选项参数时,会注册 option:${optionName} 事件( on(option:${optionName}) ),在命令行执行时触发回调( emit(option:${optionName}) )。
    • 执行命令时,如果没有匹配的命令,会通过 this.listenerCount('command:*') 获取 command:xx 事件( * 为通配符)的监听者数量,决定是否触发该事件

Error 类

下图是 Commander.js 内部定义的几个 Error 类的继承关系。

image

在内部实现上,分别定义了每个类自身的特殊字段。但值得注意的是,Error.captureStackTrace(this, this.constructor) 被频繁使用。

  • Error.captureStackTrace 使用

    Error.captureStackTrace(targetObject[, constructorOpt])

    其作用是在 targetObject 中添加一个 stack 属性。当访问 targetObject.stack 时,将以字符串的形式返回 Error.captureStackTrace 方法被调用时的代码位置信息。举例:

    index.js

    > 1 const myObject = {};
    > 2 Error.captureStackTrace(myObject);
    > 3 console.log(myObject.stack);
    

    执行node index.js后,终端输出:

    Error
        at Object.<anonymous> (xxx/index.js:2:7)
        at Module._compile (internal/modules/cjs/loader.js:689:30)
        at ...
        at ...
    

    当传入 constructorOpt 时,代码如:

    > 1 function MyError() {
    > 2    Error.captureStackTrace(this, MyError);
    > 3 }
    > 4
    > 5 console.log(new MyError().stack)
    

    终端输出:

    Error
        at Object.<anonymous> (xxx/index.js:5:13)
        at Module._compile (internal/modules/cjs/loader.js:689:30)
        at ...
        at ...
    

    可以看出,MyError 函数内部的堆栈细节被隐藏了。

  • Error.captureStackTrace 优点

    相对于 new Error().stackError.captureStackTrace 有以下优点:

    • 更简洁

      无需 new 一个新的 Error 对象,节省内存空间,同时代码上也会更加优雅。

      一般而言,捕获错误信息通常的做法是:

      try {
          new Error();
      } catch(err) {
          // err.stack 包含了堆栈信息,可以对其处理
      }
      

      而使用 Error.captureStackTrace 可以直接获取堆栈信息,实现方式更简洁。

    • 更安全

      如果需要忽略部分堆栈信息,使用 Error.captureStackTrace 会更加方便,无需手工操作。

    • 更少资源

      使用 Error.captureStackTrace 时,只有访问 targetObject.stack 时,才会进行堆栈信息的格式化工作。

      如果 targetObj.stack 未被访问,则堆栈信息的格式化工作会被省略,从而节省计算资源。

  • Error.captureStackTrace 使用场景

    Error.captureStackTrace 并不是 Node.js 创造的,而是 V8 引擎的 Stack Trace API。语法上,Node.js 中的 Error.captureStackTrace() 与 V8 引擎中所暴露的接口完全一致。

    事实上,Node.js 的 Error 类中,所有与 stack trace 有关的内容均依赖于 V8 的 Stack Trace API。

    因此,Error.captureStackTrace(targetObject[, constructorOpt]) 使用的场景有:

    • 基于 V8 引擎的运行环境,如 Node.js、Chrome 浏览器

    • Error.captureStackTrace(this, MyError)

      作用也是隐藏构造函数内部的堆栈信息,但需要明确指定构造函数名,通用性不强。

    • Error.captureStackTrace(this, arguments.callee)

      arguments.callee 表示当前函数,也有通用性。但 ES3 及之后的严格模式禁用了 arguments.callee,因此不建议使用。

    • Error.captureStackTrace(this, this.constructor)

      该做法可以隐藏构造函数内部的堆栈信息,无需指定构造函数名,通用型强。