6

编写自己的 TypeScript CLI

 2 years ago
source link: https://segmentfault.com/a/1190000041379931
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

TL;DR

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

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

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

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

项目的文件结构

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

my-script
├── .eslintrc.js
├── .prettierrc.json
├── package.json
├── tsconfig.json
├── bin
└── src
    ├── utils.ts
    └── index.ts

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

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

$ mkdir my-script && cd my-script

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

$ yarn init

配置 TypeScript

安装 TypeScriptNodeJS 的类型,运行:

$ yarn add --dev typescript @types/node

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

$ npx tsc --init

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

// tsconfig.json
{
  "compilerOptions": {
+    "outDir": "./bin"
    /* rest of the default options */
  }
}

然后,让我们测试一下。

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

$ mkdir src && touch src/index.ts

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

// src/index.ts
console.log('Hello from my-script')

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

// package.json

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

然后运行:

$ yarn tsc

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

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

$ node bin
# Hello from my-script

配置 ESLint

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

$ yarn add --dev eslint

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

$ yarn add --dev @typescript-eslint/parser @typescript-eslint/eslint-plugin

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

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

$ touch .eslintrc.js

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

// .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 脚本:

// package.json

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

然后去运行这个脚本:

$ yarn lint

配置 Prettier

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

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

$ yarn add --dev --exact prettier && touch .prettierrc.json

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

// .prettierrc.json

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

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

$ yarn add --dev eslint-config-prettier eslint-plugin-prettier

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

// eslintrc.js

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

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

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

// package.json

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

然后去运行这个脚本:

$ yarn prettier

配置 package.json

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

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

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

+ #!/usr/bin/env node
console.log('hello from my-script')

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

让我们再次编译:

$ yarn tsc

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

$ chmod u+x ./bin/index.js

让我们试一试:

# 直接执行
$ ./bin/index.js

# Hello from my-script

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

// package.json
{
+  "bin": {
+    "my-script": "./bin/index.js"
+  }
}

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

$ yarn link

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

让我们看看它是否有效:

$ my-script

# Hello from my-script

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

// 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

$ yarn add [email protected]

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

chalkconsole.log 提供颜色,例如:

// src/index.ts

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

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

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

// 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() 记录一些内容以查看结果。

// src/index.ts

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

使用 commander 设计命令

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

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

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

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

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

$ yarn add commander

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

#!/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和一个描述;
  • 一个选项:我们给它一个短标志和一个长标志,一个描述和一个默认值。

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

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

$ 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 来打印帮助信息。

$ my-script --help

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

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

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

要安装 cross-spawn

$ yarn add cross-spawn 
$ yarn add --dev @types/cross-spawn

# 安装 lighthouse
$ yarn add lighthouse

让我们编辑 src/index.ts

#!/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(
    `🗼 Running Lighthouse for ${url}. It will take a while, please wait...`
  )
  
  const results = []

  for (let i = 0; i < options.iteration; i++) {
    const { status, stdout } = spawn.sync(
      process.execPath, [
      lighthouse,
      url,
      '--output=json',
      '--chromeFlags=--headless',
      '--only-categories=performance',
    ])

    if (status !== 0) {
      continue
    }

    results.push(JSON.parse(stdout.toString()))
  }
}
      
run()

在上面的代码中,根据用户输入,多次生成新进程。在每个过程中,使用无头Chrome 运行 Lighthouse 性能分析,并收集 JSON 数据。该 result 变量将以字符串的形式保存一组独立的性能数据,下一步是汇总数据并计算最可靠的性能分数。

如果您实现了上面的代码,您将看到一个关于 requirelinting 错误,是因为 require.resolve 解析模块的路径而不是模块本身。在本文中,我们将允许编译 .eslintrc.js 中的 @typescript-eslint/no-var-requires 规则。

// .eslintrc.js
module.exports = {
+  rules: {
+    // allow require
+    '@typescript-eslint/no-var-requires': 0,
+  },
}

计算可靠的 Lighthouse 分数

一种策略是通过计算中位数来汇总报告,Lighthouse 提供了一个内部功能computeMedianRun,让我们使用它。

#!/usr/bin/env node

import chalk from 'chalk';
import { Command } from 'commander'
import spawn from 'cross-spawn'
import {draw} from './utils'

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

