前端工程化配置指南
本文講解如何構建一個工程化的前端庫,并結合 Github Actions,自動發(fā)布到 Github 和 NPM 的整個詳細流程。
示例
我們經(jīng)常看到像 Vue、React 這些流行的開源項目有很多配置文件,他們是干什么用的?他們的 Commit、Release 記錄都那么規(guī)范,是否基于某種約定?
廢話少說,先上圖!
上圖標紅就是相關的工程化配置,有 Linter、Tests,Github Actions 等,覆蓋開發(fā)、測試、發(fā)布的整個流程。
相關配置清單
下面我們從創(chuàng)建一個 TypeScript 項目開始,一步一步完成所有的工程化配置,并說明每個配置含義以及容易踩的坑。
初始化
為了避免兼容性問題,建議先將 node 升級到最新的長期支持版本。
首先在 Github 上創(chuàng)建一個 repo,拉下來之后通過npm init -y
初始化。然后創(chuàng)建src
文件夾,寫入index.ts
。
package.json
生成之后,我需要添加如下配置項:
"main": "index.js",
+ "type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
+ "publishConfig": {
+ "access": "public"
+ }
我們將項目定義為ESM規(guī)范,前端社區(qū)正逐漸向ESM標準遷移,從Node v12.0.0開始,只要設置了 "type": "module"
, Node 會將整個項目視為ESM規(guī)范,我們就可以直接寫裸寫import/export
。
publishConfig.access
表示當前項目發(fā)布到NPM的訪問級別,它有 restricted
和public
兩個選項,restricted
表示我們發(fā)布到NPM上的是私有包(收費),訪問級別默認為restricted
,因為我們是開源項目所以標記為public
。
配置
創(chuàng)建項目之后,我們開始安裝工程化相關的依賴,因為我們是 TypeScript 項目,所以也需要安裝 TypeScript 的依賴。
Typescript
先安裝 TypeScript,然后使用 tsc
命名生成 tsconfig.json
。
npm i typescript -D
npx tsc --init
然后我們需要添加修改 tsconfig.json
的配置項,如下:
{
"compilerOptions": {
/* Basic Options */
"baseUrl": ".", // 模塊解析根路徑,默認為 tsconfig.json 位于的目錄
"rootDir": "src", // 編譯解析根路徑,默認為 tsconfig.json 位于的目錄
"target": "ESNEXT", // 指定輸出 ECMAScript 版本,默認為 es5
"module": "ESNext", // 指定輸出模塊規(guī)范,默認為 Commonjs
"lib": ["ESNext", "DOM"], // 編譯需要包含的 API,默認為 target 的默認值
"outDir": "dist", // 編譯輸出文件夾路徑,默認為源文件同級目錄
"sourceMap": true, // 啟用 sourceMap,默認為 false
"declaration": true, // 生成 .d.ts 類型文件,默認為 false
"declarationDir": "dist/types", // .d.ts 類型文件的輸出目錄,默認為 outDir 目錄
/* Strict Type-Checking Options */
"strict": true, // 啟用所有嚴格的類型檢查選項,默認為 true
"esModuleInterop": true, // 通過為導入內(nèi)容創(chuàng)建命名空間,實現(xiàn) CommonJS 和 ES 模塊之間的互操作性,默認為 true
"skipLibCheck": true, // 跳過導入第三方 lib 聲明文件的類型檢查,默認為 true
"forceConsistentCasingInFileNames": true, // 強制在文件名中使用一致的大小寫,默認為 true
"moduleResolution": "Node", // 指定使用哪種模塊解析策略,默認為 Classic
},
"include": ["src"] // 指定需要編譯文件,默認當前目錄下除了 exclude 之外的所有.ts, .d.ts,.tsx 文件
}
更多詳細配置參考:www.typescriptlang.org/tsconfig
注意的點,如果你的項目涉及到WebWorker API
,需要添加到 lib
字段中
"lib": ["ESNext", "DOM", "WebWorker"],
然后我們將編譯后的文件路徑添加到 package.json
,并在 scripts
中添加編譯命令。
- "main": "index.js",
+ "main": "dist/index.js",
+ "types": "dist/types/index.d.ts"
"type": "module",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
- },
+ "scripts": {
+ "dev": "tsc --watch",
+ "build": "npm run clean && tsc",
+ "clean": "rm -rf dist"
+ },
"publishConfig": {
"access": "public"
}
types
配置項是指定編譯生成的類型文件,如果 compilerOptions.declarationDir
指定的是dist
,也就是源碼和 .d.ts
同級,那么types
可以省略。
驗證配置是否生效,在 index.ts
寫入
const calc = (a: number, b: number) => {
return a - b
}
console.log(calc(1024, 28))
在控制臺中執(zhí)行
npm run build && node dist/index.js
會在 dist
目錄中生成 types/index.d.ts
、index.js
、index.js.map
,并打印 996
。
Eslint & Prettier
代碼規(guī)范離不開各種 Linter
, 之所以把這兩個放在一起講,借用 Prettier
官網(wǎng)的一句話:“使用 Prettier 解決代碼格式問題,使用 linters 解決代碼質(zhì)量問題”。雖然eslint
也有格式化功能,但是prettier
的格式化功能更強大。
大部分同學編輯器都裝了prettier-vscode和eslint-vscode這兩個插件,如果你項目只有其中一個的配置,因為這兩者部分格式化的功能有差異,那么就會造成一個的問題,代碼分別被兩個插件分別格式化一次,網(wǎng)上解決prettier
+eslint
沖突的方案五花八門,甚至還有把整個rules
列表貼出來的。
那這里我們按照官方推薦,用最少的配置去解決prettier
和eslint
的集成問題。
Eslint
首先安裝 eslint
,然后利用 eslint
的命令行工具生成基本配置。
npm i eslint -D
npx eslint --init
執(zhí)行上面命令后會提示一些選項,我們依次選擇符合我們項目的配置。
注意,這里 eslint
推薦了三種社區(qū)主流的規(guī)范,Airbnb、Standard、Google,因個人愛好我選擇了不寫分號的 Standard規(guī)范。
生成的.eslintrc.cjs
文件應該長這樣
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'standard'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 12,
sourceType: 'module'
},
plugins: [
'@typescript-eslint'
],
rules: {
}
}
有些同學可能就要問了,這里為什么生成的配置文件名稱是.eslintrc.cjs
而不是.eslintrc.js
?
因為我們將項目定義為ESM
,eslit --init
會自動識別type
,并生成兼容的配置文件名稱,如果我們改回.js
結尾,再運行eslint
將會報錯。出現(xiàn)這個問題是eslint
內(nèi)部使用了require()
語法讀取配置。
同樣,這個問題也適用于其他功能的配置,比如后面會講到的Prettier
、Commitlint
等,配置文件都不能以xx.js
結尾,而要改為當前庫支持的其他配置文件格式,如:.xxrc
、.xxrc.json
、.xxrc.yml
。
驗證配置是否生效,修改index.ts
const calc = (a: number, b: number) => {
return a - b
}
- console.log(calc(1024, 28))
+ // console.log(calc(1024, 28))
在package.json
中添加lint
命令
"scripts": {
"dev": "tsc --watch",
"build": "npm run clean && tsc",
+ "lint": "eslint src --ext .js,.ts --cache --fix",
"clean": "rm -rf dist"
},
然后在控制臺執(zhí)行 lint
,eslint
將會提示 1 條錯誤信息,說明校驗生效。
npm run lint
# 1:7 error 'calc' is assigned a value but never used no-unused-vars
因為是 Typescript 項目所以我們還要添加Standard規(guī)范提供的 TypeScrip 擴展配置(其他規(guī)范同理)
安裝eslint-config-standard-with-typescript
npm i eslint-config-standard-with-typescript -D
添加修改 .eslintrc.cjs
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
- extends: ['standard']
+ extends: ['standard', 'eslint-config-standard-with-typescript'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
+ project: './tsconfig.json'
},
plugins: ['@typescript-eslint'],
rules: {}
}
驗證配置是否生效
在控制臺執(zhí)行lint
,eslint
將會提示 2 條錯誤信息,說明校驗生效。
npm run lint
# 1:7 error 'calc' is assigned a value but never used no-unused-vars
# 1:14 error Missing return type on function
Prettier
現(xiàn)在我們按照官網(wǎng)的推薦方式,把 prettier
集成到 eslint
的校驗中。
安裝 prettier
并初始化配置文件
npm i prettier -D
echo {}> .prettierrc.json
然后在.prettierrc.json
添加配置,這里只需要添加和你所選規(guī)范沖突的部分。
{
"semi": false, // 是否使用分號
"singleQuote": true, // 使用單引號代替雙引號
"trailingComma": "none" // 多行時盡可能使用逗號結尾
}
更多配置詳見:prettier.io/docs/en/opt…
安裝解決沖突需要用到的兩個依賴
- eslint-config-prettier 關閉可能與
prettier
沖突的規(guī)則 - eslint-plugin-prettier 使用
prettier
代替 eslint
格式化
npm i eslint-config-prettier eslint-plugin-prettier -D
再添加修改 .eslintrc.cjs
,如下:
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
- extends: ['standard', 'eslint-config-standard-with-typescript'],
+ extends: ['standard', 'eslint-config-standard-with-typescript', 'prettier'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
project: './tsconfig.json',
},
- plugins: ['@typescript-eslint'],
+ plugins: ['@typescript-eslint', 'prettier'],
- rules: {},
+ rules: {
+ 'prettier/prettier': 'error'
+ },
}
然后驗證配置是否生效,修改index.ts
- const calc = (a: number, b: number) => {
+ const calc = (a: number, b: number): number => {
return a - b
}
- // console.log(calc(1024, 28))
+ console.log(calc(1024, 28))
然后在控制臺執(zhí)行lint
,這里prettier
和eslint
的行為已保持一致,如果沒有報錯,那就成功了。
npm run lint
我們現(xiàn)在已經(jīng)完成了eslint
和prettier
的集成配置。和編輯器無關,也就是說無論你使用什么編輯器,有沒有安裝相關插件,都不會影響代碼校驗的效果。
Husky
因為一個項目通常是團隊合作,我們不能保證每個人在提交代碼之前執(zhí)行一遍lint
校驗,所以需要git hooks
來自動化校驗的過程,否則禁止提交。
安裝Husky
并生成.husky
文件夾
npm i husky -D
npx husky install
然后我們需要在每次執(zhí)行npm install
時自動啟用husky
如果你的npm
版本大于等于7.1.0
npm set-script prepare "husky install"
否則手動在package.json
中添加
"scripts": {
"dev": "tsc --watch",
"build": "npm run clean && tsc",
"lint": "eslint src --ext .js,.ts --cache --fix",
"clean": "rm -rf dist",
+ "prepare": "husky install"
},
然后添加一個lint
鉤子
npx husky add .husky/pre-commit "npm run lint"
相當于手動在.husky/pre-commit
文件寫入以下內(nèi)容:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint
測試鉤子是否生效,修改index.ts
const calc = (a: number, b: number): number => {
return a - b
}
- console.log(calc(1024, 28))
+ // console.log(calc(1024, 28))
然后提交一條commit
,如果配置正確將會自動執(zhí)行lint
并提示 1 條錯誤信息,commit
提交將會失敗。
git add .
git commit -m 'test husky'
# 1:7 error 'calc' is assigned a value but never used
Commitlint
為什么需要 Commitlint
,除了在后續(xù)的生成changelog
文件和語義發(fā)版中需要提取commit
中的信息,也利于其他同學分析你提交的代碼,所以我們要約定commit
的規(guī)范。
安裝 Commitlint
- @commitlint/cli Commitlint 命令行工具
- @commitlint/config-conventional 基于 Angular 的約定規(guī)范
npm i @commitlint/config-conventional @commitlint/cli -D
最后將Commitlint
添加到鉤子
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
創(chuàng)建.commitlintrc
,并寫入配置
{
"extends": [
"@commitlint/config-conventional"
]
}
注意,這里配置文件名使用的是.commitlintrc
而不是默認的.commitlintrc.js
,詳見 Eslint 章節(jié)
測試鉤子是否生效,修改index.ts
,讓代碼正確
const calc = (a: number, b: number): void => {
console.log(a - b)
}
- // calc(1024, 28)
+ calc(1024, 28)
提交一條不符合規(guī)范的commit
,提交將會失敗
git add .
git commit -m 'add eslint and commitlint'
修改為正確的commit
,提交成功!
git commit -m 'ci: add eslint and commitlint'
Angular 規(guī)范說明:
- docs:修改文檔,比如 README, CHANGELOG, CONTRIBUTE 等等
- style:不改變代碼邏輯 (僅僅修改了空格、格式縮進、逗號等等)
- refactor:重構(既不修復錯誤也不添加功能)
- build:構建系統(tǒng)或外部依賴項的更改
- chore:非 src 和 test 的修改,發(fā)布版本等
Jest
美好生活從測試覆蓋率 100% 開始。
安裝jest
,和類型聲明@types/jest
,它執(zhí)行需要ts-node
和ts-jest
這里暫時固定了ts-node
的版本為 v9.1.1
,新版的ts-node@v10.0.0
會導致jest
報錯,等待官方修復,詳見:issues
npm i jest @types/jest ts-node@9.1.1 ts-jest -D
初始化配置文件
npx jest --init
然后修改jest.config.ts
文件
// A preset that is used as a base for Jest's configuration
- // preset: undefined,
+ preset: 'ts-jest'
將測試命令添加到package.json
中。
"scripts": {
"dev": "tsc --watch",
"build": "npm run clean && tsc",
"lint": "eslint src --ext .js,.ts --cache --fix",
"clean": "rm -rf dist",
"prepare": "husky install",
+ "test": "jest"
},
創(chuàng)建測試文件夾__tests__
和測試文件__tests__/calc.spec.ts
修改index.ts
const calc = (a: number, b: number): number => {
return a - b
}
- // console.log(calc(1024, 28))
+ export default calc
然后在calc.spec.ts
中寫入測試代碼
import calc from '../src'
test('The calculation result should be 996.', () => {
expect(calc(1024, 28)).toBe(996)
})
驗證配置是否生效
在控制臺執(zhí)行test
,將會看到測試覆蓋率 100% 的結果。
npm run test
最后我們給__tests__
目錄也加上lint
校驗
修改package.json
"scripts": {
"dev": "tsc --watch",
"build": "npm run clean && tsc",
- "lint": "eslint src --ext .js,.ts --cache --fix",
+ "lint": "eslint src __tests__ --ext .js,.ts --cache --fix",
"clean": "rm -rf dist",
"prepare": "husky install",
"test": "jest"
},
這里如果我們直接執(zhí)行npm run lint
將會報錯,提示__tests__
文件夾沒有包含在tsconfig.json
的include
中,當我們添加到include
之后,輸出的dist
中就會包含測試相關的文件,這并不是我們想要的效果。
我們使用typescript-eslint
官方給出的解決方案,如下操作:
新建一個tsconfig.eslint.json
文件,寫入以下內(nèi)容:
{
"extends": "./tsconfig.json",
"include": ["**/*.ts", "**/*.js"]
}
在.eslintrc.cjs
中修改
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
- project: './tsconfig.json'
+ project: './tsconfig.eslint.json'
},
然后驗證配置是否生效,直接提交我們添加的測試文件,能正確提交說明配置成功。
git add .
git commit -m 'test: add unit test'
Github Actions
我們通過Github Actions
實現(xiàn)代碼合并或推送到主分支,dependabot
機器人升級依賴等動作,會自動觸發(fā)測試和發(fā)布版本等一系列流程。
在項目根目錄創(chuàng)建.github/workflows
文件夾,然后在里面新建ci.yml
文件和cd.yml
文件
在ci.yml
文件中寫入:
name: CI
on:
push:
branches:
- '**'
pull_request:
branches:
- '**'
jobs:
linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
- run: npm ci
- run: npm run lint
tests:
needs: linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
- run: npm ci
- run: npm run test
上面配置大概意思就是,監(jiān)聽所有分支的push
和pull_request
動作,自動執(zhí)行linter
和tests
任務。
GithubActions 更多用法參考:github.com/features/ac…
然后推送代碼,驗證配置是否生效
git add .
git commit -m 'ci: use github actions'
git push
此時打開當前項目的 Github 頁面,然后點擊頂部 Actions 菜單就會看到正在進行的兩個任務,一個將會成功(測試),一個將會失?。òl(fā)布)。
上面只是實現(xiàn)了代碼自動測試流程,下面實現(xiàn)自動發(fā)布的流程。
在此之前需要到NPM網(wǎng)站上注冊一個賬號(已有可忽略),并創(chuàng)建一個package
。
然后創(chuàng)建GH_TOKEN
和NPM_TOKEN
(注意,不要在代碼中包含任何的 TOKEN 信息):
- 如何創(chuàng)建 GITHUB\_TOKEN(創(chuàng)建時勾選 repo 和 workflow 權限)
- 如何創(chuàng)建 NPM\_TOKEN(創(chuàng)建時選中 Automation 權限)
將創(chuàng)建好的兩個TOKEN
添加到項目的 Actions secrets 中:
Github 項目首頁 -> 頂部 Settings 菜單 -> 側邊欄 Secrets
然后修改package.json
中的“name”
,“name”
就是你在NPM上創(chuàng)建的package
的名稱。
在cd.yml
文件中寫入:
name: CD
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
# https://github.com/semantic-release/git/issues/209
- run: npm ci --ignore-scripts
- run: npm run build
- run: npx semantic-release
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
由于“黑命貴”,Github 已將新項目的默認分支名稱更改為 “main”
,詳見:issues, 為了方便,后面統(tǒng)一稱為 主分支
所以如果你的主分支名稱是“main”
,上面的branches
需要修改為:
on:
push:
branches:
- main
pull_request:
branches:
- main
然后安裝語義發(fā)版依賴,需要用到semantic-release
和它的插件:
- semantic-release:語義發(fā)版核心庫
- @semantic-release/changelog:用于自動生成 changelog.md
- @semantic-release/git:用于將發(fā)布時產(chǎn)生的更改提交回遠程倉庫
npm i semantic-release @semantic-release/changelog @semantic-release/git -D
在項目根目錄新建配置文件.releaserc
并寫入:
{
"branches": ["master"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/github",
"@semantic-release/npm",
"@semantic-release/git"
]
}
這里同樣,如果你的主分支名稱是“main”
,上面的branches
需要修改為:
"branches": ["+([0-9])?(.{+([0-9]),x}).x", "main"],
最后新建分支 develop 分支并提交工作內(nèi)容。
git checkout -b develop
git add .
git commit -m 'feat: complete the CI/CD workflow'
git push --set-upstream origin develop
git push
然后將 develop 分支合并到 主分支,并提交,注意:這個提交會觸發(fā)測試并 發(fā)布版本 (自動創(chuàng)建tag
和changelog
)
git checkout master
git merge develop
git push
完成上面操作之后,打開 Github 項目主頁 和 NPM 項目主頁 可以看到一個 Release 的更新記錄。
最后切回到 develop 分支,創(chuàng)建一個自動更新依賴的workflow
。
在.github
文件夾中創(chuàng)建dependabot.yml
文件,并寫入內(nèi)容:
version: 2
updates:
# Enable version updates for npm
- package-ecosystem: 'npm'
# Look for `package.json` and `lock` files in the `root` directory
directory: '/'
# Check the npm registry for updates every day (weekdays)
schedule:
interval: 'weekly'
提交并查看 workflows 是否全部通過,再合并到 主分支 并提交,這個提交不會觸發(fā)版本發(fā)布。
git pull origin master
git add .
git commit -m 'ci: add dependabot'
git push
git checkout master
git merge develop
git push
觸發(fā)版本發(fā)布需要兩個條件:
- 只有當
push
和pull_request
到 主分支 上才會觸發(fā)版本發(fā)布 - 只有
commit
前綴為feat
、fix
、perf
才會發(fā)布,否則跳過。
更多發(fā)布規(guī)則,詳見:github.com/semantic-re…
SemanticRelease 使用方式,詳見:semantic-release.gitbook.io
如果你能正確配置上面所有步驟,并成功發(fā)布,那么恭喜你!你擁有了一個完全自動化的項目,它擁有:自動依賴更新、測試、發(fā)布,和自動生成版本信息等功能。
完整的項目示例:@resreq/event-hub
結語
本文未涉及到:組件庫、Monorepo、Jenkins CI 等配置,但能覆蓋絕大部前端項目 CI/CD 流程。
有些地方講得比較細,甚至有些啰嗦,但還是希望能幫助到大家!撒花!??????
作者:molvqingtai
https:///post/6971812117993226248