预备知识
开发一个脚手架,我们通常需要引入以下工具库:
- commander.js: TJ 大神出品,一个完整的 node.js 命令行解决方案
- chalk: 美化命令行输出
- Inquirer.js: 询问式应答库
如果我们的需求很简单,实现一个脚手架只需要 commander.js
就行了,本文也只使用到 commander.js
来做演示说明,有兴趣的读者可以自行到 github 上了解其他库的使用。
最简单的脚手架
在项目根目录创建 bin/cmd.ts
,代码如下所示:
#!/usr/bin/env node
import { Command } from "commander";
const bootstrap = () => {
const program = new Command();
program
.version(
require("../package.json").version,
"-v, --version",
"Output the current version."
)
.usage("<command> [options]")
.helpOption("-h, --help", "Output usage information.");
// 解析参数
program.parse(process.argv);
// 如果没有参数,输出help信息
if (!process.argv.slice(2).length) {
program.outputHelp();
}
};
bootstrap();
使用 node -r ts-node/register ./bin/cmd.ts
执行文件得到如下结果:
可以看到控制台输出了帮助信息,这符合我们的预期。但通常来说,一个脚手架应该具备接收命令,然后执行不同动作的能力,如下所示:
#!/usr/bin/env node
import { Command } from "commander";
const bootstrap = () => {
const program = new Command();
program
.version(
require("../package.json").version,
"-v, --version",
"Output the current version."
)
.usage("<command> [options]")
.helpOption("-h, --help", "Output usage information.");
+ program.command("run").action(() => {
+ console.log("i am running");
+ });
+ program.command("sleep").action(() => {
+ console.log("i am sleeping");
+ });
// 解析参数
program.parse(process.argv);
// 如果没有参数,输出help信息
if (!process.argv.slice(2).length) {
program.outputHelp();
}
};
bootstrap();
上述代码注册了 run
和 sleep
两个命令,执行的动作分别是在控制台输出 i am running
和 i am sleeping
字符串。
优化的方向
至此,抛开注册命令时的参数、别名、可选项等配置不说,看起来只需要在 bin/cmd.ts
文件中不断添加 command
和 action
就能实现我们所有的脚手架命令需求了。
但是,写代码容易,但写好代码很难,上面简单的堆叠代码会存在以下问题:
- 文件越来越庞大,可读性差
- 对多人同时开发不友好,开发合并代码会经常引起冲突
看到这里,很多读者应该会觉得上面两个问题通过 代码拆分 很容易就能解决了,事实确实如此,那么究竟该怎么拆分呢?我们要怎么写出抽象程度高、可扩展性好的脚手架代码呢?
命令加载器
首先需要明确的是,program.command('xxx').action(callback)
这部分代码需要拆分出去独立实现,这样在多人同时开发不同命令的时候就不会引起冲突。因此,我们可以开发一个命令加载器,如下所示:
export class CommandLoader {
public static load(program: Command): void {
// TODO:注册命令
}
}
有了命令加载器,bin/cmd.ts
便可以固定为以下代码,不受命令的增删查改影响:
#!/usr/bin/env node
import { Command } from "commander";
+ import { CommandLoader } from "./../commands/command.loader";
const bootstrap = () => {
const program = new Command();
program
.version(
require("../package.json").version,
"-v, --version",
"Output the current version."
)
.usage("<command> [options]")
.helpOption("-h, --help", "Output usage information.");
- program.command("run").action(() => {
- console.log("i am running");
- });
- program.command("sleep").action(() => {
- console.log("i am sleeping");
- });
+ CommandLoader.load(program);
// 解析参数
program.parse(process.argv);
// 如果没有参数,输出help信息
if (!process.argv.slice(2).length) {
program.outputHelp();
}
};
bootstrap();
command抽象
command 的模式其实比较固化:使用 program
初始化配置,并且调用 action
。因此我们可以定义一个 command 抽象类提高代码复用和进行代码约束,同时在抽象类的构造函数里面依赖注入 action 对象:
export abstract class AbstractCommand {
// 依赖注入 action
constructor(protected action: AbstractAction) {}
public abstract load(program: CommanderStatic): void;
}
实现一个命令:
export class RunCommand extends AbstractCommand {
public load(program: Command): void {
program.command("run").action(() => {
// 将参数和可选项包装成键值对形式传入 handle 方法
this.action.handle();
});
}
}
action抽象
action主要是接受参数以及可选项进行逻辑处理,参数和可选项都为键值对的形式,代码如下:
export interface Input {
name: string
value: boolean | string
}
export abstract class AbstractAction {
public abstract handle(inputs?: Input[], options?: Input[]): Promise<void>;
}
实现一个 action:
export class RunAction extends AbstractAction {
public async handle(inputs?: Input[], options?: Input[]): Promise<void> {
console.log("i am running");
}
}
对 command 和 action 进行抽象之后,命令注册就变得异常简单了:
export class CommandLoader {
public static load(program: Command): void {
// 新增命令注册
new RunCommand(new RunAction()).load(program);
new SleepCommand(new SleepAction()).load(program);
}
}
总结
本文使用面向对象编程的思想实现了一个脚手架框架,具有以下优点:
- 可执行文件(这里是
bin/cmd.ts
)和命令实现解耦 - 实现命令加载器,注册命令只需要一行代码
- 抽象 command 和 action,提高代码复用性及进行代码约束
- action 依赖注入到 command,提高可扩展性
由于篇幅有限,本文省略了很多 command 和 action 配置和处理的细节,如果有疑问可以在评论区一起探讨~
本文所有代码都可以在 github 上找到~