commander.js 原理解析

原文发表在 Github: hoperyy blog

概览

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

commander.js包含以下类:

Option
Help
Command
CommandError
InvalidOptionArgumentError

如下图:

Option类

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

--no-xx
-c, --cheese

命令行实例执行 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类的继承关系。

在内部实现上,分别定义了每个类自身的特殊字段。但值得注意的是, 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)

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

稀土掘金
我还没有学会写个人说明!
上一篇

ECMAScript 双月报告:TC39 2021年1月会议提案进度汇总

下一篇

【玩转写作平台】如何让专业编辑青睐你的文章?被推荐置顶?

你也可能喜欢

评论已经被关闭。

插入图片