Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

commander.js 原理解析 #152

Open
hoperyy opened this issue Feb 22, 2021 · 0 comments
Open

commander.js 原理解析 #152

hoperyy opened this issue Feb 22, 2021 · 0 comments

Comments

@hoperyy
Copy link
Owner

hoperyy commented Feb 22, 2021

概览

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)

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant