使用 pnpm 建立 Vue 的 Monorepo
最近想整理 GitHub 上面一些 repo,像是活動或課程作業的相關產出,
應該都可以集合成一包大專案?
剛好也想研究 Monorepo,所以稍微爬文一下自己公司用的 Lerna 和其他工具,
發現 pnpm 本身就提供類似的管理模式。
環境建置
建立資料夾後先進行初始化:
pnpm init
新增 pnpm-workspace.yaml
:
packages:
- "apps/*"
把之前的專案都搬到 apps
這個資料夾底下作為 Monorepo 的子專案,
搬完之後需要修改子專案的 package.json
,將 name
加上 @apps/
這個前綴:
{
"name": "@apps/week-1", // 從 "week-1" 改為 "@apps/week-1"
"version": "0.0.0",
"private": true,
"type": "module",
// 略...
}
pnpm 會將這個有這個前綴的資料夾,對應到剛剛在 pnpm-workspace.yaml
的設定,
將其識別為一個工作區(workspace)。
在根目錄執行 pnpm install
來測試子專案 package.json
紀錄的依賴項目能不能正常安裝,
確認子專案有出現 node_modules
之後,就可以接著運行:
pnpm --filter @apps/week-1 dev
這個指令的意思是,用參數 --filter
指向工作區 @apps/week-1
,
並且運行這個工作區 package.json
腳本中的 dev
,
跟 切換到這個目錄底下後執行 npm run dev
是一樣的意思。
如果能正常啟動的話,專案初步的搬遷已經成功了!
腳本
可以將剛剛的指令加到根目錄的腳本,之後就不用再打一長串的指令,
或是手動切換到子專案的資料夾來啟動專案:
{
"scripts": {
"week1:dev": "pnpm --filter @apps/week-1 dev",
"week1:build": "pnpm --filter @apps/week-1 build",
"week1:preview": "pnpm --filter @apps/week-1 preview"
}
}
在根目錄執行剛剛自訂的腳本來啟動子專案:
pnpm week1:dev
共用設定
在根目錄安裝的依賴項目可以作用在全部的工作區,
所以像 husky、commitlint、Prettier、ESLint 等等適用多個專案的套件,
都可以搬到根目錄做安裝與設定,讓子專案開發時可以直接共用。
這次搬的都是 六角學院 2024 Vue 前端新手營
的作業,
所以包含 Vue 本體都可以直接搬到根目錄:
{
"name": "2024-vue-camp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
},
// 把依賴項目搬到根目錄
"dependencies": {
"vue": "^3.4.29"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.8.0",
"@tsconfig/node20": "^20.1.4",
"@types/node": "^20.14.5",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"npm-run-all2": "^6.2.0",
"prettier": "^3.2.5",
"typescript": "~5.4.0",
"vite": "^5.3.1",
"vue-tsc": "^2.0.21"
},
"keywords": [],
"author": "",
"license": "ISC"
}
新增共用依賴
未來要在根目錄安裝新的套件時,需要帶上 -W
這個參數,
來標註這是要在根目錄安裝的共用依賴項目,例如:
pnpm add axios -W
ESLint
Monorepo 如果是基於微前端的架構,子專案不一定全部都是同一套前端框架,
框架之間也有不同的最佳設定,所以建議子專案的設定可以留著:
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');
module.exports = {
root: false,
extends: ['../../.eslintrc.cjs'],
rules: {
// 各種規則
}
};
將 root
設為 false
就能解除解析範圍,讓 ESLint 可以向上層目錄解析,
extends
填上根目錄的 .eslintrc.cjs
,
這樣就能繼承根目錄的設定,並自行加入、重寫規則。
可以在子專案寫一個不符合規則的寫法,看看會不會收到提示:
// 宣告一個沒有被用到的變數
const testRef = ref();
看到明顯的黃波浪,確認是 ESLint 的嚴厲斥責警告就算成功了:
'testRef' is assigned a value but never used. eslint(@typescript-eslint/no-unused-vars)
TypeScript
透過 Vite 建立的 Vue 3 專案會有 3 個 TypeScript 設定檔:
tsconfig.app.json
tsconfig.node.json
tsconfig.json
,將上面兩個設定檔加入參照(reference)
這些設定檔預設會從官方提供的現成設定 @vue/tsconfig
和 @tsconfig/node20
繼承,
上面有提過微前端架構中可能包含其他不同框架的專案,
因次需要的 TypeScript 設定也不同,會牽涉到建構工具、編碼輸出的問題,
要共用 TypeScript 設定的話,我認為只放入撰寫規則(lint)相關的設定會比較安全。
在根目錄新增共用設定 tsconfig.base.json
:
{
"compilerOptions": {
// 只放入 lint 相關的規則
"strict": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
修改子專案的 tsconfig.app.json
和 tsconfig.node.json
的 extends
,
使它們包含根目錄的共用設定:
{
"extends": ["@vue/tsconfig/tsconfig.dom.json", "../../tsconfig.base.json"],
// 以下略
}
在子專案寫一個沒有指定參數型別的函式,測試是否有成功繼承到根目錄的設定:
function greet(name) {
return 'Hello ' + name;
}
這個規則會違反 noImplicitAny
(不允許隱式的 any),所以會收到提示:
Parameter 'name' implicitly has an 'any' type.ts(7006)
不過目前無法知道這個提示會生效的原因,
是基於 @vue/tsconfig
或 @tsconfig/node20
還是根目錄的 tsconfig.base.json
,
而 extends
會照陣列順序來解析,所以順序比較後面的 "../../tsconfig.base.json"
,
如果有同名屬性的規則,應該 要覆蓋過去,
因此可以把 noImplicitAny
設為 false
來測試是不是有成功覆蓋。
確認設為 false
後如果沒有紅波浪的提示,TypeScript 的部分就算是設定完成了,
當然測完要記得改回 true
!
共用元件庫
同時經營多個產品線的話,通常會有一套共用的設計系統延伸到各個專案來維持品牌風格。
而不論開發上是純手刻,或是基於其他現成的元件庫做再封裝,
都可以做成一個共用庫,達成更好的開發一致性,以後也更好配合 design token 的改動。
先在根目錄建立資料夾 packages
,並在裡面建立一個 Vue 3 專案,
package.json
中 Vue 相關的依賴項目都可以移除,因為根目錄已經有紀錄,
將 name
改為帶有 @packages/
的前綴,也要重新設定專案進入點:
{
"name": "@packages/shared-ui", // 加入前綴
"private": true,
"version": "0.0.0",
"type": "module",
"main": "src/main.ts" // 設定為 main.ts
}
在 pnpm-workspace.yaml
加入剛剛建立的 packages
資料夾路徑:
packages:
- "apps/*"
- "packages/*"