聊聊前端脚手架的抽象与封装

1,090 阅读4分钟

预备知识

开发一个脚手架,我们通常需要引入以下工具库:

  • 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 执行文件得到如下结果:

image.png

可以看到控制台输出了帮助信息,这符合我们的预期。但通常来说,一个脚手架应该具备接收命令,然后执行不同动作的能力,如下所示:

#!/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();

上述代码注册了 runsleep 两个命令,执行的动作分别是在控制台输出 i am runningi am sleeping 字符串。

image.png

image.png

优化的方向

至此,抛开注册命令时的参数、别名、可选项等配置不说,看起来只需要在 bin/cmd.ts 文件中不断添加 commandaction 就能实现我们所有的脚手架命令需求了。

简单.gif

但是,写代码容易,但写好代码很难,上面简单的堆叠代码会存在以下问题:

  • 文件越来越庞大,可读性差
  • 对多人同时开发不友好,开发合并代码会经常引起冲突

看到这里,很多读者应该会觉得上面两个问题通过 代码拆分 很容易就能解决了,事实确实如此,那么究竟该怎么拆分呢?我们要怎么写出抽象程度高、可扩展性好的脚手架代码呢?

装逼 (2).gif

命令加载器

首先需要明确的是,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 上找到~

参考链接

装逼 (1).gif