lerna 還是 pnpm + changesets?monorepo 工具核心就看這三個功能
monorepo 是多個包在同一個項目中管理的方式,是很流行的項目組織形式。
主流的開源包基本都是用 monorepo 的形式管理的。
為什么用 monorepo 也很容易理解:
比如 babel 分為了 @babel/core、@babel/cli、@babel/parser、@babel/traverse、@babel/generator 等一系列包。
如果每個包單獨一個倉庫,那就有十多個 git 倉庫,這些 git 倉庫每個都要單獨來一套編譯、lint、發(fā)包等工程化的工具和配置,重復十多次。
工程化部分重復還不是最大的問題,最大的問題還是這三個:
- 一個項目依賴了一個本地還在開發(fā)的包,我們會通過 npm link 的方式把這個包 link 到全局,然后再 link 到那個項目的 node_modules 下。
npm link 的文檔是這么寫的:

就是把代碼 link 到全局再 link 到另一個項目,這樣只要這個包的代碼改了,那個項目就可以直接使用最新的代碼。
如果只是一個包的話,npm link 還是方便的。但現在有十幾個包了,這樣來十多次就很麻煩了。
-
需要在每個包里執(zhí)行命令,現在也是要分別進入到不同的目錄下來執(zhí)行十多次。最關鍵的是有一些包需要根據依賴關系來確定執(zhí)行命令的先后順序。
-
版本更新的時候,要手動更新所有包的版本,如果這個包更新了,那么依賴它的包也要發(fā)個新版本才行。
這也是件麻煩的事情。
因為這三個問題:npm link 比較麻煩、執(zhí)行命令比較麻煩、版本更新比較麻煩,所以就有了對 monorepo 的項目組織形式和工具的需求。
比如主流的 monorepo 工具 lerna,它描述自己解決的三個大問題也是這個:

也就是說,把理清了這三個點,就算是掌握了 monorepo 工具的關鍵了。
我們分別來看一下:
npm link 的流程實際上是這樣的:

npm 包先 link 到全局,再 link 到另一個項目的 node_modules。
而 monorepo 工具都是這樣做的:

比如一個 monorepo 項目下有 a、b、c 三個包,那么 monorepo 工具會把它們 link 到父級目錄的 node_modules。
node 查找模塊的時候,一層層往上查找,就都能找到彼此了,就完成了 a、b、c 的相互依賴。
比如用 lerna 的 demo 項目試試:
git?clone?https://github.com/lerna/getting-started-example.git
下載下來是這樣的結構:

執(zhí)行 npm install,在根目錄的 node_modules 下就會安裝很多依賴。
包括我們剛說的 link 到根 node_modules 里的包:



這個箭頭就是軟鏈接文件的意思。
底層都是系統提供的 ln -s 的命令。
比如我執(zhí)行
ln?-s?package.json?package2.json
那就是創(chuàng)建一個 package2.json 的軟連接文件,內容和 package.json 一樣。
這倆其實是一個文件,一個改了另一個也就改了:

原理都是軟連接,只不過 npm link 的那個和 monorepo 這個封裝的有點區(qū)別。
這種功能本來是 lerna 先實現的,它提供了 lerna bootstrap 來完成這種 link:

只不過后來 npm、yarn、pnpm 都內置了這個功能,叫做 workspace。就不再需要 lerna 這個 bootstrap 的命令了。
直接在 package.json 里配置 workspace 的目錄:

然后 npm install,就會完成這些 package 的 link。
而包與包之間的依賴,workspace 會處理,本地開發(fā)的時候只需要寫 * 就好,發(fā)布這個包的時候才會替換成具體的版本。

這里用的是 npm workspace:

它所解決的問題正如我們分析的:

在 npm install 的時候自動 link。
yarn workspace 也是一樣的方式:

pnpm 有所不同,是放在一個 yaml 文件里的:

此外,yarn 和 pnpm 支持 workspace 協議,需要把依賴改為這樣的形式:

這樣查找依賴就是從 workspace 里查找,而不是從 npm 倉庫了。

總之,不管是 npm workspace、yarn workspace 還是 pnpm workspace,都能達到在 npm install 的時候自動 link 的目的。
回過頭來再來看 monorepo 工具的第二大功能:執(zhí)行命令
在剛才的 demo 項目下執(zhí)行
lerna?run?build
輸出是這樣的:

lerna 會按照依賴的拓撲順序來執(zhí)行命令,并且合并輸出執(zhí)行結果。
比如 remixapp 依賴了 header 和 footer 包,所以先在 footer 和 header 下執(zhí)行,再在 remixapp 下執(zhí)行。
當然,npm workspace、yarn workspace、pnpm workspace 也是提供了多包執(zhí)行命令的支持的。
npm workspace 執(zhí)行剛才的命令是這樣的:
npm?exec?--workspaces?--?npm?run?build
可以簡寫為:
npm?exec?-ws?--?npm?run?build

也可以單獨執(zhí)行某個包下執(zhí)行:
npm?exec?--workspace?header?--workspace?footer?--?npm?run?build
可以簡寫為:
npm?exec?-w?header?-w?footer??--?npm?run?build
只不過不支持拓撲順序。
yarn workspace 可以執(zhí)行:
yarn?workspaces?run?build

但也同樣不支持拓撲順序。
我們再來試試 pnpm workspace。
npm workspace 和 yarn workspace 只要在 package.json 里聲明 workspaces 就可以。
但 pnpm workspace 要聲明在 pnpm-workspaces.yaml 里:

pnpm 在 workspace 執(zhí)行命令是這樣的:
pnpm?exec?-r?pnpm?run?build
-r 是遞歸的意思:

關鍵是 pnpm 是支持選擇拓撲排序,然后再執(zhí)行命令的:

有時候命令有執(zhí)行先后順序的要求的時候就很有用了。
總之,npm、yarn、pnpm 都和 lerna 一樣支持 workspace 下命令的執(zhí)行,而且 pnpm 和 lerna 都是支持拓撲排序的。
再來看最后一個 monorepo 工具的功能:版本管理和發(fā)布。
有個工具叫做 changesets 是專門做這個的,我們看下它能做啥就好了。
執(zhí)行 changeset init:
npx?changeset?init
執(zhí)行之后會多這樣一個目錄:

然后添加一個 changeset。
什么叫 changeset 呢?
就是一次改動的集合,可能一次改動會涉及到多個 package,多個包的版本更新,這合起來叫做一個 changeset。
我們執(zhí)行 add 命令添加一個 changeset:
npx?changeset?add
會讓你選一個項目:

哪個是 major 版本更新,哪個是 minor 版本更新,剩下的就是 pacth 版本更新。

1.2.3 ?這里面 1 就是 major 版本、2 是 minor 版本、3 是 patch 版本。
之后會讓你輸入這次變更的信息:

然后你就會發(fā)現在 .changeset 下多了一個文件記錄著這次變更的信息:

然后你可以執(zhí)行 version 命令來生成最終的 CHANGELOG.md 還有更新版本信息:
npx?changeset?version
之后那些臨時的 changeset 文件就消失了:

更改的包下都多了 CHANGELOG.md 文件:


并且都更新了版本號:


而且 remixapp 這個包雖然沒有更新,但是因為依賴的包更新了,所以也更新了一個 patch 版本:


這就是 changeset 的作用。
如果沒有這個工具呢?
你要自己一個個去更新版本號,而且你還得分析依賴關系,知道這個包被哪些包用到了,再去更改那些依賴這個包的包的版本。
就很麻煩。
這就是 monorepo 工具的版本更新功能。
更新完版本自然是要 publish 到 npm 倉庫的。
執(zhí)行 changeset publish 命令就可以,并且還會自動打 tag:

如果你不想用 changeset publish 來發(fā)布,想用 pnpm publish,那也可以用 changeset 來打標簽:

npx?changeset?tag

這就是 monorepo 工具的版本更新和發(fā)布的功能。
lerna 是自己實現的一套,但是用 pnpm workspace + changeset 也完全可以做到。
回過頭來看下這三個功能:

不同包的自動 link,npm workspace、yarn workspace、pnpm workspace 都可以做到,而 lerna bootstrap 也廢棄了,改成基于 workspace。
執(zhí)行命令這個也是都可以,只不過 lerna 和 pnpm workspace 都支持拓撲順序執(zhí)行命令。
版本更新和發(fā)布這個用 changeset 也能實現,用 lerna 的也可以。
整體看下來,似乎沒啥必要用 lerna 了,用 pnpm workspace + changesets 就完全能覆蓋這些需求。
那用 lerna 的意義在哪呢?
雖然功能上沒啥差別,但性能還是有差別的。
lerna 還支持命令執(zhí)行緩存,再就是可以分布式執(zhí)行任務。
執(zhí)行 lerna add-caching 來添加緩存的支持:

指定 build 和 test 命令是可以緩存的,輸出目錄是 dist。
那當再次執(zhí)行的時候,如果沒有變動,lerna 就會直接輸出上次的結果,不會重新執(zhí)行命令。
下面分別是第一次和第二次執(zhí)行:

至于分布式執(zhí)行任務這個,是 nx cloud 的功能,貌似是可以在多臺機器上跑任務。
所以綜合看下來,lerna 在功能上和 pnpm workspace + changesets 沒啥打的區(qū)別,但是在性能上更好點。
如果項目比較大,用 lerna 還是不錯的,否則用 pnpm workspace + changesets 也完全夠用了。
總結
monorepo 是在一個項目中管理多個包的項目組織形式。
它能解決很多問題:工程化配置重復、link 麻煩、執(zhí)行命令麻煩、版本更新麻煩等。
lerna 在文檔中說它解決了 3 個 monorepo 最大的問題:
- 不同包的自動 link
- 命令的按順序執(zhí)行
- 版本更新、自動 tag、發(fā)布

這三個問題是 monorepo 的核心問題。
第一個問題用 pmpm workspace、npm workspace、yarn workspace 都可以解決。
第二個問題用 pnpm exec 也可以保證按照拓撲順序執(zhí)行,或者用 npm exec 或者 yarn exec 也可以。
第三個問題用 changesets 就可以做到。
lerna 在功能上和 pnpm workspace + changesets 并沒有大的差別,主要是它做了命令緩存、分布式執(zhí)行任務等性能的優(yōu)化。
總之,monorepo 工具的核心就是解決這三個問題。
