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

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

$ 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.tsconsole.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.jsmodule.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.jsmodule.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 nodeconsole.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 chalk@4.1.2

确保你装置的是 chalk 4chalk 5是纯 ESM,在 TypeScript 4.6 公布之前,咱们无奈将其与 TypeScript 一起应用。

chalkconsole.log 提供色彩,例如:

// src/index.tsimport chalk from 'chalk'console.log(chalk.green('Hello from my-script'))

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

让咱们用一种更有意义的形式来应用 chalk,Lighthouse 的性能分数是采纳色彩标记的。咱们能够编写一个实用函数,依据性能评分用色彩显示数值。

// src/utils.tsimport 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.tsimport { 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 nodeimport { 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 nodeimport { 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.jsmodule.exports = {+  rules: {+    // allow require+    '@typescript-eslint/no-var-requires': 0,+  },}

计算牢靠的 Lighthouse 分数

一种策略是通过计算中位数来汇总报告,Lighthouse 提供了一个外部性能computeMedianRun,让咱们应用它。

#!/usr/bin/env nodeimport 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.yamlname: Lighthouse Dev CIon: pull_requestjobs:  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 工作流程。

资源

  • Lighthouse official website
  • Lighthouse performance scoring
  • Lighthouse Variability
  • commander GitHub repository
  • chalk GitHub repository
  • cross-spawn GitHub repository
  • @typescript-eslint/parser GitHub repository
  • @typescript-eslint/eslint-plugin GitHub respository
  • dx-scripts GitHub repository
  • Prettier & ESLint recommended configuration on GitHub
  • lighthouse/lighthouse-core/lib/median-run.js on GitHub
  • wow-actions/auto-comment GitHub Actions Marketplace