深入解析 Yarn Workspaces:高效管理 monorepo 的必備技能
本文適用於
Yarn 1.x的版本
關於 Yarn Workspaces
Yarn Workspaces 是 Yarn 提供的一個功能,專門用來管理一個 monorepo 中的多個子專案(通常稱為 workspace)。這些 workspace 可以互相依賴,並且共享相同的依賴,從而提升專案的開發效率,減少重複安裝依賴的開銷。
單一 Workspace 的定義與特性
每個 workspace 本質上是一個包含自己 package.json 文件的獨立 npm 專案,可以相互依賴,也可以作為獨立 npm 套件發佈到 npm 等套件管理平台。
- 獨立運作:每個 workspace 都像是一個獨立的 package,具有自己的 package.json 文件,包含自己專屬的依賴和設定。這允許子專案像是普通的 npm package 一樣被單獨發佈。
- 相互依賴:子專案可以相互依賴,不需重複下載依賴。
Yarn Workspaces 的定義與特性
Yarn Workspaces 允許在一個 monorepo 中同時管理多個子專案,它帶來的好處包括以下幾點:
- 多專案共享依賴:不同子專案可以共用相同的
node_modules目錄,這避免了重複安裝同一個依賴,從而節省磁碟空間和時間。 - 集中化管理:所 有子專案的依賴可以統一管理在根目錄的
yarn.lock中,避免了每個子專案都獨立管理依賴的混亂局面。 - 跨專案依賴:子專案之間可以相互依賴,Yarn 會自動解決這些依賴關係,無需手動處理。這讓子專案可以輕鬆引用其他子專案,無需將其發佈到 npm registry。
- 提升安裝效率:Yarn Workspaces 可以自動將通用的依賴 「提升」(hoist) 到 monorepo 根目錄,避免子專案之間重複安裝相同的依賴,顯著加快安裝速度,特別是在大型專案中效果尤為明顯。
注意事項
使用 Yarn Workspaces 進行開發時,必須特別注意依賴的聲明:
- 依賴未聲明的風險:如果某個子專案 packageA 在開發過程中使用了另一個子專案 packageB 的依賴 dependencyB,但忘記在 packageA 的 package.json 中聲明該依賴,可能在開發和測試階段一切運行正常。這是因為 Workspaces 會共享根目錄的 node_modules,所以 dependencyB 仍能被找到。但當 packageA 發佈到 npm 等平台後,因為依賴聲明不完整,使用者將無法正常運行這個套件,因為 dependencyB 不會被自動安裝。
- 無自動檢查:目前 Yarn Workspaces 沒有自動檢測未聲明依賴的機制,因此開發者需要格外注意,確保每個子專案的 package.json 文件中包含了所有必要的依賴聲明。
配置 Yarn Workspaces
在這一章,我們將介紹如何在 monorepo 中正確配置 Yarn Workspaces,並探討一些在配置過程中常見的問題與解決方法。
根目錄配置
首先,在 monorepo 的根目錄下,我們需要定義 Workspaces 的範圍和設定。根目錄的 package.json 應包含以下兩個重要屬性:
-
private:在根目錄的 package.json 中,必須將 private 設為 true,這是因為 monorepo 的根目錄通常不會作為一個單獨的 npm 套件發佈,而只是用來管理 Workspaces 和共享依賴。如果未將 private 設為 true,Yarn 會警告你這個專案是可發佈的。
-
workspaces:這是一個字串數組,用來定義哪些目錄是 Workspaces。Yarn 支持使用 glob patterns 來匹配特定路徑。例如,
"packages/*"會告訴 Yarn Workspaces 所有位於 packages/ 資料夾下的子目錄都是 Workspaces。package.json{
"private": true,
"workspaces": ["packages/*"]
}
個別 workspace 配置
在每個子專案(即 Workspace)中,我們需要配置自己的 package.json 文件。以下是配置時需要注意的幾個要點:
- private:在大多數情況下,個別 Workspace 不需要設置
private: true,因為這些子專案可能會單獨發佈為 npm package。 - package name:Yarn Workspaces 依據每個 package.json 中的 name 屬性來識別專案,而不是使用目錄名稱。因此,在下達
yarn workspace <workspace_name> <command>時,必須使用 package.json 中的 name 屬性,而不是該 Workspace 的目錄名稱。 - 依賴聲明:正如在第一章提到的,在 Workspaces 中開 發時,務必確認每個子專案的 package.json 文件包含了所有必要的依賴聲明,避免發佈時出現未聲明依賴的問題。
特別注意:相對路徑的問題
設定 workspaces 時,使用相對路徑(如 ./package/*)的話有可能會遇到依賴解析的問題。由於 Yarn Workspaces 預期接收 glob patterns 來匹配路徑,可能會導致 Yarn 無法正確解析依賴,這可能會導致安裝過程中發生錯誤。可以參考 Dots。
以下是我遇過的錯誤:
例子 1:有內部依賴時的解析錯誤
如果某個 workspace(如 web)依賴於另一個內部 package,當使用 yarn workspace install web 時,可能會發生依賴解析錯誤,導致安裝失敗。這是因為 Yarn 無法正確處理這樣的相對路徑。
例子 2:無內部依賴時的單一安裝
在某些情況下,如果 web 沒有依賴於其他內部 package,安裝過程可能會成功,但這只會安裝該 workspace 自己的依賴。並且,安裝完成後,會在該 workspace 的路徑下生成一個新的 yarn.lock 檔案,而不是將依賴集中在根目錄的 yarn.lock 中,這違反了 Workspaces 的集中管理原則。
Yarn Workspace 相關指令
yarn workspaces info [--json]
這個指令會列出當前專案中的所有 Workspaces,並顯示每個 Workspace 的相關資訊。
yarn workspaces info
yarn workspaces run <command>
此指令可以讓你在所有 Workspaces 中執行一個指定的命令。例如,你可以一次性在所有子專案中執行 build 或 test 指令。
yarn workspaces run build
yarn workspace <workspace_name> <command>
這個指令用來對某個特定的 Workspace 執行 Yarn 命令,相當於進入該 Workspace 的目錄中執行普通的 Yarn 指令。
e.g.
yarn workspace web build
yarn add/remove <package...> [--ignore-workspace-root-check/-W]
在默認情況下,Yarn 會防止你在根目錄中安裝依賴,因為它通常只是作為 Workspaces 的管理層,但有時我們可能需要在根目錄安裝一些工具或全局依賴,這時可以使用 -W 標誌來忽略這個限制。
e.g.
yarn add typescript -W
以 Jest 為例解釋 Yarn Workspaces 的依賴解析方法
Jest 是一個使用 Yarn Workspaces 管理其套件的範例。Jest 的專案結構典型於一個 JavaScript monorepo 專案,根目錄有一個 package.json,並且 packages/ 資料夾內包含多個子專案,每個子專案也有自己的 package.json。
根目錄的 package.json 主要負責管理專案的整體依賴與設定,而子專案 jest-matcher-utils 和 jest-diff 則在 packages/ 目錄中。這些子專案的目標是可以被單獨發佈到 npm 上。根目錄通常不會被發佈,因此會將其設置為 private。
範例根目錄 package.json:
{
"private": true,
"name": "jest",
"devDependencies": {
"chalk": "^2.0.1"
},
"workspaces": ["packages/*"]
}
其中兩個子專案的範例:
-
jest-matcher-utils:package.json{
"name": "jest-matcher-utils",
"version": "20.0.3",
"dependencies": {
"chalk": "^1.1.3",
"pretty-format": "^20.0.3"
}
} -
jest-diff(依賴於jest-matcher-utils):package.json{
"name": "jest-diff",
"version": "20.0.3",
"dependencies": {
"chalk": "^1.1.3",
"diff": "^3.2.0",
"jest-matcher-utils": "^20.0.3",
"pretty-format": "^20.0.3"
}
}
Lerna 的傳統方式
如果使用 Lerna 等工具,通常會先為每個子專案分別執行 yarn install,每個子專案都會有自己獨立的 node_modules。例如,jest-diff 專案會有自己的 jest-matcher-utils 符號連結(symlink),這會導致重複安裝許多依賴。
目錄結構如下:
jest/
| ---- node_modules/
| -------- chalk/
| ---- packages/
| -------- jest-matcher-utils/
| ------------ node_modules/
| ---------------- chalk/
| ---------------- pretty-format/
| -------- jest-diff/
| ------------ node_modules/
| ---------------- chalk/
| ---------------- diff/
| ---------------- jest-matcher-utils/ (symlink)
| ---------------- pretty-format/