编写自己的 TypeScript CLI

2022-02-25 10:02:42 浏览数 (1)

TL;DR

  • 您可以轻松编写 CLI,它比你想象的要简单;
  • 我们一起编写 CLI 以生成 Lighthouse 性能报告;
  • 你将看到如何配置 TypeScript、EsLint 和 Prettier;
  • 你会看到如何使用一些很优秀的库,比如 chalkcommander
  • 你将看到如何产生多个进程;
  • 你会看到如何在 GitHub Actions 中使用你的 CLI。

实际用例

Lighthouse 是用于深入了解网页性能的最流行的开发工具之一,它提供了一个CLI 和 Node 模块,因此我们可以以编程方式运行它。但是,如果您在同一个网页上多次运行 LIghthouse,您会发现它的分数会有所不同,那是因为存在已知的可变性。影响 Lighthouse 可变性的因素有很多,处理差异的推荐策略之一是多次运行 Lighthouse。

在本文中,我们将使用 CLI 来实施此策略,实施将涵盖:

  • 运行多个 Lighthouse 分析;
  • 汇总数据并计算中位数。

项目的文件结构

这是配置工具后的文件结构。

代码语言:javascript复制
my-script
├── .eslintrc.js
├── .prettierrc.json
├── package.json
├── tsconfig.json
├── bin
└── src
    ├── utils.ts
    └── index.ts

配置工具

我们将使用 Yarn 作为这个项目的包管理器,如果您愿意,也可以使用 NPM。

我们将创建一个名为 my-script 的目录:

代码语言:javascript复制
$ mkdir my-script && cd my-script

在项目根目录中,我们使用 Yarn 创建一个 package.json

代码语言:javascript复制
$ yarn init

配置 TypeScript

安装 TypeScript 和 NodeJS 的类型,运行:

代码语言:javascript复制
$ yarn add --dev typescript @types/node

在我们配置 TypeScript 时,可以使用 tsc 初始化一个 tsconfig.json

代码语言:javascript复制
$ npx tsc --init

为了编译 TypeScript 代码并将结果输出到 /bin 目录下,我们需要在 tsconfig.jsoncompilerOptions 中指定 outDir

代码语言:javascript复制
// tsconfig.json
{
  "compilerOptions": {
     "outDir": "./bin"
    /* rest of the default options */
  }
}

然后,让我们测试一下。

在项目根目录下,运行以下命令,这将在 /src 目录下中创建 index.ts 文件:

代码语言:javascript复制
$ mkdir src && touch src/index.ts

index.ts 中,我们编写一个简单的 console.log 并运行 TypeScript 编译器,以查看编译后的文件是否在 /bin 目录中。

代码语言:javascript复制
// src/index.ts
console.log('Hello from my-script')

添加一个用 tsc 编译 TypeScript 代码的脚本。

代码语言:javascript复制
// package.json

  "scripts": {
    "tsc": "tsc"
  },

然后运行:

代码语言:javascript复制
$ yarn tsc

你将在 /bin 目下看到一个 index.js 文件。

然后我们在项目根目录下执行 /bin 目录:

代码语言:javascript复制
$ node bin
# Hello from my-script

配置 ESLint

首先我们需要在项目中安装 ESLint。

代码语言:javascript复制
$ yarn add --dev eslint

EsLint 是一个非常强大的 linter,但它不支持 TypeScript,所以我们需要安装一个 TypeScript 解析器:

代码语言:javascript复制
$ yarn add --dev @typescript-eslint/parser @typescript-eslint/eslint-plugin

我们还安装了 @typescript-eslint/eslint-plugin,这是因为我们需要它来扩展针对 TypeScript 特有功能的 ESLint 规则。

配置 ESLint,我们需要在项目根目录下创建一个 .eslintrc.js 文件:

代码语言:javascript复制
$ touch .eslintrc.js

.eslintrc.js 中,我们可以进行如下配置:

代码语言:javascript复制
// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  extends: ['plugin:@typescript-eslint/recommended']
}

让我们进一步了解下这个配置:我们首先使用 @typescript-eslint/parser 来让 ESLint 能够理解 TypeScript 语法,然后我们应用 @typescript-eslint/eslint-plugin 插件来扩展这些规则,最后,我们启用了@typescript-eslint/eslint-plugin 中所有推荐的规则。

如果您有兴趣了解更多关于配置的信息,您可以查看官方文档 以了解更多细节。

我们现在可以在 package.json 中添加一个 lint 脚本:

代码语言:javascript复制
// package.json

{
  "scripts": {
     "lint": "eslint '**/*.{js,ts}' --fix",
  }
}

然后去运行这个脚本:

代码语言:javascript复制
$ yarn lint

配置 Prettier

Prettier 是一个非常强大的格式化程序,它附带一套规则来格式化我们的代码。有时这些规则可能会与 ESLInt 规则冲突,让我们一起看下将如何配置它们。

首先安装 Prettier ,并在项目根目录下创建一个 .prettierrc.json 文件,来保存配置:

代码语言:javascript复制
$ yarn add --dev --exact prettier && touch .prettierrc.json

您可以编辑 .prettierrc.json 并且添加您的自定义规则,你可以在官方文档中找到这些选项。

代码语言:javascript复制
// .prettierrc.json

{
  "trailingComma": "all",
  "singleQuote": true
}

Prettier 提供了与 ESLint 的便捷集成,我们将遵循官方文档中的推荐配置 。

代码语言:javascript复制
$ yarn add --dev eslint-config-prettier eslint-plugin-prettier

.eslintrc.js 中,在 extensions 数组的最后一个位置添加这个插件。

代码语言:javascript复制
// eslintrc.js

module.exports = {
  extends: [
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended' 
  ]
}

最后添加的这个 Prettier 扩展,非常重要,它会禁用所有与格式相关的 ESLint 规则,因此冲突将回退到 Prettier。

现在我们可以在 package.json 中添加一个 prettier 脚本:

代码语言:javascript复制
// package.json

{
  "scripts": {
     "prettier": "prettier --write ."
  }
}

然后去运行这个脚本:

代码语言:javascript复制
$ yarn prettier

配置 package.json

我们的配置已经基本完成,唯一缺少的是一种像执行命令那样执行项目的方法。与使用 node 执行 /bin 命令不同,我们希望能够直接调用命令:

代码语言:javascript复制
# 我们想通过它的名字来直接调用这个命令,而不是 "node bin",像这样:
$ my-script

我们怎么做呢?首先,我们需要在 src/index.ts 的顶部添加一个 Shebang):

代码语言:javascript复制
  #!/usr/bin/env node
console.log('hello from my-script')

Shebang 是用来通知类 Unix 操作系统这是 NodeJS 可执行文件。因此,我们可以直接调用脚本,而无需调用 node

让我们再次编译:

代码语言:javascript复制
$ yarn tsc

在一切开始之前,我们还需要做一件事,我们需要将可执行文件的权限分配给bin/index.js

代码语言:javascript复制
$ chmod u x ./bin/index.js

让我们试一试:

代码语言:javascript复制
# 直接执行
$ ./bin/index.js

# Hello from my-script

很好,我们快完成了,最后一件事是在命令和可执行文件之间创建符号链接。首先,我们需要在 package.json 中指定 bin 属性,并将命令指向 bin/index.js

代码语言:javascript复制
// package.json
{
   "bin": {
     "my-script": "./bin/index.js"
   }
}

接着,我们在项目根目录中使用 Yarn 创建一个符号链接:

代码语言:javascript复制
$ yarn link

# 你可以随时取消链接: "yarn unlink my-script"

让我们看看它是否有效:

代码语言:javascript复制
$ my-script

# Hello from my-script

成功之后,为了使开发更方便,我们将在 package.json 添加几个脚本:

代码语言:javascript复制
// package.json
{
  "scripts": {
     "build": "yarn tsc && yarn chmod",
     "chmod": "chmod u x ./bin/index.js",
  }
}

现在,我们可以运行 yarn build 来编译,并自动将可执行文件的权限分配给入口文件。

编写 CLI 来运行 Lighthouse

是时候实现我们的核心逻辑了,我们将探索几个方便的 NPM 包来帮助我们编写CLI,并深入了解 Lighthouse 的魔力。

使用 chalk 着色 console.log

代码语言:javascript复制
$ yarn add chalk@4.1.2

确保你安装的是 chalk 4chalk 5是纯 ESM,在 TypeScript 4.6 发布之前,我们无法将其与 TypeScript 一起使用。

chalkconsole.log 提供颜色,例如:

代码语言:javascript复制
// src/index.ts

import chalk from 'chalk'
console.log(chalk.green('Hello from my-script'))

