前端pnpm?workspace架構(gòu)實例詳解
前言
一篇幫你搞懂 pnpm workspace 的實戰(zhàn)向教程,從「為啥要用」到「怎么配」全給你捋清楚;每個知識點都會講清是什么、為什么、怎么用、注意啥,方便你系統(tǒng)學(xué)習(xí)、隨時查閱、直接落地。
一、先聊聊:我們到底遇到了啥問題?
做前端久了,多包、monorepo、組件庫聯(lián)調(diào)這些事一多,就會踩到一堆具體又磨人的坑。下面把這些痛點拆開說:具體表現(xiàn) → 典型場景 → 對你有啥影響。搞清楚這些,后面再看 pnpm workspace 解決啥就一目了然。
1.1node_modules膨脹,磁盤和時間都遭殃
具體表現(xiàn):用 npm 搞 monorepo 時,根目錄一個 node_modules,每個子包再來一個;或者多個獨立項目各自一份。每個 node_modules 里,npm 會做扁平化:把子依賴提升到頂層,同一份包可能在不同項目的 node_modules 里各存一份,重復(fù)拷貝。
典型場景:比如你有一個 monorepo,里面 5 個 app、3 個共享庫,都用 React、lodash、一堆 Babel/Webpack 相關(guān)包。單項目 node_modules 可能就 400~600MB,monorepo 里再乘上包數(shù)量、加上提升帶來的重復(fù),輕松破 2GB。npm install 第一次全量裝要幾分鐘,以后每次 npm ci 或清緩存重裝,體感也很慢。
影響:占磁盤、拉代碼慢、CI 緩存大、流水線耗時增加;本機多開幾個項目,node_modules 動不動幾十 GB。
1.2 依賴版本亂成一鍋粥:幽靈依賴與沖突
幽靈依賴的定義:某個包沒有在你自己的 package.json 的 dependencies / devDependencies 里聲明,你卻能在代碼里 import 或 require 到它。常見原因就是 npm 的扁平化:你裝了 A,A 依賴 B,B 被提升到了項目根 node_modules,于是你的代碼「意外」地能直接用 B。
典型場景:你習(xí)慣性 import _ from 'lodash',但從沒在 package.json 里加過 lodash,因為它是某個依賴的子依賴,被提升上來了。后來你升級了那個依賴,人家不再依賴 lodash,或者換了版本,你這邊沒改一行業(yè)務(wù)代碼就報錯:找不到 lodash。更坑的是「本地能跑、CI 掛」:本地可能還有別的路徑殘留或緩存,CI 干凈安裝就炸。同理,刪了某個你以為沒用的依賴,結(jié)果別的地方一直隱式用著,一刪就掛。
版本沖突:A 包要 React 18,B 包要 React 17,扁平化之后只能滿足一邊,另一邊可能用了「不對」的版本,運行時才暴露問題,調(diào)試成本很高。
1.3 本地包聯(lián)調(diào)賊麻煩:npm link的坑
典型場景:你維護一個業(yè)務(wù)組件庫,要在另一個前端項目里聯(lián)調(diào)。通常做法是 npm link:在組件庫目錄 npm link,在業(yè)務(wù)項目里 npm link your-components。但經(jīng)常會遇到:
- 雙實例問題:React、Vue 等對「單實例」有要求,link 過去可能出現(xiàn)兩個版本,引發(fā)詭異 bug。
- bin 路徑:某些 CLI 或工具通過
node_modules/.bin找可執(zhí)行文件,link 后路徑解析不對,跑不起來。 - 不同 Node 版本 / 環(huán)境: link 的是「當時本機」的構(gòu)建結(jié)果,換機器、換 Node、改點配置,行為可能不一致。
總之,改一下組件庫就要反復(fù) link、unlink、重裝,體驗很差,也容易忘步驟導(dǎo)致聯(lián)調(diào)結(jié)果不可靠。
1.4 CI 又慢又占空間
典型場景:每次 CI 全量 npm install,沒有跨項目或跨 job 的 store 復(fù)用;緩存 key 設(shè)計不當(例如只按 package.json 不按 lockfile),導(dǎo)致緩存命中率低,每次都幾乎全量裝。加上前面說的 node_modules 巨大,流水線耗時長、占用空間大,體驗和成本都不好。
上面這些,本質(zhì)都可以歸為兩類問題:一是多包怎么組織、怎么一起開發(fā)、怎么發(fā)布(項目結(jié)構(gòu) + 工作流);二是依賴怎么存、怎么解析、怎么隔離(存儲與解析策略)。pnpm 的 workspace 就是在這兩方面同時發(fā)力的方案之一:多包管理 + 更合理的依賴存儲與解析。下面先把你可能最關(guān)心的——pnpm 底層是怎么干的——講清楚,再回頭看 workspace 具體解決了啥。
二、pnpm 底層原理:為啥能省空間、裝得快、依賴還干凈?
很多人只記住結(jié)論:「pnpm 省磁盤、快、沒幽靈依賴」,但不知道它到底咋做到的。這一節(jié)把存儲模型和 node_modules 結(jié)構(gòu)說透,你后面看配置、看優(yōu)缺點都會更有數(shù)。
2.1 全局 store:content-addressable + 硬鏈接
pnpm 有一個全局 store,所有安裝過的包都會先放進這里,再通過硬鏈接掛到各個項目的 node_modules 里。
存哪兒:
- Linux:默認
~/.local/share/pnpm/store - macOS:默認
~/Library/pnpm/store - Windows:默認
%LOCALAPPDATA%\pnpm\store(即C:\Users\<你>\AppData\Local\pnpm\store)
若設(shè)置了$XDG_DATA_HOME,Linux/macOS 會改用$XDG_DATA_HOME/pnpm/store。可通過.npmrc的store-dir覆蓋,例如store-dir=D:\pnpm-store。
- Linux:默認
content-addressable(按內(nèi)容尋址):
包在 store 里按內(nèi)容哈希存,同一版本、同一份包只存一份。不同項目、不同 monorepo 子包,只要依賴的版本相同,都用這一份,去重、跨項目復(fù)用。硬鏈接:
硬鏈接可以理解為「同一份文件的多個路徑入口」,改一處全體生效,但不額外占磁盤。pnpm 從 store 把包硬鏈接到項目里的node_modules/.pnpm/...,所以看起來每個項目都有一份,實際磁盤只存 store 里那一份。
和復(fù)制的區(qū)別:不占多余空間。和符號鏈接的區(qū)別:符號鏈接是「指向另一個路徑」的小文件,硬鏈接是文件系統(tǒng)層面的多路徑同一 inode,更省空間、也更穩(wěn)定(刪掉一個鏈接不會影響 store 里的那份,只要還有別的鏈接在)。
結(jié)果:同 monorepo、同樣依賴,用 pnpm 時磁盤占用往往只有 npm 的一半左右(常見 benchmark 結(jié)論),二次安裝時大量命中 store,pnpm install 明顯更快。
2.2node_modules的真實結(jié)構(gòu):非扁平 + 嚴格依賴
npm 會把依賴扁平化提升到頂層,所以你能「意外」用到子依賴;pnpm 不這么做,結(jié)構(gòu)是非扁平的。
目錄結(jié)構(gòu)示意(精簡版):
項目根目錄的
node_modules/:- 只放你直接聲明的依賴(
dependencies/devDependencies里的包)。 - 這些「包名」多數(shù)是符號鏈接,指向
node_modules/.pnpm/<pkg>@<version>/node_modules/<pkg>。
- 只放你直接聲明的依賴(
node_modules/.pnpm/:- 里面才是實際內(nèi)容(或鏈到 store)。
- 每個
package@version一個目錄,且每個包有自己的node_modules,里面只裝它自己的依賴。 - 子依賴不會提升到項目根
node_modules,所以你沒法在業(yè)務(wù)代碼里require('某個未聲明的子依賴')。
嚴格依賴就是這樣實現(xiàn)的:
只有在 package.json 里顯式聲明的包,才會出現(xiàn)在你項目的 node_modules 頂層(或子包自己的 node_modules 里)。未聲明的包根本不在你可訪問的路徑下,require / import 會直接報錯,從根上杜絕幽靈依賴。
有些老舊工具會假設(shè)「所有依賴都在根 node_modules 扁平展開」,在 pnpm 默認結(jié)構(gòu)下會找不到包。這時可以用 public-hoist-pattern 或 node-linker=hoisted 做有限提升,相當于在「兼容舊工具」和「嚴格依賴」之間做權(quán)衡;提升多了,幽靈依賴風(fēng)險又回來了,所以能窄就窄。
2.3 workspace 包怎么被鏈接進來?
當你在 package.json 里寫 "@my/ui": "workspace:*" 時,pnpm 會:
- 在
pnpm-workspace.yaml定義的目錄里找到對應(yīng)包(如packages/ui); - 把該包所在目錄(源碼目錄)鏈接到
node_modules里對應(yīng)位置,不拷貝、不先打包。
所以,你改 packages/ui 的源碼,消費方(例如 apps/web)立即可見,不用 npm link,也沒有雙實例、路徑錯亂那些破事。這就是 workspace 協(xié)議 帶來的「本地包即源碼」的聯(lián)調(diào)體驗。
2.4 和 npm / Yarn 的存儲對比(簡要)
- npm:扁平化 + 每項目各自拷貝,多項目多份;易幽靈依賴;安裝速度、磁盤占用都一般。
- Yarn:經(jīng)典模式類似 npm;Plug’n’Play 可選,但生態(tài)兼容性要看工具。
- pnpm:全局 store + 硬鏈接 + 非扁平
node_modules,省空間、安裝快、默認嚴格依賴。
差異主要在存儲與解析策略,而不是「有沒有 workspace」這個概念。
三、pnpm workspace 解決了什么問題?(深化版)
有了第二節(jié)的原理打底,這里直接說 workspace 在「多包管理」場景下,具體幫你解決了啥;每個點都往「能用、能查」上靠。
3.1 磁盤與安裝
- store + 硬鏈接:全 workspace 共享同一 store,同版本依賴只存一份;子包、apps 裝依賴都是鏈過去,磁盤占用明顯低于 npm 同規(guī)模 monorepo(約一半量級的說法很常見)。
- workspace 包不占 store:像
@my/utils、@my/ui這種本地包,pnpm 只做鏈接到源碼目錄,不往 store 里塞,也不拷貝,改完即生效。 - 安裝速度:
pnpm install在 monorepo 里通常比npm install快不少,尤其二次安裝、CI 命中 store 時。
3.2 依賴隔離與一致性
幽靈依賴:
pnpm 默認嚴格依賴,未聲明就不能用。你刻意避免隱式依賴,配合 code review,能從根本上消滅「刪了某依賴突然掛」「本地有 CI 沒有」這類問題。
若必須兼容舊工具,再考慮public-hoist-pattern有限提升,并清楚這會帶來隱性依賴風(fēng)險。版本統(tǒng)一:
- 單一 lockfile:整個 workspace 只有一個
pnpm-lock.yaml在根目錄,所有子包、所有環(huán)境的依賴解析都以它為準,版本全倉庫一致,復(fù)現(xiàn)性高。 - catalog(pnpm 9+):在
pnpm-workspace.yaml里定義catalog,給常用依賴約定版本(如react: ^18.3.1),子包用catalog:引用,升級時只改一處,避免各包各自為政。 - overrides:根
package.json里可配pnpm.overrides,強制某依賴在全 workspace 解析成指定版本,適合解決傳遞依賴沖突、安全修復(fù)等。
- 單一 lockfile:整個 workspace 只有一個
3.3 多包協(xié)作與發(fā)布
- 統(tǒng)一裝依賴、統(tǒng)一跑腳本:根目錄一次
pnpm install,所有 workspace 包依賴都裝好;用pnpm -r run build、pnpm --filter ...批量或定向跑腳本,配合根package.json的scripts,協(xié)作流程清晰。 - 按需發(fā)布:
pnpm publish -r可遞歸發(fā)布,結(jié)合--filter只發(fā)布改動的包;配合 changesets 做 version + changelog + publish,適合多包獨立發(fā)版。 - 權(quán)限與發(fā)包:可以按包名、按目錄做 access 控制,和現(xiàn)有 npm registry 權(quán)限模型配合使用。
四、pnpm workspace 架構(gòu)長什么樣?
4.1 目錄樹與職責(zé)
下面是一個常見的 pnpm workspace 根目錄結(jié)構(gòu),以及各部分的職責(zé)。
項目根目錄
├── pnpm-workspace.yaml # 聲明哪些目錄是 workspace 包(唯一、僅根目錄)
├── package.json # 根包:公共 devDependencies、批量腳本、overrides 等
├── pnpm-lock.yaml # 全 workspace 唯一 lockfile,所有人、CI 共用一個
├── .npmrc # 可選:store-dir、node-linker、hoist 等
├── packages/
│ ├── ui/ # 如:組件庫
│ ├── utils/ # 公共工具
│ ├── config-eslint/ # 共享 ESLint 配置
│ └── ...
└── apps/
├── web/ # 前端應(yīng)用
├── docs/ # 文檔站
└── ...
根 package.json:
- 放全倉庫共用的 devDependencies(如 TypeScript、ESLint、Vitest、Prettier)。
- 定義
scripts,用pnpm -r、--filter批量或定向執(zhí)行子包的 build、dev、test。 - 根包通常
"private": true,不發(fā)布;可加packageManager、pnpm.overrides等。
pnpm-workspace.yaml:
- 唯一,只能放在根目錄。
- 通過
packages數(shù)組聲明哪些目錄算 workspace 包(如packages/*、apps/*),只有這些才能被workspace:*引用。 - pnpm 官方推薦用這個文件,而不是
package.json的workspaces字段。
pnpm-lock.yaml:
- 全 workspace 共用一個,在根目錄。
- 鎖死所有依賴(含 workspace 包解析結(jié)果),保證任意環(huán)境
pnpm install結(jié)果一致。
packages/*:
- 一般放可復(fù)用庫:組件庫、工具庫、配置包等。
- 各自有
package.json,通過workspace:*相互依賴或被apps/*依賴。
apps/*:
- 一般放應(yīng)用:前端項目、文檔站、Demo 等。
- 依賴
packages/*時用workspace:*,改庫即生效。
有的項目還會加 tools/* 放腳本、CLI 等,本質(zhì)上一樣:在 pnpm-workspace.yaml 里寫上對應(yīng) glob 即可。
4.2 命名與布局約定
- packages:可復(fù)用、可能發(fā)布到 npm 的庫;apps:入口應(yīng)用、不發(fā)布或只發(fā)構(gòu)建產(chǎn)物。
- 何時拆
apps?當你明確有「多個應(yīng)用 + 共享 packages」時,拆開更清晰;只有一兩個 app 時,全放packages也沒問題,按團隊習(xí)慣來。 - 依賴方向:
- 子包互相依賴、app 依賴子包,一律用
workspace:*。 - 禁止循環(huán)依賴(A 依賴 B,B 又依賴 A),否則安裝、構(gòu)建都會出問題。
- 根包通常不作為業(yè)務(wù)依賴,只提供腳本和公共 devDependencies。
- 子包互相依賴、app 依賴子包,一律用
4.3 workspace 包的解析與匹配機制
靠啥匹配?
pnpm 解析 workspace:* 時,只看 package.json 里的 name,和目錄名、路徑都無關(guān)。你寫 "@my/ui": "workspace:*",pnpm 就會在 pnpm-workspace.yaml 聲明的那堆目錄里,找 name 為 @my/ui 的包;找到就把該包所在目錄鏈進 node_modules,找不到就直接報錯,不會悄悄去 npm 裝一個。
具體流程:
- 讀
pnpm-workspace.yaml,收集所有匹配packages的目錄(如packages/*、apps/*); - 逐個讀這些目錄下的
package.json,拿到name,建成一張 「name → 目錄」 的映射; - 解析依賴時,遇到
workspace:*、workspace:^等,用依賴里的包名去這張表里查; - 查到了 → 用該包所在目錄做鏈接目標,鏈到當前包的
node_modules里; - 查不到 → 報錯(例如
ERR_PNPM_NO_MATCHING_PACKAGE),安裝中止。
所以:包名必須和依賴里寫的一模一樣。packages/ui 的 name 要是 @my/ui,別的地方才能 "@my/ui": "workspace:*";寫成 @my/components 就匹配不上。
幾種寫法:
- workspace:*:匹配 workspace 里同名包的任意版本,并鏈到源碼目錄;開發(fā)聯(lián)調(diào)最常用。
- workspace:^、workspace:~:按 semver 匹配 workspace 內(nèi)版本;發(fā)布時會被替換成具體版本號(如
1.0.0),發(fā)布出去的package.json里不會還帶著workspace:。 - workspace:../packages/utils(相對路徑):明確指向某個目錄,不靠
name匹配;適合臨時調(diào)試或路徑敏感的布局。
別名:
可以用 "別名": "workspace:真實包名@*" 把 workspace 包掛到另一個名字下,例如 "react": "workspace:my-react@*"。發(fā)布時同樣會替換成普通依賴形式。
找不到會怎樣?
只會報錯,不會回退到 npm 裝。這樣你才能確定:用的一定是本地的 workspace 包,沒有誤用遠端的。
4.4 依賴圖與構(gòu)建順序
workspace 里包和包之間的依賴關(guān)系,會形成一張有向圖:誰依賴誰,一目了然。pnpm 跑 pnpm -r run build 這類遞歸命令時,默認按這張圖的拓撲順序執(zhí)行:先跑被依賴的,再跑依賴別人的,避免「還沒 build 完就被別人 require」的坑。
拓撲順序是啥?
簡單說:若 A 依賴 B,則一定先執(zhí)行 B 的 build,再執(zhí)行 A 的 build。例如 utils → ui → web,順序就是 utils → ui → web。同一層之間(比如多個 app 互不依賴)誰先誰后不保證,但層級不會亂。
默認行為:
- pnpm -r run build(以及
pnpm -r run <script>):按依賴圖拓撲排序,再依次執(zhí)行;沒有-r時則只跑當前包。 - pnpm -r --parallel run build:不管順序,所有包并行跑;跑
dev、test時常用--parallel,但 build 一般要保證順序,所以慎用--parallel。
怎么知道誰依賴誰?
- 看各包
package.json的dependencies/devDependencies里對 workspace 包、普通包的引用; - 用
pnpm why <pkg>看某包被誰依賴;pnpm list -r看全 workspace 的依賴樹(注意list默認不按拓撲序,按字母序); - 有些團隊會接 Turborepo、Nx 等,用它們畫依賴圖、跑拓撲并行 build(同一層并行,層與層之間仍按依賴順序)。
循環(huán)依賴:
若出現(xiàn) A → B → C → A,依賴圖成環(huán),拓撲排序搞不定,pnpm 會報錯;安裝、-r 執(zhí)行都可能掛。所以必須保證 workspace 內(nèi)無環(huán),設(shè)計時就要避免「包互相依賴」。
4.5 安裝與打包:workspace 如何工作
安裝(pnpm install)
在根目錄執(zhí)行 pnpm install 時,大致會做這幾步:
- 讀 workspace 定義:解析
pnpm-workspace.yaml,得到所有 workspace 包目錄(如packages/*、apps/*)。 - 收集包信息:逐個讀這些目錄下的
package.json,建 name → 目錄 映射,并算出整棵依賴樹(含對 npm 包的依賴)。 - 解析
workspace:*:遇到workspace:*等,按 4.3 的規(guī)則匹配到本地包目錄,不從 registry 拉包。 - 鏈接 workspace 包:把匹配到的本地包目錄鏈到各包的
node_modules里(符號鏈接或 junction),不拷貝、不往 store 塞;改源碼立即生效。 - 裝外部依賴:對 npm 上的包,按平時那套來:store + 硬鏈接,裝到
node_modules/.pnpm等位置。 - 寫 lockfile:把所有依賴(含
workspace:*的解析結(jié)果)寫入根目錄的pnpm-lock.yaml。
所以:workspace 包只做鏈接,不占 store;占磁盤、耗時的主要是外部依賴,而它們?nèi)宰?store 復(fù)用。
打包 / 構(gòu)建(pnpm -r run build)
構(gòu)建不改依賴安裝方式,只是按依賴圖順序跑各包的 build 腳本:
- 算依賴圖:根據(jù)各包
package.json的依賴關(guān)系,得到有向圖。 - 拓撲排序:排出「被依賴的在前、依賴別人的在后」的順序(pnpm 內(nèi)部用類似
graph-sequencer的方式處理)。 - 依次執(zhí)行:按該順序?qū)γ總€ workspace 包執(zhí)行
pnpm run build(或你配的其它 script)。 - 若某包沒有
build腳本,pnpm 會報錯或跳過該包,視配置而定。
因此:先裝依賴,再構(gòu)建;裝依賴保證 node_modules 里 workspace 包、npm 包都就位,構(gòu)建則按依賴順序生成各包產(chǎn)物。
若用 --parallel,pnpm 會忽略拓撲順序,所有包一起跑;適合 dev、test 等不嚴格要求「被依賴的先跑」的場景,但 build 一般別開 --parallel,否則可能用到尚未 build 的依賴。
和 Turborepo / Nx 的關(guān)系
pnpm 只負責(zé)依賴安裝 + 按拓撲序跑 script;緩存、增量構(gòu)建、遠程緩存等,可交給 Turborepo、Nx。通常做法是:pnpm 管 install 和 workspace 鏈接,Turbo/Nx 管 build / test 的調(diào)度與緩存,兩者一起用沒問題。
五、優(yōu)缺點一覽(夠直白版)+ 逐條詳解
5.1 優(yōu)點總覽
| 點 | 說明 |
|---|---|
| 省磁盤、安裝快 | 全局 store + 硬鏈接,避免重復(fù)存包;workspace 包用鏈接,不復(fù)制。 |
| 依賴干凈 | 嚴格依賴,無幽靈依賴;lockfile 唯一,版本一致。 |
| 本地聯(lián)調(diào)友好 | workspace:* 直接鏈到源碼,改即生效,無需 npm link。 |
| monorepo 友好 | 內(nèi)建 workspace 支持,-r、--filter 過濾、并行跑腳本很方便。 |
| 易于做權(quán)限與發(fā)布 | 配合 pnpm publish -r、changesets 做按包發(fā)布、權(quán)限控制。 |
詳細說明:
- 省磁盤、安裝快:原理即第二節(jié)的 store + 硬鏈接;workspace 包不進 store,只做鏈接。典型收益是 monorepo 磁盤占用和
pnpm install耗時明顯下降。 - 依賴干凈:嚴格依賴 + 單一 lockfile,少很多「刪了某包就掛」「本地有 CI 沒有」的玄學(xué)問題;注意若用了
public-hoist-pattern等,要控制范圍,否則又引入隱性依賴。 - 本地聯(lián)調(diào):改
packages/ui立刻在apps/web里生效,無需 link;注意跑 dev 的終端要在根目錄或?qū)?yīng) app 目錄,且已執(zhí)行過根目錄的pnpm install。 - monorepo 友好:
pnpm -r、--filter能力足,再配合 Turborepo/Nx 做任務(wù)編排、緩存,體驗更好。 - 發(fā)布:按包發(fā)布、changesets 管理版本與 changelog,和現(xiàn)有 registry 流程兼容。
5.2 缺點 / 注意點總覽
| 點 | 說明 |
|---|---|
| 和 npm 不完全兼容 | 部分工具假設(shè)「所有依賴扁平在根 node_modules」,可能報錯,需適配。 |
| 學(xué)習(xí)與遷移成本 | 團隊要搞懂 workspace、workspace:*、pnpm-workspace.yaml、--filter 等。 |
| 部分舊工具兼容性 | 極端老舊的構(gòu)建/調(diào)試工具對 pnpm 的 node_modules 結(jié)構(gòu)可能不友好。 |
| 需統(tǒng)一包管理 | 全 repo 必須用 pnpm,不能混用 npm/yarn,否則 lockfile、鏈接會亂。 |
詳細說明:
和 npm 不完全兼容:
有些 Webpack 插件、老版 Babel、個別 CLI 會直接去根node_modules找包,pnpm 默認非扁平就可能找不到。處理辦法:- 用
node-linker=hoisted(.npmrc)切回類 npm 扁平結(jié)構(gòu),會犧牲嚴格依賴; - 或只用
public-hoist-pattern把有問題的包提升上來,盡量窄配。
- 用
學(xué)習(xí)與遷移成本:
團隊至少要會:workspace 概念、pnpm-workspace.yaml、workspace:*協(xié)議、根目錄pnpm install、--filter與-r的用法。可以抽半小時過一遍本文 + 官方文檔,再在試點項目跑一遍。舊工具兼容性:
建議先小范圍試點,遇到具體工具再查 pnpm 兼容性 或社區(qū) issue;大多數(shù)現(xiàn)代前端工具已支持。統(tǒng)一包管理:
全倉庫只用 pnpm,禁止npm install/yarn。用packageManager鎖版本,CI 里corepack enable && pnpm install,避免有人用錯包管理器導(dǎo)致 lockfile 或鏈接關(guān)系錯亂。
適合:中大型前端項目、組件庫 + 多應(yīng)用、多包復(fù)用的 monorepo。
不大適合:單應(yīng)用、沒有多包復(fù)用需求的小項目;用 pnpm 單倉也能受益,但 workspace 收益有限。
六、應(yīng)用場景(什么時候上 workspace?)
下面按場景拆:誰用、解決啥問題、推薦結(jié)構(gòu)、關(guān)鍵配置、日常工作流。你對照自己項目,能直接套用或微調(diào)。
6.1 UI 組件庫 + 多個業(yè)務(wù)項目
場景:你們有一個業(yè)務(wù)組件庫,要同時支撐 2~3 個前端項目;組件庫頻繁迭代,需要在各項目里即時驗證,而不是先發(fā) npm 再裝。
推薦結(jié)構(gòu):
packages/ ui/ # 組件庫 apps/ web-admin/ web-h5/ web-docs/ # 組件文檔
web-admin、web-h5、web-docs 都依賴 @my/ui,用 workspace:*。
關(guān)鍵配置:
pnpm-workspace.yaml:packages: ['packages/*', 'apps/*']。- 各 app 的
package.json:"@my/ui": "workspace:*"。 - 根
scripts:如"dev:docs": "pnpm --filter web-docs run dev","build:ui": "pnpm --filter @my/ui run build"。
工作流:
改 packages/ui → 在 apps/web-docs 或任意 app 里直接看效果;要發(fā)版時用 changesets 給 @my/ui 打 version、寫 changelog、publish,各 app 再決定何時把 workspace:* 換成固定版本(若你們發(fā) npm 的話)。
6.2 多應(yīng)用 + 公共 utils / config
場景:多條產(chǎn)品線、多個前端應(yīng)用,共享 utils、api-client、eslint-config 等,希望統(tǒng)一版本、統(tǒng)一升級。
推薦結(jié)構(gòu):
packages/ utils/ api-client/ config-eslint/ apps/ app-a/ app-b/
apps 按需依賴 @my/utils、@my/api-client;config-eslint 被各 app 的 devDependencies 引用。
關(guān)鍵配置:
pnpm-workspace.yaml:同上。- 各包用
workspace:*互引;根package.json可放公共 devDependencies,或用 catalog 統(tǒng)一 React、TypeScript 等版本。 - 根腳本:
"build": "pnpm -r --filter './apps/*' run build",只構(gòu)建 apps。
工作流:
公共邏輯在 packages/* 改,各 app 自動用到;發(fā)版用 changesets 按包發(fā)布,各 app 通過 workspace:* 或固定版本消費。
6.3 文檔站 + 組件庫
場景:組件庫配套一個文檔站(如 VitePress、Docusaurus),文檔站要直接引用源碼里的組件做 Demo,而不是已發(fā)布的 npm 包。
推薦結(jié)構(gòu):
packages/ ui/ apps/ docs/
docs 依賴 @my/ui,workspace:*。
關(guān)鍵配置:
- 同上,
packages+apps;docs里"@my/ui": "workspace:*"。 - 文檔站構(gòu)建配置里保證能解析
packages/ui的源碼(通常 workspace 鏈接后沒問題)。
工作流:
改組件 → 跑 docs 的 dev,文檔里實時看效果;發(fā)版時先發(fā) @my/ui,再更新文檔站里對版本的說明(若文檔站自己也要發(fā))。
6.4 全棧 monorepo(前后端同倉)
場景:前端 + Node 服務(wù)同倉,共享類型、常量或少量 utils,用同一套依賴管理。
推薦結(jié)構(gòu):
packages/ types/ shared-utils/ apps/ web/ api/ # Node 服務(wù)
api 和 web 都依賴 @my/types、@my/shared-utils,workspace:*。
關(guān)鍵配置:
pnpm-workspace.yaml包含packages/*、apps/*。- 根
package.json的scripts里分別--filter web、--filter api跑 dev/build。
工作流:
改 types 或 shared-utils,前后端同時生效;各自部署時只構(gòu)建對應(yīng) app,公共邏輯通過 workspace 鏈進去。
只要你存在「多個包 + 互相依賴 + 要一起開發(fā)」的需求,workspace 就很值得上;上面四種可以組合,比如「組件庫 + 多應(yīng)用 + 文檔站」一起做。
七、詳細教程:從零搭一個 pnpm workspace
下面按步驟做一遍,每步會寫操作、預(yù)期結(jié)果、常見報錯與排查。路徑、包名和上文保持一致,你照抄就能跑通。
7.1 環(huán)境準備
安裝 pnpm:
npm install -g pnpm
或用 Corepack(Node 16.9+):
corepack enable corepack prepare pnpm@latest --activate
建議用 pnpm 8.x 或 9.x,Node 18+ 更省心。
校驗:
pnpm -v node -v
看到版本號即成功。
7.2 初始化根項目
mkdir my-workspace && cd my-workspace pnpm init
會生成根目錄 package.json。編輯成類似:
{
"name": "my-workspace",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@9.0.0"
}
private: true:根包不會被pnpm publish發(fā)出去,避免誤發(fā)。packageManager:鎖死 pnpm 版本,配合corepack enable使用;可選但推薦。
7.3 配置pnpm-workspace.yaml
在項目根目錄新建 pnpm-workspace.yaml:
packages: - 'packages/*' - 'apps/*'
packages/*:packages/下每個子目錄(如packages/ui、packages/utils)都算一個 workspace 包。apps/*:同理。- 只有被列出來的目錄才會被 pnpm 當成 workspace 成員,才能被
workspace:*引用。
預(yù)期:保存后暫無輸出;之后 pnpm install 時 pnpm 會掃描這些目錄。
7.4 創(chuàng)建子包目錄并初始化
mkdir -p packages/ui packages/utils apps/web
然后逐個初始化(Windows 用戶可用 PowerShell,mkdir -p 若不可用就分步 mkdir):
cd packages/utils && pnpm init && cd ../.. cd packages/ui && pnpm init && cd ../.. cd apps/web && pnpm init && cd ../..
(Windows:若 mkdir -p 報錯,可改為 mkdir packages\ui、mkdir packages\utils、mkdir apps\web 等分步創(chuàng)建;cd ../.. 在 PowerShell 中同樣適用。)
每個子包會多一個 package.json。接下來改包名、入口、exports。
packages/utils/package.json:
{
"name": "@my/utils",
"version": "0.0.1",
"main": "index.js",
"exports": {
".": "./index.js"
}
}
packages/ui/package.json:
{
"name": "@my/ui",
"version": "0.0.1",
"main": "index.js",
"exports": {
".": "./index.js"
}
}
apps/web/package.json:
{
"name": "web",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "echo \"dev placeholder\"",
"build": "echo \"build placeholder\""
}
}
exports:現(xiàn)代 Node 和打包器都認,用來明確入口,避免多余文件被引用;對 ESM、TS 等更友好。web的dev/build先占位,后面驗證完 workspace 再換成真實命令。
7.5 用workspace:*做包間依賴
packages/ui/package.json 里加依賴 @my/utils:
{
"name": "@my/ui",
"version": "0.0.1",
"main": "index.js",
"exports": { ".": "./index.js" },
"dependencies": {
"@my/utils": "workspace:*"
}
}
apps/web/package.json 里加依賴 @my/ui:
{
"name": "web",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "echo \"dev placeholder\"",
"build": "echo \"build placeholder\""
},
"dependencies": {
"@my/ui": "workspace:*"
}
}
workspace:* 表示「用當前 workspace 里的同名包,追蹤源碼」;裝完依賴后會鏈接到對應(yīng)包目錄,改代碼即時生效。
7.6 根目錄執(zhí)行pnpm install
務(wù)必在根目錄執(zhí)行(若不在根目錄,先 cd 到項目根):
pnpm install
預(yù)期:
- 根目錄出現(xiàn)
node_modules/、pnpm-lock.yaml; packages/ui、apps/web的node_modules里會有@my/utils、@my/ui的鏈接;- lockfile 里能看到對
workspace:的解析,例如:
packages:
'@my/utils@workspace:*':
resolution: { directory: packages/utils, type: directory }
'@my/ui@workspace:*':
resolution: { directory: packages/ui, type: directory }
(省略其他字段;實際 lockfile 還有 name、version 等。)
若報 ERR_PNPM_NO_MATCHING_PACKAGE:檢查 pnpm-workspace.yaml 的 packages 是否包含對應(yīng)目錄,以及子包 name 是否和依賴里寫的一致。
7.7 根package.json里加批量腳本
根目錄 package.json 增加:
{
"name": "my-workspace",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@9.0.0",
"scripts": {
"dev": "pnpm -r --parallel run dev",
"build": "pnpm -r run build",
"build:web": "pnpm --filter web run build"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
pnpm -r:遞歸在所有 workspace 包里執(zhí)行同名 script。pnpm -r --parallel:并行跑,適合dev。pnpm --filter web run build:只對web包執(zhí)行build。
Windows:若使用 PowerShell,scripts 里的雙引號、&& 等和 Unix 略有差異,一般上述寫法沒問題;若遇解析錯誤,可改為 node 跑一小段腳本封裝命令。
7.8 驗證 workspace 鏈路
- 在
packages/utils/index.js寫:
module.exports = { add: (a, b) => a + b };
- 在
packages/ui/index.js寫:
const { add } = require('@my/utils');
module.exports = { add, hello: 'from ui' };
- 在
apps/web里加個臨時腳本驗證。給apps/web/package.json的scripts增加一行"run:check",例如:
"scripts": {
"dev": "echo \"dev placeholder\"",
"build": "echo \"build placeholder\"",
"run:check": "node -e \"const x=require('@my/ui'); console.log(x.add(1,2), x.hello)\""
}
保存后,在根目錄執(zhí)行:
pnpm --filter web run run:check
預(yù)期輸出:3 'from ui'。
若 Cannot find module '@my/ui':
- 確認在根目錄執(zhí)行過
pnpm install; - 確認
apps/web的dependencies里有"@my/ui": "workspace:*"; - 看看
apps/web/node_modules/@my下是否有ui的鏈接。
若 ENOENT 等路徑類錯誤:
- 檢查
packages/utils、packages/ui是否有index.js,以及package.json的main/exports是否指向它。
驗證通過后,可以把 web 的 dev / build 換成真實命令(如 Vite、Next 等),繼續(xù)開發(fā)。
八、配置說明(可查閱手冊)
這一節(jié)把 pnpm workspace 相關(guān)配置 拆開講:每項是啥、怎么配、適用場景、注意點。方便你以后查。
8.1pnpm-workspace.yaml
- 唯一性:整個倉庫只放一個在根目錄;pnpm 只認根目錄這份。
packages:- 字符串數(shù)組,每個元素是一個 glob 或具體路徑。
- 例:
'packages/*'、'apps/*'、'tools/*',或'packages/ui'、'packages/utils'。 - 只有匹配到的目錄且其中包含
package.json,才會被當作 workspace 包。
- 排除:部分版本支持
!排除,如!'packages/legacy/*',以你用的 pnpm 文檔為準。 - 與
package.json的workspaces:pnpm 官方推薦用pnpm-workspace.yaml定義 workspace,不用workspaces字段;若同時存在,以pnpm-workspace.yaml為準。
示例:
packages: - 'packages/*' - 'apps/*' - 'tools/*'
8.2 根目錄package.json
private: true:根包不發(fā)布,避免誤pnpm publish。packageManager:如"pnpm@9.0.0",鎖包管理器 + 版本;需corepack enable。scripts:結(jié)合pnpm -r、--filter做批量或定向執(zhí)行(見 8.6)。pnpm.overrides:強制某依賴在全 workspace 解析成指定版本。裝依賴時 pnpm 會按 overrides 解析,并反映在 lockfile;適合修安全漏洞、解決傳遞依賴沖突。{ "pnpm": { "overrides": { "lodash": "4.17.21" } } }catalog(pnpm 9+):在pnpm-workspace.yaml里定義(不是 package.json),子包用catalog:引用;見下方示例。
catalog 示例(pnpm-workspace.yaml):
packages: - 'packages/*' - 'apps/*' catalog: react: ^18.3.1 react-dom: ^18.3.1
子包 package.json:
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
}
}
升級時只改 catalog 即可,所有用 catalog: 的包一起變。
8.3workspace:協(xié)議
workspace:*:用當前 workspace 里同名包的任意版本,并鏈接到源碼目錄。開發(fā)聯(lián)調(diào)默認用這個。workspace:^、workspace:~:按 semver 匹配 workspace 內(nèi)版本;發(fā)布時 pnpm 會把它替換成實際版本號(如1.0.0),所以發(fā)布到 npm 的包不會還帶著workspace:。- 鎖文件里的表現(xiàn):表示解析為本地
'@my/ui@workspace:*': resolution: { directory: packages/ui, type: directory }packages/ui目錄。
日常開發(fā) workspace:* 就夠用;若你們有嚴格的 semver 約束再考慮 ^ / ~。
8.4pnpm-lock.yaml
- 唯一:整份 workspace 共用一個 lockfile,放在根目錄。
- 內(nèi)容:鎖住所有依賴(含 workspace 解析結(jié)果)的版本、完整性校驗等。
- 維護:用
pnpm install、pnpm add等變更依賴,不要手改。 - CI:務(wù)必把
pnpm-lock.yaml納入 git;CI 里pnpm install --frozen-lockfile可保證和 lockfile 完全一致,復(fù)現(xiàn)構(gòu)建。
8.5.npmrc(項目級)
放在項目根目錄,只影響當前倉庫。
常見項:
| 配置項 | 含義 | 示例 |
|---|---|---|
| store-dir | 全局 store 路徑 | store-dir=D:\pnpm-store |
| node-linker | 鏈接方式 | isolated(默認)/ hoisted |
| hoisted | 已廢棄,用 node-linker | — |
| public-hoist-pattern | 哪些包提升到根 node_modules | public-hoist-pattern[]=*eslint* |
| shamefully-hoist | 全部提升,類似 npm | true,易幽靈依賴,慎用 |
| auto-install-peers | 自動裝 peerDependencies | true |
| strict-peer-dependencies | peer 未滿足時報錯 | true |
node-linker=hoisted:切回類 npm 扁平結(jié)構(gòu);兼容性好,但失去嚴格依賴。public-hoist-pattern:只把匹配的包提升,例如 ESLint、Prettier 等工具常見需求;能窄就窄,減少幽靈依賴。resolution-mode:依賴解析策略(如lowest-direct);lockfile-include-tty等可按需查文檔。
示例(只提升部分工具):
public-hoist-pattern[]=*eslint* public-hoist-pattern[]=*prettier*
8.6--filter完整語法
--filter 用來限定要對哪些 workspace 包執(zhí)行命令,常與 pnpm -r、pnpm add 等一起用。
| 寫法 | 含義 | 示例 |
|---|---|---|
--filter <pkg> | 指定包(按 name 或路徑) | pnpm --filter web run build |
--filter <pkg>... | pkg 以及依賴了 pkg 的所有包(dependents) | pnpm -r --filter '@my/ui...' run build |
--filter ...<pkg> | pkg 以及被 pkg 依賴的所有包(dependencies) | pnpm -r --filter '...web' run build |
--filter ...^<pkg> | 僅依賴了 pkg 的包,不含 pkg 自身 | pnpm -r --filter '...^@my/ui' run test |
@scope/* | 通配,所有 @scope 下包 | pnpm -r --filter '@my/*' run build |
示例:
# 只給 web 裝 lodash pnpm add lodash --filter web # 只給名字匹配 @my/* 的包跑 build pnpm -r --filter '@my/*' run build # 只給依賴了 @my/ui 的包跑 test(不含 @my/ui 自身,例如 web、docs) pnpm -r --filter '...^@my/ui' run test # 只給 web 及其依賴的 workspace 包跑 build(含 web 自身) pnpm -r --filter '...web' run build
多 filter 可組合,例如 --filter '@my/ui...' --filter web 表示滿足任一條件的包。僅要「依賴了某包」的包且排除該包本身時,用 ...^<pkg>。
8.7 依賴提升(hoisting)
- 默認:pnpm 不提升,依賴裝在各自包的
node_modules或.pnpm下,嚴格隔離。 public-hoist-pattern:把匹配的包額外提升到根node_modules,方便某些工具查找;提升范圍越大,幽靈依賴風(fēng)險越高。shamefully-hoist:幾乎全部提升,和 npm 類似;不推薦,除非你只是臨時兼容舊工具。
對比:
- 不提升:根
node_modules只有直接依賴,子依賴在.pnpm里,嚴格。 - 提升后:根
node_modules會出現(xiàn)被提升的包,未聲明也可能被引用,所以要想清楚再開。
8.8 只用 pnpm / 鎖包管理
- 全倉庫統(tǒng)一用 pnpm,禁止
npm、yarn,否則 lockfile 和鏈接會亂。 - 根
package.json設(shè)packageManager,如"pnpm@9.0.0"。 - 啟用 Corepack:
corepack enable;CI 里先corepack enable再pnpm install,保證版本一致。
九、和 npm / Yarn workspace 的簡單對比
| 能力 | npm workspaces | Yarn workspace | pnpm workspace |
|---|---|---|---|
| 磁盤占用 | 高,多份拷貝 | 一般 | 低,store+硬鏈接 |
| 安裝速度 | 一般 | 較快 | 快 |
| node_modules 結(jié)構(gòu) | 扁平 | 扁平或 PnP | 非扁平,.pnpm |
| 幽靈依賴 | 易出現(xiàn) | 有 | 默認嚴格,無 |
| lockfile 格式 | package-lock.json | yarn.lock | pnpm-lock.yaml |
| workspace 協(xié)議 | workspace:* 等 | workspace:* 等 | workspace:* 等 |
| 配置方式 | package.json workspaces | package.json workspaces | pnpm-workspace.yaml |
| filter/scripts | 無內(nèi)置 filter | 有 workspaces 腳本 | -r、--filter 等 |
| CI 緩存友好度 | 一般 | 較好 | 好(store 可復(fù)用) |
何時選 pnpm workspace:
- 你打算認真搞 monorepo、多包復(fù)用,且關(guān)注磁盤、安裝速度、依賴干凈。
- 愿意統(tǒng)一用 pnpm,并接受一點學(xué)習(xí)與遷移成本。
何時繼續(xù)用 npm / Yarn:
- 現(xiàn)有 npm/Yarn 腳本、CI 已經(jīng)很成熟,團隊不想動。
- 單倉庫、包很少,workspace 收益有限,用 pnpm 單倉也不錯,不必非上 workspace。
pnpm 的差異主要來自存儲與解析策略,而不是「有沒有 workspace」本身。
十、進階與延伸
10.1 發(fā)版:按包發(fā)布 + changesets
pnpm publish -r:遞歸發(fā)布所有 未 private 的 workspace 包;可加--filter只發(fā)改動的,例如先pnpm -r --filter '@my/ui...' run build再pnpm publish -r --filter '@my/ui'。- changesets:
- 用
changeset管理 version bump 和 changelog; - 流程大致:改代碼 →
pnpm changeset選包、選版本類型、寫 changelog →pnpm changeset version更新版本號 →pnpm publish -r發(fā)布。
這樣多包獨立發(fā)版、可追溯,很常見。
- 用
10.2 任務(wù)編排:Turborepo / Nx
- 根
package.json的build、dev等可以交給 Turbo 或 Nx 跑:他們按依賴圖做拓撲排序,只跑該跑的,且能做遠程/本地緩存,加速 CI 和本地構(gòu)建。 - pnpm workspace 只負責(zé)依賴安裝與鏈接;Turborepo/Nx 負責(zé)任務(wù)調(diào)度,兩者配合良好。
10.3 參考
- pnpm 官方文檔
- pnpm workspace
- pnpm-workspace.yaml
- CLI:
pnpm --help、pnpm install --help、pnpm add --help等
十一、小結(jié)與 FAQ
11.1 小結(jié)
- 問題:多包重復(fù)安裝、幽靈依賴、本地聯(lián)調(diào)麻煩、CI 又慢又占空間 → 本質(zhì)是多包管理 + 依賴存儲/解析沒做好;pnpm workspace 針對這兩點設(shè)計。
- 原理:全局 store + 硬鏈接省空間、提速;非扁平
node_modules+ 嚴格依賴防幽靈依賴;workspace 包鏈到源碼,改即生效。 - 架構(gòu):根
pnpm-workspace.yaml+ 根package.json+ 唯一pnpm-lock.yaml+packages/*/apps/*;子包用workspace:*互引,禁止循環(huán)依賴。 - 配置:弄清
pnpm-workspace.yaml、根package.json、workspace:協(xié)議、.npmrc常用項、--filter用法即可上手。 - 建議:按第七節(jié)親手搭一遍,再在一個小項目里拆一個
utils包用workspace:*引用,跑幾天 dev/build,體感會很明顯;后續(xù)再接 changesets、Turborepo 等。
11.2 FAQ
Q:子包的依賴裝到根還是裝到各自包?
A:各自 package.json 里聲明,各自裝;pnpm 會把實體放在 store、在對應(yīng)包的 node_modules/.pnpm 下鏈接。根 package.json 只放全倉庫共用的 devDependencies(如 TS、ESLint)和腳本。
Q:workspace:* 發(fā)布到 npm 前要改嗎?
A:不用。pnpm publish 時會把 workspace:* 等替換成實際版本號再發(fā)布,發(fā)布出去的 package.json 里是普通版本范圍。
Q:Windows 下路徑或腳本有問題怎么辦?
A:
- 路徑盡量別帶中文、空格;
store-dir等用正斜杠或系統(tǒng)可識別的形式。 - 若在 PowerShell 里
scripts報錯,可試著用node寫一個小腳本封裝pnpm -r/--filter等命令,再在scripts里調(diào)該腳本。 - 全局 pnpm、Node 建議用官方安裝包或 nvm-windows,避免權(quán)限、路徑異常。
如果你有具體的目錄結(jié)構(gòu)或 package.json 想優(yōu)化,可以貼出來,按你現(xiàn)在的項目一步步改也行。
到此這篇關(guān)于前端pnpm workspace架構(gòu)的文章就介紹到這了,更多相關(guān)前端pnpm workspace內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
nodejs獲取本機內(nèi)網(wǎng)和外網(wǎng)ip地址的實現(xiàn)代碼
這篇文章主要介紹了nodejs獲取本機內(nèi)網(wǎng)和外網(wǎng)ip地址的實現(xiàn)代碼,需要的朋友可以參考下2014-06-06
nodejs實現(xiàn)遍歷文件夾并統(tǒng)計文件大小
這篇文章主要介紹了nodejs實現(xiàn)遍歷文件夾并統(tǒng)計文件大小,下面使用nodejs的遍歷文件夾文件內(nèi)容,并且讀取所有的文件,并采取排序往大到小的順序進行輸出,需要的朋友可以參考下2015-05-05
npm安裝yarn后找不到y(tǒng)arn報錯的解決過程
這篇文章主要給大家介紹了關(guān)于npm安裝yarn后找不到y(tǒng)arn報錯的解決過程,文中通過圖文介紹的非常詳細,對遇到同樣問題的同學(xué)具有一定的參考性,需要的朋友可以參考下2023-04-04
把Node.js程序加入服務(wù)實現(xiàn)隨機啟動
這篇文章主要介紹了把Node.js程序加入服務(wù)實現(xiàn)隨機啟動,本文使用qckwinsvc實現(xiàn)這個需求,講解了qckwinsvc的安裝和使用,需要的朋友可以參考下2015-06-06

