TL;DR
- 您可以輕鬆編寫 CLI,它比你想象的要簡單;
- 我們一起編寫 CLI 以生成 Lighthouse 效能報告;
- 你將看到如何配置 TypeScript、EsLint 和 Prettier;
- 你會看到如何使用一些很優秀的庫,比如
chalk
和commander
; - 你將看到如何產生多個程式;
- 你會看到如何在 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.json
的 compilerOptions
中指定 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 chalk@4.1.2
確保你安裝的是 chalk 4
,chalk 5
是純 ESM,在 TypeScript 4.6 釋出之前,我們無法將其與 TypeScript 一起使用。
chalk
為 console.log
提供顏色,例如:
// src/index.ts
import chalk from 'chalk'
console.log(chalk.green('Hello from my-script'))
現在在你的專案根目錄下執行 yarn build && my-script
並檢視輸出日誌,會發現列印結果變成了綠色。
讓我們用一種更有意義的方式來使用 chalk
,Lighthouse 的效能分數是採用顏色標記的。我們可以編寫一個實用函式,根據效能評分用顏色顯示數值。
// 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
去定義:
要使用引數和選項,我們首先解析命令並記錄變數。
現在我們可以執行命令並觀察輸出日誌。
$ 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
變數將以字串的形式儲存一組獨立的效能資料,下一步是彙總資料並計算最可靠的效能分數。
如果您實現了上面的程式碼,您將看到一個關於 require
的 linting
錯誤,是因為 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 工作流程。
資源
- Lighthouse official website
- Lighthouse performance scoring
- Lighthouse Variability
commander
GitHub repositorychalk
GitHub repositorycross-spawn
GitHub repository@typescript-eslint/parser
GitHub repository@typescript-eslint/eslint-plugin
GitHub respositorydx-scripts
GitHub repository- Prettier & ESLint recommended configuration on GitHub
lighthouse/lighthouse-core/lib/median-run.js
on GitHubwow-actions/auto-comment
GitHub Actions Marketplace