现在在你的项目根目录下运行 yarn build && my-script 并查看输出日志,会发现打印结果变成了绿色。

让我们用一种更有意义的方式来使用 chalk,Lighthouse 的性能分数是采用颜色标记的。我们可以编写一个实用函数,根据性能评分用颜色显示数值。

代码语言:javascript复制
// src/utils.ts

import chalk from 'chalk'

/**
 * Coloring display value based on Lighthouse score.
 *
 * - 0 to 0.49 (red): Poor
 * - 0.5 to 0.89 (orange): Needs Improvement
 * - 0.9 to 1 (green): Good
 */
export function draw(score: number, value: number) {
  if (score >= 0.9 && score <= 1) {
    return chalk.green(`${value} (Good)`)
  }
  if (score >= 0.5 && score < 0.9) {
    return chalk.yellow(`${value} (Needs Improvement)`)
  }
  return chalk.red(`${value} (Poor)`)
}

src/index.ts 中使用它,并尝试使用 draw() 记录一些内容以查看结果。

代码语言:javascript复制
// src/index.ts

import { draw } from './utils'
console.log(`Perf score is ${draw(0.64, 64)}`)

使用 commander 设计命令

要使我们的 CLI 具有交互性,我们需要能够读取用户输入并解析它们。commander 是定义接口的一种描述性方式,我们可以以一种非常干净和纪实的方式实现界面。

我们希望用户与 CLI 交互,就是简单地传递一个 URL 让 Lighthouse 运行,我们还希望传入一个选项来指定 Lighthouse 应该在 URL 上运行多少次,如下:

代码语言:javascript复制
# 没有选项
$ my-script https://dawchihliou.github.io/

# 使用选项
$ my-script https://dawchihliou.github.io/ --iteration=3

使用 commander 可以快速的实现我们的设计。

代码语言:javascript复制
$ yarn add commander

让我们清除 src/index.ts 然后重新开始:

代码语言:javascript复制
#!/usr/bin/env node

import { Command } from 'commander'

async function run() {
  const program = new Command()

  program
    .argument('<url>', 'Lighthouse will run the analysis on the URL.')
    .option(
      '-i, --iteration <type>',
      'How many times Lighthouse should run the analysis per URL',
      '5'
    )
    .parse()
      
  const [url] = program.args
  const options = program.opts()
      
  console.log(`url: ${url}, iteration: ${options.iteration}`)
}
      
run()

我们首先实例化了一个 Command,然后使用实例 program 去定义:

  • 一个必需的参数:我们给它起了一个名称 url和一个描述;
  • 一个选项:我们给它一个短标志和一个长标志,一个描述和一个默认值。

要使用参数和选项,我们首先解析命令并记录变量。

现在我们可以运行命令并观察输出日志。

代码语言:javascript复制
$ yarn build

# 没有选项
$ my-script https://dawchihliou.github.io/

# url: https://dawchihliou.github.io/, iteration: 5

# 使用选项
$ my-script https://dawchihliou.github.io/ --iteration=3
# 或者
$ my-script https://dawchihliou.github.io/ -i 3

# url: https://dawchihliou.github.io/, iteration: 3

很酷吧?!另一个很酷的特性是,commander 会自动生成一个 help 来打印帮助信息。

代码语言:javascript复制
$ my-script --help

在单独的操作系统进程中运行多个 Lighthouse 分析

我们在上一节中学习了如何解析用户输入,是时候深入了解 CLI 的核心了。

运行多个 Lighthouse 的建议是在单独的进程中运行它们,以消除干扰的风险。cross-spawn 是用于生成进程的跨平台解决方案,我们将使用它来同步生成新进程来运行 Lighthouse。

要安装 cross-spawn

代码语言:javascript复制
$ yarn add cross-spawn 
$ yarn add --dev @types/cross-spawn

# 安装 lighthouse
$ yarn add lighthouse

让我们编辑 src/index.ts

代码语言:javascript复制
#!/usr/bin/env node

import { Command } from 'commander'
import spawn from 'cross-spawn'

const lighthouse = require.resolve('lighthouse/lighthouse-cli')

async function run() {
  const program = new Command()

  program
    .argument('<url>', 'Lighthouse will run the analysis on the URL.')
    .option(
      '-i, --iteration <type>',
      'How many times Lighthouse should run the analysis per URL',
      '5'
    )
    .parse()
      
  const [url] = program.args
  const options = program.opts()
      
  console.log(
    `


	

0 人点赞