// For simplicity, we use require here because lighthouse doesn't provide type declaration.
const {
  computeMedianRun,
} = require('lighthouse/lighthouse-core/lib/median-run.js')

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(
    `🗼 Running Lighthouse for ${url}. It will take a while, please wait...`
  )
  
  const results = []

  for (let i = 0; i < options.iteration; i++) {
    const { status, stdout } = spawn.sync(
      process.execPath, [
      lighthouse,
      url,
      '--output=json',
      '--chromeFlags=--headless',
      '--only-categories=performance',
    ])

    if (status !== 0) {
      continue
    }

    results.push(JSON.parse(stdout.toString()))
  }
                                         
  const median = computeMedianRun(results)
                                         
  console.log(`\n${chalk.green('✔')} Report is ready for ${median.finalUrl}`)
  console.log(
    `🗼 Median performance score: ${draw(
      median.categories.performance.score,
      median.categories.performance.score * 100
    )}`
  )
  
  const primaryMatrices = [
    'first-contentful-paint',
    'interactive',
    'speed-index',
    'total-blocking-time',
    'largest-contentful-paint',
    'cumulative-layout-shift',
  ];

  primaryMatrices.map((matrix) => {
    const { title, displayValue, score } = median.audits[matrix];
    console.log(`🗼 Median ${title}: ${draw(score, displayValue)}`);
  });
}
      
run()

在底层,computeMedianRun 返回最接近第一次 Contentful Paint 的中位数和 Time to Interactive 的中位数的分数。这是因为它们表示页面初始化生命周期中的最早和最新时刻,这是一种确定中位数的更可靠的方法,而不是简单的从单个测量中找到中位数的方法。

现在再试一次命令,看看结果如何。

$ yarn build && my-script https://dawchihliou.github.io --iteration=3

在 GitHub Actions 中使用 CLI

我们的实现已经完成,让我们在自动化的工作流中使用 CLI,这样我们就可以在CD/CI 管道中对性能进行基准测试。

首先,让我们在 NPM 上发布这个包(假设)。

我发布了一个 NPM 包 dx-scripts,其中包含了 my-script 的生产版本,我们将用 dx-script 编写 GitHub Actions 工作流来演示我们的 CLI 应用程序。

在 NPM 上发布(示例)

我们需要在 packgage.json 中添加一个 files 属性,来发布 /bin 目录。

// package.json

{
+  "files": ["bin"],
}

然后简单的运行:

$ yarn publish

现在包就在 NPM 上了(假设)!

编写工作流

让我们讨论一下工作流,我们希望工作流:

  • 当有更新时运行一个 pull 请求;
  • 针对功能分支预览 URL 运行 Lighthouse 性能分析;
  • 用分析报告通知 pull 请求;

因此,在工作流成功完成后,您将看到来自 GitHub Action Bot 的评论与您的 Lighthouse 分数。

为了专注于 CLI 的应用,我将在工作流中对功能分支预览 URL 进行硬编码。

在应用程序存储库中,安装 dx-scripts

$ yarn add --dev dx-script

添加一个 lighthouse-dev-ci.yaml 到 GitHub 工作流目录中:

# .github/workflows/lighthouse-dev-ci.yaml

name: Lighthouse Dev CI
on: pull_request
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    env:
      # You can substitute the harcoded preview url with your preview url
      preview_url: https://dawchihliou.github.io/
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: '16.x'
      - name: Install dependencies
        run: yarn
      # You can add your steps here to create a preview
      - name: Run Lighthouse
        id: lighthouse
        shell: bash
        run: |
          lighthouse=$(npx dx-scripts lighthouse $preview_url)
          lighthouse="${lighthouse//'%'/'%25'}"
          lighthouse="${lighthouse//$'\n'/'%0A'}"
          lighthouse="${lighthouse//$'\r'/'%0D'}"
          echo "::set-output name=lighthouse_report::$lighthouse"
      - name: Notify PR
        uses: wow-actions/auto-comment@v1
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          pullRequestSynchronize: |
            👋 @{{ author }},
            Here is your Lighthouse performance overview🎉
            ```
            ${{ steps.lighthouse.outputs.lighthouse_report }}
            ```

在 “Run Lighthouse” 步骤中,我们运行 dx-script Lighthouse CLI,替换特殊字符以打印多行输出,并将输出设置在一个变量 lighthouse_report 中。在 “Notify PR” 步骤中,我们用 “Run Lighthouse” 步骤的输出写了一条评论,并使用 wow-actions/auto-comment 操作来发布评论。

写一个 CLI 还不错吧?让我们来看看我们已经涵盖的所有内容:

  • 配置 TypeScript;
  • 配置 ESLint;
  • 配置 Prettier;
  • 在本地执行您的命令;
  • 用着色日志 chalk;
  • 定义你的命令 commander
  • spawning processes;
  • 执行 Lighthouse CLI;
  • 使用 Lighthouse 的内部库计算平均性能分数;
  • 将您的命令发布为 npm 包;
  • 将您的命令应用于 GitHub Action 工作流程。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK