
作者:子弈
轉(zhuǎn)發(fā)鏈接:https://juejin.im/post/6856410900577026061
目錄
從零開始配置 TypeScript 項(xiàng)目的教程(一)
從零開始配置 TypeScript 項(xiàng)目的教程(二) 本篇
Jest 確保構(gòu)建
單獨(dú)通過執(zhí)行Unpm run test 命令進(jìn)行單元測(cè)試,這里演示執(zhí)行構(gòu)建命令時(shí)的單元測(cè)試(需要保證構(gòu)建之前所有的單元測(cè)試用例都能通過)。如果測(cè)試失敗,那么應(yīng)該防止繼續(xù)構(gòu)建,例如進(jìn)行失敗的構(gòu)建行為:
PS C:CodeGitalgorithms> npm run build
> [email protected] build C:CodeGitalgorithms
> npm run lint-strict && npm run jest && rimraf dist types && gulp
> [email protected] lint-strict C:CodeGitalgorithms
> eslint src --max-warnings 0
> [email protected] jest C:CodeGitalgorithms
> jest --coverage
PASS dist/test/greet.spec.js
FAIL test/greet.spec.ts
● src/greet.ts › name param test
expect(received).toBe(expected) // Object.is equality
Expected: "Hello from world 1"
Received: "Hello from world"
3 | describe("src/greet.ts", () => {
4 | it("name param test", () => {
> 5 | expect(greet("world")).toBe("Hello from world 1");
| ^
6 | });
7 | });
8 |
at Object.<anonymous> (test/greet.spec.ts:5:28)
----------|---------|----------|---------|---------|-------------------
| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
| ---------- | --------- | ---------- | --------- | --------- | ------------------- |
| All files | 100 | 100 | 100 | 100 |
| greet.ts | 100 | 100 | 100 | 100 |
| ---------- | --------- | ---------- | --------- | --------- | ------------------- |
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 3.45 s
Ran all test suites.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] jest: `jest --coverage`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] jest script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! C:Users子弈AppDataRoamingnpm-cache_logs2020-07-12T13_42_11_628Z-debug.log
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] build: `npm run lint-strict && npm run jest && rimraf dist types && gulp`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! C:Users子弈AppDataRoamingnpm-cache_logs2020-07-12T13_42_11_673Z-debug.log
需要注意由于是并行(&&)執(zhí)行腳本,因此執(zhí)行構(gòu)建命令時(shí)(npm run build)會(huì)先執(zhí)行 ESLint 校驗(yàn),如果 ESLint 校驗(yàn)失敗那么退出構(gòu)建,否則繼續(xù)進(jìn)行 Jest 單元測(cè)試。如果單元測(cè)試失敗那么退出構(gòu)建,只有當(dāng)兩者都通過時(shí)才會(huì)進(jìn)行源碼構(gòu)建。
Jest 確保代碼上傳
除了預(yù)防不負(fù)責(zé)任的代碼構(gòu)建以外,還需要預(yù)防不負(fù)責(zé)任的代碼提交。配合 lint-staged 可以防止未跑通單元測(cè)試的代碼進(jìn)行遠(yuǎn)程提交:
"scripts": {
"lint": "eslint src --max-warnings 0",
"test": "jest --bail --coverage",
},
"lint-staged": {
"*.ts": [
"npm run lint",
"npm run test"
]
}
此時(shí)如果單元測(cè)試有誤,都會(huì)停止代碼提交:
husky > pre-commit (node v12.13.1)
[STARTED] Preparing...
[SUCCESS] Preparing...
[STARTED] Running tasks...
[STARTED] Running tasks for *.ts
[STARTED] npm run lint
[SUCCESS] npm run lint
[STARTED] npm run jest
[FAILED] npm run jest [FAILED]
[FAILED] npm run jest [FAILED]
[SUCCESS] Running tasks...
[STARTED] Applying modifications...
[SKIPPED] Skipped because of errors from tasks.
[STARTED] Reverting to original state because of errors...
[SUCCESS] Reverting to original state because of errors...
[STARTED] Cleaning up...
[SUCCESS] Cleaning up...
× npm run jest:
FAIL test/greet.spec.ts
src/greet.ts
× name param test (4 ms)
● src/greet.ts › name param test
expect(received).toBe(expected) // Object.is equality
Expected: "Hello from world 1"
Received: "Hello from world"
3 | describe("src/greet.ts", () => {
4 | it("name param test", () => {
> 5 | expect(greet("world")).toBe("Hello from world 1");
| ^
6 | });
7 | });
8 |
at Object.<anonymous> (test/greet.spec.ts:5:28)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 1.339 s, estimated 3 s
Ran all test suites related to files matching /C:\Code\Git\algorithms\src\index.ts|C:\Code\Git\algorithms\test\greet.spec.ts/i.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] jest: `jest --bail --findRelatedTests --coverage "C:/Code/Git/algorithms/src/index.ts" "C:/Code/Git/algorithms/test/greet.spec.ts"`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] jest script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! C:Users子弈AppDataRoamingnpm-cache_logs2020-07-12T14_33_51_183Z-debug.log
> [email protected] jest C:CodeGitalgorithms
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] jest script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! C:Users子弈AppDataRoamingnpm-cache_logs2020-07-12T14_33_51_183Z-debug.log
> [email protected] jest C:CodeGitalgorithms
> jest --bail --findRelatedTests --coverage "C:/Code/Git/algorithms/src/index.ts" "C:/Code/Git/algorithms/test/greet.spec.ts"
----------|---------|----------|---------|---------|-------------------
| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
| ---------- | --------- | ---------- | --------- | --------- | ------------------- |
| All files | 0 | 0 | 0 | 0 |
| ---------- | --------- | ---------- | --------- | --------- | ------------------- |
husky > pre-commit hook failed (add --no-verify to bypass)
git exited with error code 1
溫馨提示:想要了解更多關(guān)于 Jest 的生態(tài)可以查看 awesome-jest[113]。
Jest 對(duì)于 ESLint 支持
src 目錄下的源碼通過配置 @typescript-eslint/eslint-plugin 可進(jìn)行推薦規(guī)則的 ESLint 校驗(yàn),為了使得 test 目錄下的測(cè)試代碼能夠進(jìn)行復(fù)合 Jest 推薦規(guī)則的 ESLint 校驗(yàn),可以通過配置 eslint-plugin-jest[114] 進(jìn)行支持(ts-jest 項(xiàng)目就是采用了該插件進(jìn)行 ESLint 校驗(yàn),具體可查看配置文件 `ts-jest/.eslintrc.js`[115]),這里仍然采用推薦的規(guī)則配置:
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
// 新增推薦的 ESLint 校驗(yàn)規(guī)則
// 所有規(guī)則集查看:https://github.com/jest-community/eslint-plugin-jest#rules(recommended 標(biāo)識(shí)表明是推薦規(guī)則)
"plugin:jest/recommended",
],
};
為了驗(yàn)證推薦規(guī)則是否生效,這里可以找一個(gè) `no-identical-title`[116] 規(guī)則進(jìn)行驗(yàn)證:
import greet from "@/greet";
describe("src/greet.ts", () => {
it("name param test", () => {
expect(greet("world")).toBe("Hello from world 1");
});
});
// 這里輸入了重復(fù)的 title
describe("src/greet.ts", () => {
it("name param test", () => {
expect(greet("world")).toBe("Hello from world 1");
});
});
需要注意修改 package.json 中的 ESLint 校驗(yàn)范圍:
"scripts": {
// 這里對(duì) src 和 test 目錄進(jìn)行 ESLint 校驗(yàn)
"lint": "eslint src test --max-warnings 0",
},
執(zhí)行 npm run lint 進(jìn)行單元測(cè)試的格式校驗(yàn):
PS C:CodeGitalgorithms> npm run lint
> [email protected] lint C:CodeGitalgorithms
> eslint src test --max-warnings 0
C:CodeGitalgorithmstestgreet.spec.ts
9:10 error Describe block title is used multiple times in the same describe block jest/no-identical-title
? 1 problem (1 error, 0 warnings)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] lint: `eslint src test --max-warnings 0`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] lint script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! C:Users子弈AppDataRoamingnpm-cache_logs2020-07-13T02_25_12_043Z-debug.log
此時(shí)會(huì)發(fā)現(xiàn) ESLint 拋出了相應(yīng)的錯(cuò)誤信息。需要注意采用此 ESLint 校驗(yàn)之后也會(huì)在 VS Code 中實(shí)時(shí)生成錯(cuò)誤提示(相應(yīng)的代碼下會(huì)有紅色波浪線,鼠標(biāo)移入后會(huì)產(chǎn)生 Tooltip 提示該錯(cuò)誤的相應(yīng)規(guī)則信息,除此之外當(dāng)前工程目錄下對(duì)應(yīng)的文件名也會(huì)變成紅色),此后的 Git 提交以及 Build 構(gòu)建都會(huì)失敗!
溫馨提示:如果你希望 Jest 測(cè)試的代碼需要一些格式規(guī)范,那么可以查看 eslint-plugin-jest-formatting[117] 插件。
Npm Script Hook
當(dāng)你查看前端開源項(xiàng)目時(shí)第一時(shí)間可能會(huì)找 package.json 中的 main、bin 以及files 等字段信息,除此之外如果還想深入了解項(xiàng)目的結(jié)構(gòu),可能還會(huì)查看scripts 腳本字段信息用于了解項(xiàng)目的開發(fā)、構(gòu)建、測(cè)試以及安裝等流程。npm 的腳本功能非常強(qiáng)大,你可以利用腳本制作項(xiàng)目需要的任何流程工具。本文不會(huì)過多介紹 npm 腳本的功能,只是講解一下其中用到的 鉤子[118] 功能。
目前在本項(xiàng)目中使用的一些腳本命令如下(就目前而言腳本相對(duì)較少,定義還蠻清晰的):
"lint": "eslint src test --max-warnings 0",
"test": "jest --bail --coverage",
"build": "npm run lint && npm run prettier && npm run test && rimraf dist types && gulp",
"changelog": "rimraf CHANGELOG.md && conventional-changelog -p angular -i CHANGELOG.md -s"
重點(diǎn)看下 build 腳本命令,會(huì)發(fā)現(xiàn)這個(gè)腳本命令包含了大量的繼發(fā)執(zhí)行腳本,但真正和 build 相關(guān)的只有 rimraf dist types && gulp 這兩個(gè)腳本。這里通過 npm 的腳本鉤子 pre 和 post 將腳本的功能區(qū)分開,從而使腳本的語義更加清晰(當(dāng)然腳本越來越多的時(shí)候也可能容易增加開發(fā)者的認(rèn)知負(fù)擔(dān))。npm 除了指定一些特殊的腳本鉤子以外(例如prepublish、postpublish、preinstall、postinstall等),還可以對(duì)任意腳本增加 pre 和 post 鉤子,這里通過自定義鉤子將并發(fā)執(zhí)行的腳本進(jìn)行簡(jiǎn)化:
"lint": "eslint src test --max-warnings 0",
"test": "jest --bail --coverage",
"prebuild": "npm run lint && npm run prettier && npm run test",
"build": "rimraf dist types && gulp",
"changelog": "rimraf CHANGELOG.md && conventional-changelog -p angular -i CHANGELOG.md -s"
此時(shí)如果執(zhí)行 npm run build 命令時(shí)事實(shí)上類似于執(zhí)行了以下命令:
npm run prebuild && npm run build
之后設(shè)計(jì)的腳本如果繼發(fā)執(zhí)行繁多,那么都會(huì)采用 npm scripts hook 進(jìn)行設(shè)計(jì)。
溫馨提示:大家可能會(huì)奇怪什么地方需要類似于 preinstall 或 preuninstall 這樣的鉤子,例如查看 husky - package.json[119],husky 在安裝的時(shí)候因?yàn)橐踩?Git Hook 腳本從而帶來了一些副作用(此時(shí)當(dāng)然可以通過 preinstall觸發(fā) Git Hook 腳本植入的邏輯)。如果不想使用 husky,那么卸載后需要清除植入的腳本從而不妨礙原有的 Git Hook 功能。當(dāng)然如果想要了解更多關(guān)于 npm 腳本的信息,可以查看 npm-scripts[120] 或 npm scripts 使用指南[121]。
Vuepress
Vuepress 背景
一般組件庫或工具庫都需要設(shè)計(jì)一個(gè)演示文檔(提供良好的開發(fā)體驗(yàn))。一般的工具庫可以采用 tsdoc[122]、jsdoc[123] 或 esdoc[124] 等工具進(jìn)行 API 文檔的自動(dòng)生成,但往往需要符合一些注釋規(guī)范,這些注釋規(guī)范在某種程度上可能會(huì)帶來開發(fā)負(fù)擔(dān),當(dāng)然也可以交給 VS Code 的插件進(jìn)行一鍵生成,例如 Document This For jsdoc[125] 或 TSDoc Comment[126]。
組件庫 Element UI 采用 vue-markdown-loader[127](Convert Markdown file to Vue Component using markdown-it) 進(jìn)行組件的 Demo 演示設(shè)計(jì),但是配置相對(duì)復(fù)雜。更簡(jiǎn)單的方式是配合 Vuepress[128] 進(jìn)行設(shè)計(jì),它的功能非常強(qiáng)大,但前提是熟悉 Vue,因?yàn)榭梢栽?Markdown 中使用 Vue 語法。當(dāng)然如果是 React 組件庫的 Demo 演示,則可以采用 dumi[129] 生成組件 Demo 演示文檔(不知道沒有更加好用的類 Vuepress 的 React 組件文檔生成器, 更多和 React 文檔相關(guān)也可以了解 react-markdown[130]、react-static[131] 等)。
由于之前采用過 Vuepress 設(shè)計(jì) Vue 組件庫的 Demo 演示文檔,因此這里仍然沿用它來設(shè)計(jì)工具庫包的 API 文檔(如果你想自動(dòng)生成 API 文檔,也可以額外配合 tsdoc 工具)。采用 Vuepress 設(shè)計(jì)文檔的主要特點(diǎn)如下:
- 可以在 Markdown 中直接使用 Vue(還可以自定義 Vue 文檔視圖組件)
- 內(nèi)置了很多 Markdown 拓展
- 可以使用 Webpack 進(jìn)行構(gòu)建定制化配置
- 默認(rèn)主題支持搜索能力
- 可以安裝 Vuepress 插件(后續(xù)需要支持的 Latex[132] 排版就可以利用現(xiàn)有的插件能力生成)
- 默認(rèn)響應(yīng)式
Vuepress 配置
先按照官方的 快速上手[133] 文檔進(jìn)行依賴安裝和 npm scripts 腳本設(shè)置:
"scripts": {
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
}
按照 Vuepress 官網(wǎng)約定優(yōu)于配置的原則進(jìn)行演示文檔的目錄結(jié)構(gòu)[134]設(shè)計(jì),官方的文檔可能一下子難以理解,可以先設(shè)計(jì)一個(gè)最簡(jiǎn)單的目錄:
.
├── docs
│ ├── .vuepress
│ │ └── config.js # 配置文件
│ └── README.md # 文檔首頁
└── package.json
根據(jù)默認(rèn)主題 / 首頁[135]在 docs/README.md 進(jìn)行首頁設(shè)計(jì):
---
home: true
# heroImage: /hero.png
heroText: algorithms-utils
tagline: 算法與 TypeScript 實(shí)現(xiàn)
actionText: 開始學(xué)習(xí)
actionLink: /guide/
features:
- title: 精簡(jiǎn)理論
details: 精簡(jiǎn)《算法導(dǎo)論》的內(nèi)容,幫助自己更容易學(xué)習(xí)算法理論知識(shí)。
- title: 習(xí)題練習(xí)
details: 解答《算法導(dǎo)論》的習(xí)題,幫助自己更好的實(shí)踐算法理論知識(shí)。
- title: 面題精選
details: 搜集常見的面試題目,提升自己的算法編程能力以及面試通過率。
footer: MIT Licensed | Copyright © 2020-present 子弈
---
根據(jù)配置[136] 對(duì) docs/.vuepress/config.js 文件進(jìn)行基本配置:
const packageJson = require("../../package.json");
module.exports = {
// 配置網(wǎng)站標(biāo)題
title: packageJson.name,
// 配置網(wǎng)站描述
description: packageJson.description,
// 配置基本路徑
base: "/algorithms/",
// 配置基本端口
port: "8080",
};
此時(shí)通過 npm run docs:dev 進(jìn)行開發(fā)態(tài)文檔預(yù)覽:
PS C:CodeGitalgorithms> npm run docs:dev
> [email protected] docs:dev C:CodeGitalgorithms
> vuepress dev docs
wait Extracting site metadata...
tip Apply theme @vuepress/theme-default ...
tip Apply plugin container (i.e. "vuepress-plugin-container") ...
tip Apply plugin @vuepress/register-components (i.e. "@vuepress/plugin-register-components") ...
tip Apply plugin @vuepress/active-header-links (i.e. "@vuepress/plugin-active-header-links") ...
tip Apply plugin @vuepress/search (i.e. "@vuepress/plugin-search") ...
tip Apply plugin @vuepress/nprogress (i.e. "@vuepress/plugin-nprogress") ...
√ Client
Compiled successfully in 5.31s
i ?wds?: Project is running at http://0.0.0.0:8080/
i ?wds?: webpack output is served from /algorithms-utils/
i ?wds?: Content not from webpack is served from C:CodeGitalgorithmsdocs.vuepresspublic
i ?wds?: 404s will fallback to /index.html
success [23:13:14] Build 10b15a finished in 5311 ms!
> VuePress dev server listening at http://localhost:8080/algorithms-utils/
效果如下:

當(dāng)然除了以上設(shè)計(jì)的首頁,在本項(xiàng)目中還會(huì)設(shè)計(jì)導(dǎo)航欄[137]、側(cè)邊欄[138]、使用插件[139]、使用組件[140]等。這里重點(diǎn)講解一下 Webpack 構(gòu)建[141] 配置。
為了在 Markdown 文檔中可以使用 src 目錄的 TypeScript 代碼,這里對(duì) .vuepress/config.js 文件進(jìn)行配置處理:
const packageJson = require("../../package.json");
const sidebar = require("./config/sidebar.js");
const nav = require("./config/nav.js");
const path = require("path");
module.exports = {
title: packageJson.name,
description: packageJson.description,
base: "/algorithms/",
port: "8080",
themeConfig: {
nav,
sidebar,
},
plugins: [
"vuepress-plugin-cat",
[
"mathjax",
{
target: "svg",
macros: {
"*": "\times",
},
},
],
// 增加 Markdown 文檔對(duì)于 TypeScript 語法的支持
[
"vuepress-plugin-typescript",
{
tsLoaderOptions: {
// ts-loader 的所有配置項(xiàng)
},
},
],
],
chainWebpack: (config) => {
config.resolve.alias.set("image", path.resolve(__dirname, "public"));
// 在文檔中模擬庫包的引入方式
// 例如發(fā)布了 algorithms-utils 庫包之后,
// import greet from 'algorithms-utils/greet.ts' 在 Vuepress 演示文檔中等同于
// import greet from '~/src/greet.ts',
// 其中 ~ 在這里只是表示項(xiàng)目根目錄
config.resolve.alias.set(
"algorithms-utils",
path.resolve(__dirname, "../../src")
);
},
};
溫馨提示:這里的 Webpack 配置采用了 webpack-chain[142] 鏈?zhǔn)讲僮鳎绻胍捎?Webpack 對(duì)象的配置方式則可以查看 Vuepress - 構(gòu)建流程 - configurewebpack[143]。
此時(shí)可以在 Vuepress 的 Markdown 文檔中進(jìn)行 TypeScript 引入的演示文檔設(shè)計(jì):
# Test vuepress
::: danger 測(cè)試 Vuepress
引入 greet.ts 并進(jìn)行調(diào)用測(cè)試。
:::
<template>
<collapse title="查看答案">{{msg}}</collapse>
</template>
<template>
<div>{{msg}}</div>
</template>
<script lang="ts">
import greet from 'algorithms-utils/greet'
const msg = greet('ziyi')
export default {
data() {
return {
msg
}
},
}
</script>
啟動(dòng) Vuepress 查看演示文檔:

可以發(fā)現(xiàn)在 Markdown 中引入的 src/greet.ts 代碼生效了,最終通過 npm run docs:build 可以生成演示文檔的靜態(tài)資源進(jìn)行部署和訪問。
溫馨提示:更多本項(xiàng)目的 Vuepress 配置信息可查看 Commit 信息,除此之外如果還想知道更多 Vuepress 的生態(tài),例如有哪些有趣插件或主題,可查看 awesome-vuepress[144] 或 Vuepress 社區(qū)[145]。
文檔工具和規(guī)范
通常在書寫文檔的時(shí)候很多同學(xué)都不注重文檔的潔癖,其實(shí)書寫文檔和書寫代碼一樣需要一些格式規(guī)范。markdownlint[146] 是類似于 ESLint 的 Markdown 格式校驗(yàn)工具,通過它可以更好的規(guī)范我們書寫的文檔。當(dāng)然 Markdown 的格式校驗(yàn)不需要像 ESLint 或者 Prettier 校驗(yàn)?zāi)菢舆M(jìn)行強(qiáng)約束,簡(jiǎn)單的能夠做到提示和 Save Auto Fix 即可。
通過安裝 Vs Code 插件 markdownlint[147] 并進(jìn)行 Save Auto Fix 配置(在插件中明確列出了哪些規(guī)則是可以被 Fix 的)。安裝完成后查看剛剛進(jìn)行的測(cè)試文件:

此時(shí)會(huì)發(fā)現(xiàn)插件生效了,但是在 Markdown 中插入 html 是必須的一個(gè)能力(Vuepress 支持的能力就是在 Markdown 中使用 Vue),因此可以通過 .markdownlintrc 文件將相應(yīng)的規(guī)則屏蔽掉。
溫馨提示:如果你希望在代碼提交之前或文檔構(gòu)建之前能夠進(jìn)行 Markdown 格式校驗(yàn),則可以嘗試它的命令行接口 markdownlint-cli[148]。除此之外,如果對(duì)文檔的設(shè)計(jì)沒有想法或者不清楚如何書寫好的技術(shù)文檔,可以查看 技術(shù)文章的寫作技巧分享[149],一定能讓你有所收獲。
Github Actions
CI / CD 背景
前提提示:個(gè)人對(duì)于 CI / CD 可能相對(duì)不夠熟悉,只是簡(jiǎn)單的玩過 Travis、Gitlab CI / CD 以及 Jenkins。
關(guān)于 CI / CD 的背景這里就不再過多介紹,有興趣的同學(xué)可以看看以下一些好文:
- Introduction to CI/CD with GitLab(中文版)[150]
- GitHub Actions 入門教程[151]
- Github Actions 官方文檔[152]
- 當(dāng)我有服務(wù)器時(shí)我做了什么 · 個(gè)人服務(wù)器運(yùn)維指南[153](這個(gè)系列有點(diǎn)佩服啊)
在 Introduction to CI/CD with GitLab(中文版)[154] 中你可以清晰的了解到 CI 和 CD 的職責(zé)功能:

通過以下圖可以更清晰的發(fā)現(xiàn) Gitlab 在每個(gè)階段可用的功能:

由于本項(xiàng)目依賴 Github,因此沒法使用 Gitlab 默認(rèn)集成的能力。之前的 Github 項(xiàng)目采用了 Travis 進(jìn)行項(xiàng)目的 CI / CD 集成,現(xiàn)在因?yàn)橛辛烁奖愕?Github Actions,因此決定采用 Github 自帶的 Actions 進(jìn)行 CI / CD 能力集成(大家如果想更多了解這些 CI / CD 的差異請(qǐng)自行 google 哈)。Github Actions 所帶來的好處在于:
- 可復(fù)用的 Actions(以前你需要寫復(fù)雜的腳本,現(xiàn)在可以復(fù)用別人寫好的腳本,可以簡(jiǎn)單理解為 CI 腳本插件化)
- 支持更多的 webhook[155],這些當(dāng)然是 Github 生態(tài)特有的競(jìng)爭(zhēng)力
當(dāng)然也會(huì)產(chǎn)生一些限制[156],這些限制主要是和執(zhí)行時(shí)間以及次數(shù)相關(guān)。需要注意類似于 Jenkins 等支持本地連接運(yùn)行,Github Actions 也支持連接到本地機(jī)器運(yùn)行 workflow,因此部分限制可能不受本地運(yùn)行的限制。
溫馨提示:本項(xiàng)目中使用到的 CI / CD 功能相對(duì)簡(jiǎn)單,如果想了解更多通用的 Actions,可查看 官方 Actions[157] 和 awesome-actions[158]。最近在使用 Jenkins 做前端的自動(dòng)化構(gòu)建優(yōu)化,后續(xù)可能會(huì)出一篇簡(jiǎn)單的教程文章(當(dāng)然會(huì)跟普通講解的用法會(huì)有所不同嘍)。
Github Actions 配置
本項(xiàng)目的配置可能會(huì)包含以下三個(gè)方面:
- 自動(dòng)更新靜態(tài)資源流程
- 發(fā)布庫包流程
- 提交 Pull Request 流程
這里主要講解自動(dòng)更新靜態(tài)資源流程,大致需要分為以下幾個(gè)步驟(以下都是在 Github 服務(wù)器上進(jìn)行操作,你可以理解為新的服務(wù)環(huán)境):
- 拉取當(dāng)前 Github 倉庫代碼并切換到相應(yīng)的分支
- 安裝 Node 和 Npm 環(huán)境
- 安裝項(xiàng)目的依賴
- 構(gòu)建庫包和演示文檔的靜態(tài)資源
- 發(fā)布演示文檔的靜態(tài)資源
通過查看 官方 Actions[159] 和 awesome-actions[160],找到所需的 Actions:
- Checkout[161]: 從 Github 拉取倉庫代碼到 Github 服務(wù)器的 $GITHUB_WORKSPACE目錄下
- cache[162]: 緩存 npm
- setup-node[163]: 安裝 Node 和 Npm 環(huán)境
- actions-gh-pages[164]: 在 Github 上發(fā)布靜態(tài)資源
溫馨提示:可用的 Action 很多,這里只是設(shè)置了一個(gè)簡(jiǎn)單的流程。
在 .github/workflows 下新增 mian.yml 配置文件:
# 以下都是官方文檔的簡(jiǎn)單翻譯
# 當(dāng)前的 yml(.yaml) 文件是一個(gè) workflow,是持續(xù)集成一次運(yùn)行的一個(gè)過程,必須放置在項(xiàng)目的 .github/workflow 目錄下
# 如果不清楚 .yml 文件格式語法,可以查看 https://www.codeproject.com/Articles/1214409/Learn-YAML-in-five-minutes
# 初次編寫難免會(huì)產(chǎn)生格式問題,可以使用 VS Code 插件進(jìn)行格式檢測(cè),https://marketplace.visualstudio.com/items?itemName=OmarTawfik.github-actions-vscode
# 具體各個(gè)配置屬性可查看 https: //docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
# workflow 的執(zhí)行仍然會(huì)受到一些限制,例如
# - 每個(gè) job 最多執(zhí)行 6 小時(shí)(本地機(jī)器不受限制)
# - 每個(gè) workflow 最多執(zhí)行 72 小時(shí)
# - 并發(fā) job 的數(shù)量會(huì)受到限制
# - 更多查看 https: //docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#usage-limits
# name: 當(dāng)前 workflow 的名稱
name: Algorithms
# on: 指定 workflow 觸發(fā)的 event
#
# event 有以下幾種類型
# - webhook
# - scheduled
# - manual
on:
# push: 一個(gè) webhook event,用于提交代碼時(shí)觸發(fā) workflow,也可以是觸發(fā)列表,例如 [push, pull_request]
# workflows 觸發(fā)的 event 大部分是基于 webhook 配置,以下列舉幾個(gè)常見的 webhook:
# - delete: 刪除一個(gè) branch 或 tag 時(shí)觸發(fā)
# - fork / watch: 某人 fork / watch 項(xiàng)目時(shí)觸發(fā)(你問有什么用,發(fā)送郵件通知不香嗎?)
# - pull_request: 提交 PR 時(shí)觸發(fā)
# - page_build: 提交 Github Pages-enabled 分支代碼時(shí)觸發(fā)
# - push: 提交代碼到特定分支時(shí)觸發(fā)
# - registry_package: 發(fā)布或跟新 package 時(shí)觸發(fā)
# 更多 webhook 可查看 https: //docs.github.com/en/actions/reference/events-that-trigger-workflows
# 從這里可以看出 Git Actions 的一大特點(diǎn)就是 Gihub 官方提供的一系列 webhook
push:
# branches: 指定 push 觸發(fā)的特定分支,這里你可以通過列表的形式指定多個(gè)分支
branches:
- feat/framework
#
# branches 的指定可以是通配符類型,例如以下配置可以匹配 refs/heads/releases/10
# - 'releases/**'
#
# branches 也可以使用反向匹配,例如以下不會(huì)匹配 refs/heads/releases/10
# - '!releases/**'
#
# branches-ignore: 只對(duì) [push, pull_request] 兩個(gè) webhook 起作用,用于指定當(dāng)前 webhook 不觸發(fā)的分支
# 需要注意在同一個(gè) webhook 中不能和 branches 同時(shí)使用
#
# tags: 只對(duì) [push, pull_request] 兩個(gè) webhook 起作用,用于指定當(dāng)前 webhook 觸發(fā)的 tag
#
# tags:
# - v1 # Push events to v1 tag
# - v1.* # Push events to v1.0, v1.1, and v1.9 tags
#
# tags-ignore: 類似于 branches-ignore
#
# paths、paths-ignore...
#
# 更多關(guān)于特定過濾模式可查看 https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
#
# 其他的 webhook 控制項(xiàng)還包括 types(不是所有的 webhook 都有 types),例如已 issues 為例,可以在 issues 被 open、reopened、closed 等情況下觸發(fā) workflow
# 更多 webhook 的 types 可查看 https: //docs.github.com/en/actions/reference/events-that-trigger-workflows#webhook-events
#
# on:
# issues:
# types: [opened, edited, closed]
# 除此之外如果對(duì)于每個(gè)分支有不同的 webhook 觸發(fā),則可以通過以下形式進(jìn)行多個(gè) webhook 配置
#
# push:
# branches:
# - master
# pull_request:
# branches:
# - dev
#
# 除了以上所說的 webhook event,還有 scheduled event 和 manual event
# scheduled event: 用于定時(shí)構(gòu)建,例如最小的時(shí)間間隔是 5 min 構(gòu)建一次
# 具體可查看 https: //docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events
# env: 指定環(huán)境變量(所有的 job 生效,每一個(gè) job 可以獨(dú)立通過 jobs.<job_id>.env、jobs.<job_id>.steps.env 配置)
# defaults / defaults.run: 所有的 job 生效,每一個(gè) job 可以獨(dú)立通過 jobs.<job_id>.defaults 配置
# deafults
# defaults.run
# jobs: 一個(gè) workflow 由一個(gè)或多個(gè) job 組成
jobs:
# job id: 是 job 的唯一標(biāo)識(shí),可以通過 _ 進(jìn)行連接,例如: my_first_job,例如這里的 build 就是一個(gè) job id
build_and_deploy:
# name: 在 Github 中顯示的 job 名稱
name: Build And Deploy
#
# needs: 用于繼發(fā)執(zhí)行 job,例如當(dāng)前 job build 必須在 job1 和 job2 都執(zhí)行成功的基礎(chǔ)上執(zhí)行
# needs: [job1, job2]
#
# runs-on: job 運(yùn)行的環(huán)境配置,包括:
# - windows-latest
# - windows-2019
# - ubuntu-20.04
# - ubuntu-latest
# - ubuntu-18.04
# - ubuntu-16.04
# - macos-latest
# - macos-10.15
# - self-hosted(本地機(jī)器,具體可查看 https: //docs.github.com/en/actions/hosting-your-own-runners/using-self-hosted-runners-in-a-workflow)
runs-on: ubuntu-latest
#
# outputs: 用于輸出信息
#
# env: 用于設(shè)置環(huán)境變量
#
# defaults: 當(dāng)前所有 step 的默認(rèn)配置
#
# defaults.run
# if: 滿足條件執(zhí)行當(dāng)前 job
# steps: 一個(gè) job 由多個(gè) step 組成,step 可以
# - 執(zhí)行一系列 tasks
# - 執(zhí)行命令
# - 執(zhí)行 action
# - 執(zhí)行公共的 repository
# - 在 Docker registry 中的 action
steps:
#
# id: 類似于 job id
#
# if: 類似于 job if
#
# name: 當(dāng)前 step 的名字
- name: Checkout
#
# uses: 用于執(zhí)行 action
#
# action: 可以重復(fù)使用的單元代碼
# - 為了 workflow 的安全和穩(wěn)定建議指定 action 的發(fā)布版本或 commit SHA
# - 使用指定 action 的 major 版本,這樣可以允許你接收 fixs 以及 安全補(bǔ)丁并同時(shí)保持兼容性
# - 盡量不建議使用 master 版本,因?yàn)?nbsp;master 很有可能會(huì)被發(fā)布新的 major 版本從而破壞了 action 的兼容性
# - action 可能是 JAVAScript 文件或 Docker 容器,如果是 Docker 容器,那么 runs-on 必須指定 linux 環(huán)境
#
# 指定固定 commit SHA
# uses: actions/setup-node@74bc508
# 指定一個(gè) major 發(fā)布版本
# uses: actions/setup-node@v1
# 指定一個(gè) minor 發(fā)布版本
# uses: actions/[email protected]
# 指定一個(gè)分支
# uses: actions/setup-node@master
# 指定一個(gè) Github 倉庫子目錄的特定分支、ref 或 SHA
# uses: actions/aws/ec2@master
# 指定當(dāng)前倉庫所在 workflows 的目錄地址
# uses: ./.github/actions/my-action
# 指定在 Dock Hub 發(fā)布的 Docker 鏡像地址
# uses: docker: //alpine: 3.8
# A Docker image in a public registry
# uses: docker: //gcr.io/cloud-builders/gradle
# checkout action 主要用于向 github 倉庫拉取源代碼(需要注意 workflow 是運(yùn)行在服務(wù)器上,因此需要向當(dāng)前 github 拉取倉庫源代碼)
# 它的功能包括但不限于
# - Fetch all history for all tags and branches
# - Checkout a different branch
# - Checkout HEAD^
# - Checkout multiple repos (side by side)
# - Checkout multiple repos (nested)
# - Checkout multiple repos (private)
# - Checkout pull request HEAD commit instead of merge commit
# - Checkout pull request on closed event
# - Push a commit using the built-in token
# checkout action: https: //github.com/actions/checkout
uses: actions/checkout@v2
# with: action 提供的輸入?yún)?shù)
with:
# 指定 checkout 的分支、tag 或 SHA
# 更多 checkout action 的配置可查看 https: //github.com/actions/checkout#usage
ref: feat/ci
# args: 用于 Docker 容器的 CMD 指令參數(shù)
# entrypoint: Docker 容器 action(覆蓋 Dockerfile 的 ENTRYPOINT) 和 JavaScript action 都可以使用
#
# run: 使用當(dāng)前的操作系統(tǒng)的默認(rèn)的 non-login shell 執(zhí)行命令行程序
# 運(yùn)行單個(gè)腳本
# run: npm install
# 運(yùn)行多個(gè)腳本
# run: |
# npm ci
# npm run build
#
# working-directory: 用于指定當(dāng)前腳本運(yùn)行的目錄
# working-directory: ./temp
#
# shell: 可以指定 shell 類型進(jìn)行執(zhí)行,例如 bash、pwsh、Python、sh、cmd、powershell
# shell: bash
#
# env: 除了可以設(shè)置 workflow 以及 job 的 env,也可以設(shè)置 step 的 env(可以理解為作用域不同,局部作用域的優(yōu)先級(jí)更高)
#
# comtinue-on-error: 默認(rèn)當(dāng)前 step 失敗則會(huì)阻止當(dāng)前 job 繼續(xù)執(zhí)行,設(shè)置 true 時(shí)當(dāng)前 step 失敗則可以跳過當(dāng)前 job 的執(zhí)行
- name: Cache
# cache action: https://github.com/actions/cache
# cache 在這里主要用于緩存 npm,提升構(gòu)建速率
uses: actions/cache@v2
# npm 緩存的路徑可查看 https://docs.npmjs.com/cli/cache#cache
# 由于這里 runs-on 是 ubuntu-latest,因此配置 ~/.npm
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# github-script action: https://github.com/actions/github-script
# 在 workflow 中使用 Script 語法調(diào)用 Github API 或引用 workflow context
# setup-node action: https://github.com/actions/setup-node
# 配置 Node 執(zhí)行環(huán)境(當(dāng)前構(gòu)建的服務(wù)器默認(rèn)沒有 Node 環(huán)境,可以通過 Action 安裝 Node)
# 需要注意安裝 Node 的同時(shí)會(huì)捆綁安裝 npm,如果想了解為什么會(huì)捆綁,可以 Google 一下有趣的故事哦
# 因此使用了該 action 后就可以使用 npm 的腳本在服務(wù)器進(jìn)行執(zhí)行啦
# 這里也可以嘗試 v2-beta 版本哦
- name: Set Node
uses: actions/setup-node@v1
with:
# 也可以通過 strategy.matrix.node 進(jìn)行靈活配置
# 這里本地使用 node 的 12 版本構(gòu)建,因此這里就進(jìn)行版本固定啦
node-version: "12"
- run: npm install
- run: npm run build
- run: npm run docs:build
- name: Deploy
# 用于發(fā)布靜態(tài)站點(diǎn)資源
# actions-gh-pages action: https://github.com/peaceiris/actions-gh-pages
uses: peaceiris/actions-gh-pages@v3
with:
# GTIHUB_TOKEN:https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token
# Github 會(huì)在 workflow 中自動(dòng)生成 GIHUBT_TOKEN,用于認(rèn)證 workflow 的運(yùn)行
github_token: ${{ secrets.GITHUB_TOKEN }}
# 靜態(tài)資源目錄設(shè)置
publish_dir: ./docs/.vuepress/dist
# 默認(rèn)發(fā)布到 gh-pages 分支上,可以指定特定的發(fā)布分支
publish_branch: gh-pages1 # default: gh-pages
full_commit_message: ${{ github.event.head_commit.message }}
#
# timeout-minutes: 一個(gè) job 執(zhí)行的最大時(shí)間,默認(rèn)是 6h,如果超過時(shí)間則取消執(zhí)行
#
# strategy.matrix: 例如指定當(dāng)前 job 的 node 版本列表、操作系統(tǒng)類型列表等
# strategy.fail-fast
# strategy.max-parallel
# continue-on-error: 一旦當(dāng)前 job 執(zhí)行失敗,那么 workflow 停止執(zhí)行。設(shè)置為 true 可以跳過當(dāng)前 job 執(zhí)行
# container: Docker 容器配置,包括 image、env、ports、volumes、options 等配置
#
# services: 使用 Docker 容器 Action 或者 服務(wù) Action 必須使用 Linux 環(huán)境運(yùn)行
溫馨提示:這里不再敘述具體的配置過程,更多可查看配置文件中貼出的鏈接信息。
上傳 CI 的配置文件后,Github 就會(huì)進(jìn)行自動(dòng)構(gòu)建,具體如下:

正在構(gòu)建或者構(gòu)建完成后可查看每個(gè)構(gòu)建的信息,如果初次構(gòu)建失敗則可以通過構(gòu)建信息找出失敗原因,并重新修改構(gòu)建配置嘗試再次構(gòu)建。除此之外,每次構(gòu)建失敗 Github 都會(huì)通過郵件的形式進(jìn)行通知:

如果構(gòu)建成功,則每次你推送新的代碼后,Github 服務(wù)會(huì)進(jìn)行一系列流程并自動(dòng)更新靜態(tài)資源站點(diǎn)。
總結(jié)
希望大家看完這篇文檔之后如果想使用其中某些工具能夠養(yǎng)成以下一些習(xí)慣:
- 通篇閱讀工具的文檔,了解相同功能的不同工具的差異點(diǎn)
通篇閱讀工具對(duì)應(yīng)的官方 Github README 文檔以及官方站點(diǎn)文檔,了解該工具設(shè)計(jì)的核心哲學(xué)、核心功能、解決什么核心問題。前端的工具百花齊放,同樣的功能可能可以采用多種不同的工具實(shí)現(xiàn)。如果想要在項(xiàng)目中使用適當(dāng)?shù)墓ぞ撸偷弥肋@些工具的差異。完整的閱讀相應(yīng)的官方文檔,有助于你理解各自的核心功能和差異。
- 在調(diào)研了各個(gè)工具的差異之后,選擇認(rèn)為合適的工具進(jìn)行實(shí)踐
在實(shí)踐的過程中你會(huì)對(duì)該工具的使用越來越熟悉。此時(shí)如果遇到一些問題或者想要實(shí)現(xiàn)某些功能,在通篇閱讀文檔的基礎(chǔ)上會(huì)變得相對(duì)容易。當(dāng)然如果遇到一些報(bào)錯(cuò)信息無法解決,此時(shí)第一時(shí)間應(yīng)該是搜索當(dāng)前工具所對(duì)應(yīng)的 Github Issues。除此之外,你也可以根據(jù)錯(cuò)誤的堆棧信息追蹤工具的源碼,了解源碼之后可能會(huì)對(duì)錯(cuò)誤信息產(chǎn)生的原因更加清晰。
- 在完成以上兩步之后,你應(yīng)該總結(jié)工具的使用技巧啦,此時(shí)在此通讀工具文檔可能會(huì)產(chǎn)生不一樣的收獲