Skip to content

commander.js 原理解析 #152

@hoperyy

Description

@hoperyy

概览

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

commander.js包含以下类:

  • Option
  • Help
  • Command
  • CommandError
  • InvalidOptionArgumentError

如下图:

image

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类使用时的主要流程:

image

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

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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions