每当我们想要创建一个基于 NodeJS 的命令行工具时,就会衍生出一堆问题需要解决,比如如何准备开发环境,如何打包转译代码,如何使代码在转译后保持可调用的状态同时尽可能的压缩体积,以及怎样设计项目分配 CommandOption 等等,这会浪费巨大的时间,而且并非一定有成果。这时你可以注意到社区几乎所有的命令行工具都是自成一派,并没有严谨的框架或约定约束,也无所谓的最佳实践,这使想要特别是第一次想要开发命令行工具的开发者望而却步,或是几番努力最后却不尽如人意。

举个例子来说,腾讯的 omi 是一个有众多使用者的框架,但其命令行工具 omi / omi-cli 的体验之差让人难以置信。仅一些简单的下载和创建模板的任务,造出长篇大论的文件不说,下载时依赖数千,包的体积巨大,模块是随心所欲整体项目缺乏设计。对于一个用户来说,在使用这样的命令行工具时要为网络与磁盘 I/O 付出巨大代价。

现代化的命令框架 func

Func 的出现就是为了解决这些问题。Func 本身的实现参阅了社区内诸多基于 NodeJS 的命令行工具的优秀实现,与流行的框架设计思路相结合,以优雅的设计、小体积、高性能等为目标,同时关注开发者体验,大幅度的提升了命令行工具项目的可扩展性与可读性,几乎是如今 NodeJS 社区中开发命令行工具的最优解。我们可以尝试使用 Func 构建一个命令行工具。

构建项目

在以前流行的一些命令行参数解析的库中,我们在构建项目前需要准备大量的脚本与配置,甚至还要解决文件权限、bin、代码转译等等问题,但使用 func,我们可以仅通过一行命令初始化项目:

npm init func

运行命令快速创建 func 项目

项目初始化后进入文件夹,随机使用 npm installyarn 安装依赖,现在就可以正式开发了。可以注意到,func 的项目模板中为我们准备了 startbuild 2 个脚本,它们都是由 func-service 驱动的,帮助你一键切换开发与生产模式,我们所要做的就是专注于命令行逻辑本身,实现逻辑就够了。

实现逻辑

我们可以随意的创建一个类,当它被加上 Command 注解时这就是一个命令,而被加上 Option 注解时就会转变为一个选项:

import { Command } from 'func'

@Command({ name: 'test' })
export class Test {}

在命令行中运行 <YOUR NAME> test 时,Test 类将会被调用。选项也是同理。你可以在 constructor 中开始自己的代码,这和你在任何地方写 NodeJS 没有什么不同,当你觉得差不多的时候,运行 npm build 就可以将它打包,一切就是这么简单。

有时候,当我们需要使用到命令行携带的参数时,比如处理 <NAME> something params -option 这样复杂的输入,你可以直接在类中注入命令行参数,对,就是像 IoC 那样:

@Command({ name: 'test' })
export class Test {
  constructor(private args: CommandArgsProvider) {}
}

CommandArgsProvider 实际上是一个 class 类型,当你标记一个参数为此类型时,func 会在运行时为你注入所有的命令参数,同样的也支持 OptionArgsProviderRegisterProvider 等等,你可以在 官方的文档 阅读它们的具体类型。

打包与发布

运行 npm build 可以得到一个打包后的文件,这是由 ncc 编译后的文件,通常它只有一个 (如果携带 extra 可能会有多个,但它们会自动链接),同时为你链接好了 bin,你要做的唯一一件事就是将包发布出去。为什么 func 使用这种方式发布呢?

我们知道当你在安装一个包或是使用 npx 执行包时 (这在使用命令行工具的人群中很常见),NPM 所花费的时间大约在 3 个部分,即对比包的依赖,下载包,执行。

Func 生成的项目已经足够的小,能够大量节省下载时间,同时也有足够好的性能,现在要解决的就是在大量依赖时的对比分析问题,将文件打包成单文件不依赖外部环境时会极大的减少所需时间,如果你再将所有的依赖移入 devDependencies 中,几乎能够在一瞬间完成 分析 - 下载 - 运行 这三个步骤。这样的体验是难以想象的。

是的,这里推荐你把所有的依赖当做开发依赖处理,这似乎违背了 NPM Package 的开发哲学,但在构建超高性能的命令行应用时这样做却大有裨益。用户在使用命令行工具时几乎不可能会以此为基础构建新的项目 (除非有额外需求),通常我们也不需要让用户共享生产依赖的缓存,也建议跳过所有的依赖对比。

在运行 func build 完成的包时,我们注意到几乎无需任何依赖,这是因为在单个文件中已经包含所有的资源,也就意味着用户在运行 .js 文件时,堆栈中真的就只有 .js 文件内的内容,不会引用其他,不会加载任何无关紧要的东西。此时我们也就无需用户关心 dependencies,甚至可以移除它们,这样一来,下载或即时运行时就直接跳过了 对比依赖版本 这一步,这其中省略了无数的请求也就会会极大的增加速度,npm init func 能够在 1 秒左右立刻开始安装也是这样的道理。

优化与经验

现在你已经知道了怎样快速的构建一个合格且优雅的命令行工具,那怎样做的更好呢?通常来说你需要遵循这几点: