Quick Start:用 pnpm 管理 Monorepo 專案
1、什麼是 Monorepo ?
Monorepo
是管理專案程式碼的方式之一,指在一個大的專案倉庫(repo)中 管理多個模組/包(package),這種型別的專案大都在專案根目錄下有一個packages資料夾,分多個專案管理。大概結構如下:
├── packages
| ├── pkg1
| | ├── package。json
| ├── pkg2
| | ├── package。json
├── package。json
目前很很多大型專案採用這樣的結構,比如:
Babel
、
vue3
和
vite
等。
Monorepo 的好處在哪裡嘞?
統一管理
。比如微前端專案,多個子應用可以放在同一個
monorepo
中方便管理;後端用
node。js
的專案放在
monorepo
中也可以使用同一套技術棧管理。在CI/CD等流水線過程中,方便統一迭代或升級版本,也方便做通用化的配置,適用到多個子專案當中。
依賴提升
。如果多個專案都依賴了諸如
react
、
vue
或
TypeScript
等常用庫,那可以透過
lerna
或者
yarn workspace
將依賴提升到最外層,多個子模組/包複用同一個依賴,減小專案體積。
2、什麼是 pnpm
非常推薦先閱讀這兩篇文章瞭解 pnpm 的優勢:
zoomdong:Pnpm: 最先進的包管理工具
熊貓MrPanda:【譯】pnpm vs Yarn: monorepo node_modules
在開始之前,可以帶著下面幾個問題閱讀:
什麼是”幻影依賴“和”NPM 分身“?
什麼是”硬連結“和”軟連結“?
pnpm 是如何進行包的快取?
monorepo的場景下,pnpm 如何將依賴提升?
3、快速開始
先全域性安裝一個 pnpm,然後透過:
pnpm init
建立一個專案——在本例中,我們實現一個前後端分離、共同管理的 demo。
首先,我們需要理解 pnpm 中的工作區,在 pnpm 中可以透過建立並配置 pnpm-workspace。yaml 設定 workspace:
packages:
- ‘packages/**’
上面這段配置的意思就是透過 glob 語法將
packages
下的所有資料夾都當做一個package,新增到 monorepo 中進行管理。
接下來,我們就按照配置,建立
packages
資料夾,並在其目錄中建立三個子專案,分別是:web端、node端和工具類。
├── packages
│ ├── server
│ ├── tools
│ └── web
3.1、全域性依賴
我們知道不管是一個 web 專案還是一個 node 專案,它都是基於同一種語言編寫,所以我們可以只安裝一次 TypeScript,供三個專案使用,這就體現出了 monorepo 的優勢。
類似於
TypeScript
或
lodash
這樣通用的依賴,我們通常可以把他們安裝到根目錄,即使用下面這條命令:
pnpm install typescript -D -W
這裡的
-D
指令大家都很熟悉,就是把依賴作為
devDependencies
安裝;而
-W
就是把依賴安裝到根目錄的
node_modules
當中,目錄結構大致如下:
├── node_modules
│ ├── @types
│ └── typescript -> 。pnpm/typescript@4。4。4/node_modules/typescript
├── package。json
├── packages
│ ├── server
│ ├── tools
│ └── web
├── pnpm-lock。yaml
└── pnpm-workspace。yaml
雖然packages下的專案都沒有安裝 ts,但是倘若在專案中使用到,就會透過依賴遞迴查詢的原則逐級往上尋找,自然會找到 monorepo 中根目錄的依賴。
3.2、區域性依賴
對於某些依賴,可能僅存在於某幾個 package 中,我們就可以單獨為他們安裝,當然,可以透過
cd packges/xxx
後,執行
pnpm install xxx
,但這樣重複操作多次未免有些麻煩,pnpm 提供了一個快捷指令——filter。
比如我們只在 web 應用中用到
vue
,那就可以為它單獨安裝。首先要拿到它的 package name,這個是我們在子專案中自定義的,通常是”@名稱空間+包名@“的方式,比如
@vite/xx
或
@babel/xx
,在本例中,我們都以@panda開頭。那麼命令如下:
pnpm install vue -r ——filter @panda/web
在
packages/web/package。json
中,我們可以看到:
“dependencies”: {
“vue”: “^3。0。4”
}
同時,該子專案的
node_modules
中也可以看到
vue
被添加了進來。
3.3、link 機制
在 monorepo 中,我們往往需要 package 間的引用,比如本例中的
@panda/tools
就會被
@panda/server
和
@panda/web
依賴。
我們可以使用類似於 3。2 中的方法安裝:
pnpm i @panda/tools -r ——filter @panda/server @panda/web
在執行前,我們可以新增一個函式,比如:
export const getNow = () => (new Date()。getTime());
匯出一個函式後,我們執行剛才的安裝指令,這時在專案中會以
workspace
的版本體現。我們檢視web專案中的 package。json:
“dependencies”: {
“@panda/tools”: “workspace:^1。0。0”, // 透過 workspace 為本地引用
“vue”: “^3。0。4”
},
我們再進一步檢視web專案下的 node_modules,可以發現:
透過軟連結引用了我們的tools package,我們就可以使用了正常的引入和呼叫了!
import { getNow } from ‘@panda/tools’;
getNow();
使用VSCode自帶的快速跳轉,我們可以直接跳轉到tools原始碼定義檔案中,這樣就巧妙地運用了 monorepo 的優勢。
這時你會有一個疑問:當這樣的工具包被髮布到平臺後,如何識別其中的
workspace
呢?
實際上,當執行了
pnpm publish
後,會把基於的workspace的依賴變成外部依賴,如:
//
before
“dependencies”
:
{
“@panda/tools”
:
“workspace:^1。0。0”
}
,
//
after
“dependencies”
:
{
“@panda/tools”
:
“^1。0。0”
}
,
解決了開發環境和生產環境對依賴的問題。
3.4、啟動與開發
一個 monorepo 往往是一個整體的專案,所以我們需要同時執行多個指令,在 pnpm 中,可以透過
-C
進行配置:
“scripts”: {
“start”: “pnpm -C 。/packages/server start:server & pnpm -C 。/packages/web dev”,
}
這條命令的含義就是同時執行服務端和前端程式碼。
而
start:server
和
dev
都是我們在子專案的
scripts
中自己配置的。
如果經過了 git 合碼後,專案的依賴變化比較大,可以配置一條 clean 指令:
“scripts”: {
“clean”: “rm -rf node_modules **/*/node_modules”,
}
這樣根目錄和子應用下的
node_modules
都會被刪除。再次執行
pnpm install
也會同時為根目錄和所有 package 安裝需要的依賴。
在實際開發中,我們往往會糾結什麼樣的程式碼應該放在業務專案中,什麼樣的程式碼要單獨抽離為一個 package。舉幾個很簡單的例子:
對於一個大型的應用我們通常需要定製一套元件庫,那麼我們就可以把元件庫最基礎的部分抽離成 package,在業務程式碼中進行二次業務封裝,這樣在其他的專案也能複用。
如果一個專案包含 PC 和 H5,我們不妨把這兩類專案拆分成不同的 package。
如果你在打造一個工具,而它具備了 parser、render等能力,那也可以將不同的能力解耦到不同的 package 當中。
4、總結
總的來說,pnpm 管理 monorepo 已經是當下非常熱門且推薦的做法,已經有很多專案為我們親身實踐過:
如果你對工程化比較感興趣,或者 npm/yarn 給你的專案管理造成了困擾,都可以嘗試下使用 pnpm 來最佳化你的專案。