這篇文章旨在記錄有關 Monorepo 的全部技術點,以及基於 typescript + yarn workspaces + lerna 進行多個 node package 開發管理的最佳實踐。node
NOTE: 本文針對 library 開發的場景進行描述,其中的實踐並不必定知足全部的 monorepo 的場景,文章也不會全面的介紹各類 monorepo 的應用場景下的方案,可是會在部分章節必要的時候作一些簡單的說明。react
正如咱們所熟知的,npm 和 yarn 都是原生的 node packge 的管理工具,他們在包管理方面具備相互兼容的功能,好比,依賴管理(dependency management),包發佈(publish),安裝(install)等。git
npm 是隨着 node 的發佈一塊兒發佈的,屬於 node 官方維護的包管理工具。github
yarn 是 Facebook 爲了改善前期 npm 在大型項目中依賴安裝性能而獨立開發維護的包管理工具,在依賴管理方面,yarn 除了大幅提高了安裝性能在,在分佈發佈系統中依賴一致性方面也進行了保證(yarn.lock), 從而解決了分佈發佈中因爲依賴版本不一致而致使應用表現不一致的問題。web
npm5 以後,npm 慢慢補齊了在性能與依賴一致性方面的差距,因此若是僅僅是單獨的做爲項目的依賴管理來講,使用 yarn 或者 npm 都是能夠的。typescript
yarn 與 npm 的最大區別是,yarn 原生引入了 workspaces
概念,讓使用 yarn 進行依賴管理的項目具有了原生的 mono-repo 的能力,而 npm 原生至今也沒有對等的功能,而要在不適用 yarn 的狀況下實現 workspaces 的能力,咱們不得不借助 lerna
來實現。接下來的章節,咱們會具體介紹 mono-repo
& yarn workspaces
& lerna
。express
與 mono-repo
相對應的一個概念的 multi-repo
. 這兩個概念實際上是同一個問題的不一樣解決方案。npm
在咱們進行多個項目開發的過程當中,總會存在一些能夠複用的邏輯,這時候,咱們會經過可複用邏輯的相關性,把若干可複用邏輯封裝到同一個 package 中,而後咱們可能還會指望根據 package 的相關性,將若干 package 聚合在一塊兒進行管理(版本控制,git / svn),在進行 package 聚合管理的時候,就出現了兩種不一樣的管理方案:json
multi-repo: 最開始的時候,不少開發者會爲每一個獨立的 package 建立一個 git 倉庫,多個 package 就會存在多個相互獨立的 git 倉庫,這就是所謂的 multi-repo
.bootstrap
mono-repo(多包倉庫): multi-repo 最大的問題在於同一個開發者同時維護多個 package, 且這些 package 之間還存在相互依賴的時候,管理起來會至關的麻煩,不只須要檢出多個倉庫,並且須要在某個 package 更新以後,手動的去更新他的依賴方,並且不一樣 package 的相同依賴須要安裝多份,因此爲了解決上述問題,提升開發效率,mono-repo
誕生了,mono-repo 會在同一個 git 倉庫中管理一組具備相關性的 package, 不一樣 package 之間的共同依賴會被提高,並且 package 之間的相互依賴也會在其中一個 package 更新後自動更新其依賴(也能夠不自動升級,下面的章節進行介紹),併發布新的版本。
因此,從上一章節的介紹中,咱們能夠了解到使用 mono-repo 的硬核指標就是:就有相關性的一組 package 的管理
,咱們能夠嘗試問本身如下幾個問題來肯定咱們是否須要使用 mono-repo:
正在維護的這一組 packages 具備相關性麼?好比:都是一些工具類的庫,或者是是一個系列工具的不一樣部件的封裝(react),或者是一個體繫系統下的不一樣插件(bable)
這些 packages 之間是否相互依賴?好比有一個 core package, 被多個其餘 package 所依賴,而且須要在 core 版本發生變化後,升級依賴方的版本
這些 packages 之間是否有比較多得共同依賴?(具備侷限性,適當參考)
從這一章節開始,咱們就須要動手來實踐,到底怎麼建立 mono-repo, 以及集成一些最佳實踐,讓咱們的項目更加標準與高效。
建立 mono-repo 業內其實有多種方案,鑑於筆者精力,本文只介紹其中比較流行且比較活躍的兩種方案 yarn workspaces
和 lerna
.
咱們先簡單的對比下這兩種方案:
相同點
均可以獨立的建立 mono-repo
packages 之間的相互依賴(如下以 local dependency 來講明)均可以使用 syslink 來連接到本地,也能夠是直接安裝已經發布到指定 registry 中的版本
packages 的共同依賴均可以提高到根目錄,避免重複安裝,lerna 默認狀況下不會提高,須要在 bootstrap 的時候顯示指定 --hoist
參數
不一樣點
yarn workspaces 不具具有 local dependency 之間語義版本的自動管理,包的統一發布等,須要進入到各個包內進行手動版本更新或者發佈
lerna 默認使用 npm 做爲包管理工具,可是經過 npmClient
來改成 yarn, 可是若是僅僅是使用 yarn 做爲依賴管理工具,yarn 和 npm 區別不大
yarn2 對 workspaces 有了不少提高,也提供了對 local dependency 版本更新時的管理,workspaces 發佈等新功能,感興趣的能夠深刻研究,後續筆者實踐後,也會整理一些使用心得出來,官方文檔
從以上的對比中,咱們能夠發現,yarn workspaces 進行 mono-repo 的管理時,其實方案是不完備的,好比進行相似 react 或者 babel 這種有明確的版本管理的 library 類型的 mono-repo 管理只使用 yarn workspaces 是不夠的,可是對於一個 node 開發的小型 web 項目,該項目由 client 端,server 端以及一個本身開發的組件庫組成,對於這種類型的項目,沒有很強的版本管理負擔(僅組件庫須要),且各個子項目又具有必定的獨立性,爲了提升開發場景下的構建效率,僅使用 yarn workspaces 來建立 mono-repo 是很是輕量的選擇。
因此進行 library 的多包倉庫的管理時,推薦的方案是 yarn workspaces + lerna 組合,沒錯,這兩個方案是能夠完美結合的,自己 yarn 的 workspaces 就是一個偏底層的能力,而 lerna 自己也僅是利用 npm 或 yarn 提供的能力來工做的,因此咱們能夠切換 larna 底層的多包能力爲 yarn workspaces 而同時使用 leran 額外的命令來進行包的版本管理和發佈。
完整的項目能夠參考 github.com/dancon/mono… 。
筆者假設各位看官已經安裝 node 以及全局安裝 yarn 1.x 或者 2.x ,若是沒有,能夠參考如下文檔自行安裝:
在 github 建立一個遠程項目,遠程地址爲
檢出遠程倉庫, 並進行項目初始化,這裏使用 monorepo
進行演示
# 檢出代碼庫 git clone <remote-repo url> # 進入項目根目錄 cd monorepo # 初始化爲 npm 項目 yarn init --yes 複製代碼
<remote-repo url>
替換爲你建立的 github 倉庫地址
至此項目只有一個 package.json
文件
package.json
, 具體能夠參考 classic.yarnpkg.com/en/docs/wor…{
"private": true, // 避免根項目被髮布出去
"workspaces": [
"packages/*"
] // 暫時填寫爲 packages/* 指定 workspace 位置爲 packages
}
複製代碼
# 確保在 monorepo 目錄下,建立 packages 目錄,做爲 yarn workspace 或者子項目 mkdir packages 複製代碼
至此,項目已是在 yarn workspaces 模式下工做。
# 安裝 lerna, 注意,使用 yarn add -W 將 lerna 安裝到根目錄 yarn add -W -D lerna # lerna 初始化, 使用 independent 模式 yarn lerna init --independent 複製代碼
yarn add
的時候,若是沒有指定-W
參數會報錯自行建立 .gitignore 文件
生成 lerna.json 配置文件,內容以下:
{
"packages": [
"packages/*"
],
"version": "independent"
}
複製代碼
添加如下配置,讓 lerna 切換使用 yarn 進行依賴管理,而且使用 yarn workspaces
{
// 此項配置爲可選,在部分 IDE(VS Code 支持)中會讀這個 json schema 進行配置項的智能提示
"$schema": "http://json.schemastore.org/lerna",
"packages": [
"packages/*"
],
"version": "independent",
"npmClient": "yarn", // 告知 lerna 使用 yarn 做爲包管理工具
"useWorkspaces": true // 使用 yarn workspaces
}
複製代碼
至此,已經支持使用 yarn workspaces + lerna 進行管理了。
# 安裝依賴 yarn add -W -D typescript # 在更目錄下初始化 yarn tsc --init 複製代碼
修改根目錄下的 tsconfig.json 爲以下內容
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"allowJs": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
// 排除一些不須要處理的文件,在後面的章節中會詳細介紹其中的一些文件以及文件夾
"exclude": [
"node_modules",
"packages/**/node_modules",
"packages/**/lib",
"packages/**/test",
"packages/**/*.test.ts",
"packages/**/jest.config.js"
]
}
複製代碼
# 接下來建立兩個 package 分別爲 core 和 pkg1 cd packages mkdir core mkdir pkg1 複製代碼
每一個 package 的目錄結構以下:
. ├── src │ └── index.ts # 源碼目錄 ├── jest.config.js # jest 配置 yarn jest --init ├── package.json # yarn init --yes └── tsconfig.json # package 的 ts 配置 複製代碼
jest.config.js
的具體配置在下面的章節中重點說明
package.json
着重說明如下配置:
{
"name": "@scope/core",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"publishConfig": { // 若是咱們的包名包含在一個特殊的 @scope 下,爲了能讓包正常發佈,必須添加該項配置
"access": "public"
},
"devDependencies": {
"@types/node": "^13.13.5"
},
"scripts": {
"test": "jest",
"build": "rm -rf lib && tsc" // 添加構建腳本
}
}
複製代碼
tsconfig.json
配置內容以下:
{
"extends": "../../tsconfig.json", // 繼承根目錄下的 tsconfig.json 配置
"compilerOptions": {
"rootDir": "src",
"outDir": "lib"
}
}
複製代碼
接下來重點說明 mono-repo 中項目依賴管理,涉及如下幾個主題:
包的安裝
公共包安裝
爲全部 package 安裝包
安裝本地依賴
項目結合 lerna + workspaces, 因此包的安裝方式有兩種:
# 經過 yarn workspace 安裝 yarn add -W <package-name> yarn workspace <workspace-name> add <package-name> # <workspace-name> 爲對應包 package.json 中的 name # 經過 lerna 安裝 lerna add <package-name> # 爲全部 packages/* 安裝依賴 lerna add <package-name> --scope <workspace-name> # 效果同 yarn workspace 複製代碼
NOTE:
yarn add -W 效果和 lerna add 不指定
--scope
是不等價的yarn add -W 是安裝通用依賴,更新根目錄下的 package.json
lerna add 不指定 --scope 是爲全部的 package 安裝依賴,更新全部 packages/**/package.json
若是咱們須要爲本身的包添加一個本地依賴,好比爲 @scope/pkg1 添加 @scope/core 做爲依賴,咱們既可使用 yarn workspace 或者 lerna add --scope 來安裝
yarn workspace @scope/pkg1 add @scope/core@1.0.0 # 等價於 lerna add @scope/core@1.0.0 --scope @scope/pkg1 複製代碼
以上方式指定了與本地 @scope/core 相同或者兼容的版本,因此 @scope/core 實際上是經過 syslinks 的方式指向本地倉庫
若是咱們不指定版本,則 yarn 或者 lerna 會從 npm registry 中拉取在線包,這時候,咱們項目中引用的就是 node_modules 中的包
如下圖爲 local dependency,指定相同或者兼容的版本號
不指定版本
版本管理與發佈
yarn workspaces 並無提供統一的版本管理與發佈,若是不是 lerna, 咱們也能夠單獨使用 yarn version,yarn publish 來進行管理,可是相互之間的依賴都須要人工管理,低效而易錯。
使用 lerna 利用 commit convention 經過 git message 來自動的進行版本管理,而且在包版本變動後,自動更新依賴方的版本,而且能夠各個包自動生成 CHANGELOG.md, 爲了使用 lerna 的這些能力,咱們只需進行如下配置:
lerna.json
{
"packages": [
"packages/*"
],
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"publish": {
"conventionalCommits": true, // 啓用 commit convention 自動進行版本管理
"registry": "https://registry.npmjs.org/", // 指定 registry
"message": "chore: publish" // lerna 自動提交時的 commit message 前綴
},
"version": {
"message": "chore: publish"
}
}
}
複製代碼
第一次發佈的時候,無需經過 commit message 來自動升級版本,這時候,咱們可使用 from-package
lerna publish from-package
複製代碼
根目錄下的 package.json
添加以下 scripts
命令
{
"scripts": {
"release": "lerna publish", // 添加 release script
"build": "lerna exec --stream yarn build",
"test": "lerna exec -- yarn test --passWithNoTests",
"lint": "eslint --ext js,jsx,ts,tsx packages --fix",
"gen": "plop"
}
}
複製代碼
而後,後續的發佈中經過 yarn 來執行
# 第一次發佈 yarn release from-package # 以後的發佈 yarn release 複製代碼
關於 commit convention 能夠參考: Conventional Commits
mono-repo 中,關於 commit message 須要單獨說明的是,在每次提交的時候,最好爲每一個包的修改指定對應的 scope, 好比:
git commit -m 'feat(core): add a new feature'
咱們也能夠利用 commitlint 來規範咱們的提交信息,後面的章節會詳細介紹其使用。
接下來咱們着重介紹一些經常使用的命令:
yarn workspaces info
展現當前項目中全部 workspace 的依賴關係執行結果以下:
yarn workspaces v1.21.1 { "@pandolajs-test/core": { "location": "packages/core", "workspaceDependencies": [], "mismatchedWorkspaceDependencies": [] }, "@pandolajs-test/pkg1": { "location": "packages/pkg1", "workspaceDependencies": [ "@pandolajs-test/core" ], "mismatchedWorkspaceDependencies": [] } } ✨ Done in 0.04s. 複製代碼
yarn workspaces run <command>
在全部 workspace 中執行 <command>
lerna 中的等價命令:
lerna run <command>
yarn workspace <workspace> <command>
在指定的 <workspace>
中執行 yarn 的命令官方文檔:GitHub - lerna/lerna: A tool for managing JavaScript projects with multiple packages.
經常使用的全局參數
經常使用的命令
lerna bootstrap
lerna add
lerna publish
lerna run
lerna exec
每一個 workspace 中的 tsconfig.json 繼承根目錄下的 tsconfig.json 提高通用配置。
yarn add -W -D typescript
複製代碼
yarn tsc --init
複製代碼
每一個 workspace 中使用本身的 jest 配置
yarn add -W -D jest @types/jest ts-jest
複製代碼
yarn jest --init
複製代碼
{
"preset": "ts-jest",
}
複製代碼
更多查看文檔:www.robertcooper.me/using-eslin…
yarn add -W -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier eslint-config-standard eslint-config-standard-with-typescript eslint-plugin-import eslint-plugin-jest eslint-plugin-node eslint-plugin-prettier eslint-plugin-promise eslint-plugin-standard
複製代碼
.eslintrc.json
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
tsconfigRootDir: __dirname,
project: [
'./packages/**/tsconfig.json'
]
},
env: {
node: true
},
plugins: [
'@typescript-eslint',
'jest',
'prettier'
],
extends: [
'eslint:recommended',
'standard-with-typescript',
'prettier/@typescript-eslint',
'plugin:jest/recommended',
'plugin:prettier/recommended'
],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/triple-slash-reference': 'off',
'@typescript-eslint/strict-boolean-expressions': 'off'
}
}
複製代碼
.vscode/setting.json
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
複製代碼
.prettierrc.json
{
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "none"
}
複製代碼
可根據本身口味進行修改
yarn add -W -D husky lint-staged
複製代碼
.lintstagedrc
{
"packages/**/*.{js,ts,jsx,tsx}": [
"eslint --fix"
]
}
複製代碼
.huskrc.json
{
"hooks": {
// commitlint 配置的 git hook
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
// 注意這裏 --since HEAD 的做用 參考:https://github.com/lerna/lerna/tree/master/core/filter-options#--since-ref
"pre-commit": "lerna exec --concurrency 1 --stream lint-staged --since HEAD"
}
}
複製代碼
官方文檔:commitlint - Lint commit messages
yarn add -W -D @commitlint/cli @commitlint/config-conventional @commitlint/config-lerna-scopes
複製代碼
.commitlintrc.json
{
"extends": [
"@commitlint/config-conventional",
"@commitlint/config-lerna-scopes"
]
}
複製代碼
官方文檔:GitHub - plopjs/plop: Consistency Made Simple
具體參考代碼庫實現:GitHub - dancon/monorepo
yarn add -W -D plop
複製代碼
plopfile.js
module.exports = function(plop) { // ... } 複製代碼
經常使用方法
setGenerator
setHelper
項目根目錄配置如下 script
{
"scripts": {
"gen": "plop"
}
}
複製代碼
而後執行 yarn gen [template-name]
就能夠建立你配置的模板,或者不指定 [template-name]
會列出全部你配置的模板。更多使用方式能夠參考官方文檔。