跳到主要内容 前端 pnpm workspace 架构详解 | 极客日志
JavaScript Node.js 大前端
前端 pnpm workspace 架构详解 pnpm workspace 是解决前端多包管理、依赖冲突及磁盘占用问题的方案。通过全局 store 和硬链接机制节省空间,利用非扁平化 node_modules 杜绝幽灵依赖。其底层原理、目录结构、配置方法(pnpm-workspace.yaml)、依赖协议(workspace:*)及构建流程,并提供从零搭建实战教程,帮助团队实现高效的 Monorepo 开发与发布。
DataScient 发布于 2026/4/6 更新于 2026/4/13 1 浏览前端 pnpm workspace 架构详解
一、先聊聊:我们到底遇到了啥问题?
做前端久了,多包、monorepo、组件库联调这些事一多,就会踩到一堆具体又磨人的坑。下面把这些痛点拆开说:具体表现 → 典型场景 → 对你有啥影响 。搞清楚这些,后面再看 pnpm workspace 解决啥就一目了然。
1.1 node_modules 膨胀,磁盘和时间都遭殃
具体表现 :用 npm 搞 monorepo 时,根目录一个 ,每个子包再来一个;或者多个独立项目各自一份。每个 里,npm 会做 :把子依赖提升到顶层,同一份包可能在不同项目的 里各存一份, 。
node_modules
node_modules
扁平化
node_modules
重复拷贝
典型场景 :比如你有一个 monorepo,里面 5 个 app、3 个共享库,都用 React、lodash、一堆 Babel/Webpack 相关包。单项目 node_modules 可能就 400~600MB,monorepo 里再乘上包数量、加上提升带来的重复,轻松破 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。
典型场景 :你习惯性 import _ from 'lodash',但从没在 package.json 里加过 lodash,因为它是某个依赖的子依赖,被提升上来了。后来你升级了那个依赖,人家不再依赖 lodash,或者换了版本,你这边没改一行业务代码 就报错:找不到 lodash。更坑的是「本地能跑、CI 挂」:本地可能还有别的路径残留或缓存,CI 干净安装就炸。同理,删了某个你以为没用的依赖 ,结果别的地方一直隐式用着,一删就挂。
版本冲突 :A 包要 React 18,B 包要 React 17,扁平化之后只能满足一边,另一边可能用了「不对」的版本,运行时才暴露问题,调试成本很高。
1.3 本地包联调贼麻烦:npm link 的坑 典型场景 :你维护一个业务组件库,要在另一个前端项目里联调。通常做法是 npm link:在组件库目录 npm link,在业务项目里 npm link your-components。但经常会遇到:
双实例问题 :React、Vue 等对「单实例」有要求,link 过去可能出现两个版本,引发诡异 bug。
bin 路径 :某些 CLI 或工具通过 node_modules/.bin 找可执行文件,link 后路径解析不对,跑不起来。
不同 Node 版本 / 环境 :link 的是「当时本机」的构建结果,换机器、换 Node、改点配置,行为可能不一致。
总之,改一下组件库就要反复 link、unlink、重装 ,体验很差,也容易忘步骤导致联调结果不可靠。
1.4 CI 又慢又占空间 典型场景 :每次 CI 全量 npm install,没有跨项目或跨 job 的 store 复用;缓存 key 设计不当(例如只按 package.json 不按 lockfile),导致缓存命中率低,每次都几乎全量装。加上前面说的 node_modules 巨大,流水线耗时长、占用空间大,体验和成本都不好。
上面这些,本质都可以归为两类问题 :一是多包怎么组织、怎么一起开发、怎么发布 (项目结构 + 工作流);二是依赖怎么存、怎么解析、怎么隔离 (存储与解析策略)。pnpm 的 workspace 就是在这两方面同时发力的方案之一:多包管理 + 更合理的依赖存储与解析。下面先把你可能最关心的——pnpm 底层是怎么干的 ——讲清楚,再回头看 workspace 具体解决了啥。
二、pnpm 底层原理:为啥能省空间、装得快、依赖还干净? 很多人只记住结论:「pnpm 省磁盘、快、没幽灵依赖」,但不知道它到底咋做到的。这一节把存储模型 和 node_modules 结构 说透,你后面看配置、看优缺点都会更有数。
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)
若设置了 $XDG_DATA_HOME,Linux/macOS 会改用 $XDG_DATA_HOME/pnpm/store。可通过 .npmrc 的 store-dir 覆盖,例如 store-dir=D:\pnpm-store。
content-addressable(按内容寻址) :
包在 store 里按内容哈希存,同一版本、同一份包只存一份 。不同项目、不同 monorepo 子包,只要依赖的版本相同,都用这一份,去重、跨项目复用 。
硬链接 :
硬链接可以理解为「同一份文件的多个路径入口」,改一处全体生效,但不额外占磁盘 。pnpm 从 store 把包硬链接到项目里的 node_modules/.pnpm/...,所以看起来每个项目都有一份,实际磁盘只存 store 里那一份。
和复制 的区别:不占多余空间。和符号链接 的区别:符号链接是「指向另一个路径」的小文件,硬链接是文件系统层面的多路径同一 inode,更省空间、也更稳定(删掉一个链接不会影响 store 里的那份,只要还有别的链接在)。
结果 :同 monorepo、同样依赖,用 pnpm 时磁盘占用往往只有 npm 的一半左右 (常见 benchmark 结论),二次安装时大量命中 store,pnpm install 明显更快。
2.2 node_modules 的真实结构:非扁平 + 严格依赖 npm 会把依赖扁平化 提升到顶层,所以你能「意外」用到子依赖;pnpm 不这么做 ,结构是非扁平 的。
项目根目录的 node_modules/:
只放你直接声明 的依赖(dependencies / devDependencies 里的包)。
这些「包名」多数是符号链接 ,指向 node_modules/.pnpm/<pkg>@<version>/node_modules/<pkg>。
node_modules/.pnpm/:
里面才是实际内容(或链到 store)。
每个 package@version 一个目录,且每个包有自己的 node_modules ,里面只装它自己的依赖 。
子依赖不会提升到项目根 node_modules,所以你没法 在业务代码里 require('某个未声明的子依赖')。
严格依赖 就是这样实现的:
只有 在 package.json 里显式声明的包,才会出现在你项目的 node_modules 顶层(或子包自己的 node_modules 里)。未声明的包根本不在你可访问的路径下,require / import 会直接报错,从根上杜绝幽灵依赖 。
有些老旧工具会假设「所有依赖都在根 node_modules 扁平展开」,在 pnpm 默认结构下会找不到包。这时可以用 public-hoist-pattern 或 node-linker=hoisted 做有限提升 ,相当于在「兼容旧工具」和「严格依赖」之间做权衡;提升多了,幽灵依赖风险又回来了,所以能窄就窄。
2.3 workspace 包怎么被链接进来? 当你在 package.json 里写 "@my/ui": "workspace:*" 时,pnpm 会:
在 pnpm-workspace.yaml 定义的目录里找到对应包(如 packages/ui);
把该包所在目录 (源码目录)链接到 node_modules 里对应位置,不拷贝、不先打包 。
所以,你改 packages/ui 的源码,消费方(例如 apps/web)立即可见 ,不用 npm link,也没有双实例、路径错乱那些破事。这就是 workspace 协议 带来的「本地包即源码」的联调体验。
2.4 和 npm / Yarn 的存储对比(简要)
npm :扁平化 + 每项目各自拷贝,多项目多份;易幽灵依赖;安装速度、磁盘占用都一般。
Yarn :经典模式类似 npm;Plug'n'Play 可选,但生态兼容性要看工具。
pnpm :全局 store + 硬链接 + 非扁平 node_modules,省空间、安装快、默认严格依赖。
差异主要在存储与解析策略 ,而不是「有没有 workspace」这个概念。
三、pnpm workspace 解决了什么问题?(深化版) 有了第二节的原理打底,这里直接说 workspace 在「多包管理」场景下,具体帮你解决了啥;每个点都往「能用、能查」上靠。
3.1 磁盘与安装
store + 硬链接 :全 workspace 共享同一 store,同版本依赖只存一份;子包、apps 装依赖都是链过去,磁盘占用明显低于 npm 同规模 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 有限提升,并清楚这会带来隐性依赖风险。
版本统一 :
单一 lockfile :整个 workspace 只有一个 pnpm-lock.yaml 在根目录,所有子包、所有环境的依赖解析都以它为准,版本全仓库一致 ,复现性高。
catalog(pnpm 9+) :在 pnpm-workspace.yaml 里定义 catalog,给常用依赖约定版本(如 react: ^18.3.1),子包用 catalog: 引用,升级时只改一处,避免各包各自为政。
overrides :根 package.json 里可配 pnpm.overrides,强制某依赖在全 workspace 解析成指定版本,适合解决传递依赖冲突、安全修复等。
3.3 多包协作与发布
统一装依赖、统一跑脚本 :根目录一次 pnpm install,所有 workspace 包依赖都装好;用 pnpm -r run build、pnpm --filter ... 批量或定向跑脚本,配合根 package.json 的 scripts,协作流程清晰。
按需发布 :pnpm publish -r 可递归发布,结合 --filter 只发布改动的包;配合 changesets 做 version + changelog + publish,适合多包独立发版。
权限与发包 :可以按包名、按目录做 access 控制,和现有 npm registry 权限模型配合使用。
四、pnpm workspace 架构长什么样?
4.1 目录树与职责 下面是一个常见的 pnpm workspace 根目录结构,以及各部分的职责。
项目根目录 ├── pnpm-workspace.yaml
├── package.json
├── pnpm-lock.yaml
├── .npmrc
├── packages/
│ ├── ui/
│ ├── utils/
│ ├── config-eslint/
│ └── ...
└── apps/
├── web/
├── docs/
└── ...
根 package.json :
放全仓库共用 的 devDependencies(如 TypeScript、ESLint、Vitest、Prettier)。
定义 scripts,用 pnpm -r、--filter 批量或定向执行子包的 build、dev、test。
根包通常 "private": true,不发布;可加 packageManager、pnpm.overrides 等。
pnpm-workspace.yaml :
唯一 ,只能放在根目录。
通过 packages 数组声明哪些目录算 workspace 包(如 packages/*、apps/*),只有这些才能被 workspace:* 引用。
pnpm 官方推荐用这个文件,而不是 package.json 的 workspaces 字段。
pnpm-lock.yaml :
全 workspace 共用一个 ,在根目录。
锁死所有依赖(含 workspace 包解析结果),保证任意环境 pnpm install 结果一致。
packages/* :
一般放可复用库 :组件库、工具库、配置包等。
各自有 package.json,通过 workspace:* 相互依赖或被 apps/* 依赖。
apps/* :
一般放应用 :前端项目、文档站、Demo 等。
依赖 packages/* 时用 workspace:*,改库即生效。
有的项目还会加 tools/* 放脚本、CLI 等,本质上一样:在 pnpm-workspace.yaml 里写上对应 glob 即可。
4.2 命名与布局约定
packages :可复用、可能发布到 npm 的库;apps :入口应用、不发布或只发构建产物。
何时拆 apps?当你明确有「多个应用 + 共享 packages」时,拆开更清晰;只有一两个 app 时,全放 packages 也没问题,按团队习惯来。
依赖方向 :
子包互相依赖、app 依赖子包,一律用 workspace:* 。
禁止循环依赖 (A 依赖 B,B 又依赖 A),否则安装、构建都会出问题。
根包通常不 作为业务依赖,只提供脚本和公共 devDependencies。
4.3 workspace 包的解析与匹配机制 靠啥匹配?
pnpm 解析 workspace:* 时,只看 package.json 里的 name ,和目录名、路径都无关。你写 "@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 里同名包的任意版本 ,并链到源码目录;开发联调最常用。
workspace:^ 、workspace:~ :按 semver 匹配 workspace 内版本;发布时 会被替换成具体版本号(如 1.0.0),发布出去的 package.json 里不会还带着 workspace:。
**workspace:../packages/utils**(相对路径):明确指向某个目录,不靠 name 匹配;适合临时调试或路径敏感的布局。
别名 :
可以用 "别名": "workspace:真实包名@*" 把 workspace 包挂到另一个名字下,例如 "react": "workspace:my-react@*"。发布时同样会替换成普通依赖形式。
找不到会怎样?
只会报错,不会 回退到 npm 装。这样你才能确定:用的一定是本地的 workspace 包,没有误用远端的。
4.4 依赖图与构建顺序 workspace 里包和包之间的依赖关系 ,会形成一张有向图 :谁依赖谁,一目了然。pnpm 跑 pnpm -r run build 这类递归命令时,默认按这张图的拓扑顺序 执行:先跑被依赖的,再跑依赖别人的 ,避免「还没 build 完就被别人 require」的坑。
拓扑顺序是啥?
简单说:若 A 依赖 B,则一定先 执行 B 的 build,再 执行 A 的 build。例如 utils → ui → web,顺序就是 utils → ui → web。同一层之间(比如多个 app 互不依赖)谁先谁后不保证,但层级不会乱。
**pnpm -r run build**(以及 pnpm -r run <script>):按依赖图拓扑排序 ,再依次执行;没有 -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(同一层并行,层与层之间仍按依赖顺序)。
循环依赖 :
若出现 A → B → C → A,依赖图成环,拓扑排序搞不定,pnpm 会报错;安装、-r 执行都可能挂。所以必须 保证 workspace 内无环,设计时就要避免「包互相依赖」。
4.5 安装与打包:workspace 如何工作 安装(pnpm install)
在根目录 执行 pnpm install 时,大致会做这几步:
读 workspace 定义 :解析 pnpm-workspace.yaml,得到所有 workspace 包目录(如 packages/*、apps/*)。
收集包信息 :逐个读这些目录下的 package.json,建 name → 目录 映射,并算出整棵依赖树 (含对 npm 包的依赖)。
解析 workspace:* :遇到 workspace:* 等,按 4.3 的规则匹配到本地包目录,不 从 registry 拉包。
链接 workspace 包 :把匹配到的本地包目录 链到各包的 node_modules 里(符号链接或 junction),不拷贝、不往 store 塞 ;改源码立即生效。
装外部依赖 :对 npm 上的包,按平时那套来:store + 硬链接,装到 node_modules/.pnpm 等位置。
写 lockfile :把所有依赖(含 workspace:* 的解析结果)写入根目录的 pnpm-lock.yaml。
所以:workspace 包只做链接,不占 store ;占磁盘、耗时的主要是外部依赖,而它们仍走 store 复用。
打包 / 构建(pnpm -r run build)
构建不 改依赖安装方式,只是按依赖图顺序 跑各包的 build 脚本:
算依赖图 :根据各包 package.json 的依赖关系,得到有向图。
拓扑排序 :排出「被依赖的在前、依赖别人的在后」的顺序(pnpm 内部用类似 graph-sequencer 的方式处理)。
依次执行 :按该顺序对每个 workspace 包执行 pnpm run build(或你配的其它 script)。
若某包没有 build 脚本,pnpm 会报错或跳过该包,视配置而定。
因此:先装依赖,再构建 ;装依赖保证 node_modules 里 workspace 包、npm 包都就位,构建则按依赖顺序生成各包产物。
若用 --parallel ,pnpm 会忽略拓扑顺序 ,所有包一起跑;适合 dev、test 等不严格要求「被依赖的先跑」的场景,但 build 一般别开 --parallel ,否则可能用到尚未 build 的依赖。
和 Turborepo / Nx 的关系
pnpm 只负责依赖安装 + 按拓扑序跑 script ;缓存、增量构建、远程缓存 等,可交给 Turborepo、Nx。通常做法是:pnpm 管 install 和 workspace 链接,Turbo/Nx 管 build / test 的调度与缓存,两者一起用没问题。
五、优缺点一览(够直白版)+ 逐条详解
5.1 优点总览 点 说明 省磁盘、安装快 全局 store + 硬链接,避免重复存包;workspace 包用链接,不复制。 依赖干净 严格依赖,无幽灵依赖;lockfile 唯一,版本一致。 本地联调友好 workspace:* 直接链到源码,改即生效,无需 npm link。monorepo 友好 内建 workspace 支持,-r、--filter 过滤、并行跑脚本很方便。 易于做权限与发布 配合 pnpm publish -r、changesets 做按包发布、权限控制。
省磁盘、安装快 :原理即第二节的 store + 硬链接;workspace 包不进 store,只做链接。典型收益是 monorepo 磁盘占用和 pnpm install 耗时明显下降。
依赖干净 :严格依赖 + 单一 lockfile,少很多「删了某包就挂」「本地有 CI 没有」的玄学问题;注意若用了 public-hoist-pattern 等,要控制范围,否则又引入隐性依赖。
本地联调 :改 packages/ui 立刻在 apps/web 里生效,无需 link;注意跑 dev 的终端要在根目录 或对应 app 目录,且已执行过根目录的 pnpm install。
monorepo 友好 :pnpm -r、--filter 能力足,再配合 Turborepo/Nx 做任务编排、缓存,体验更好。
发布 :按包发布、changesets 管理版本与 changelog,和现有 registry 流程兼容。
5.2 缺点 / 注意点总览 点 说明 和 npm 不完全兼容 部分工具假设「所有依赖扁平在根 node_modules」,可能报错,需适配。 学习与迁移成本 团队要搞懂 workspace、workspace:*、pnpm-workspace.yaml、--filter 等。 部分旧工具兼容性 极端老旧的构建/调试工具对 pnpm 的 node_modules 结构可能不友好。 需统一包管理 全 repo 必须用 pnpm,不能混用 npm/yarn,否则 lockfile、链接会乱。
和 npm 不完全兼容 :
有些 Webpack 插件、老版 Babel、个别 CLI 会直接去根 node_modules 找包,pnpm 默认非扁平就可能找不到。处理办法:
用 **node-linker=hoisted**(.npmrc)切回类 npm 扁平结构,会牺牲严格依赖;
或只用 public-hoist-pattern 把有问题的包提升上来,尽量窄配。
学习与迁移成本 :
团队至少要会:workspace 概念、pnpm-workspace.yaml、workspace:* 协议、根目录 pnpm install、--filter 与 -r 的用法。可以抽半小时过一遍本文 + 官方文档,再在试点项目跑一遍。
旧工具兼容性 :
建议先小范围试点,遇到具体工具再查 pnpm 兼容性 或社区 issue;大多数现代前端工具已支持。
统一包管理 :
全仓库只用 pnpm ,禁止 npm install / yarn。用 **packageManager** 锁版本,CI 里 corepack enable && pnpm install,避免有人用错包管理器导致 lockfile 或链接关系错乱。
适合 :中大型前端项目、组件库 + 多应用、多包复用的 monorepo。
不大适合 :单应用、没有多包复用需求的小项目;用 pnpm 单仓也能受益,但 workspace 收益有限。
六、应用场景(什么时候上 workspace?) 下面按场景 拆:谁用、解决啥问题、推荐结构、关键配置、日常工作流。你对照自己项目,能直接套用或微调。
6.1 UI 组件库 + 多个业务项目 场景 :你们有一个业务组件库,要同时支撑 2~3 个前端项目;组件库频繁迭代,需要在各项目里即时验证 ,而不是先发 npm 再装。
packages/
ui/
apps/
web-admin/
web-h5/
web-docs/
web-admin、web-h5、web-docs 都依赖 @my/ui,用 workspace:*。
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 里直接看效果;要发版时用 changesets 给 @my/ui 打 version、写 changelog、publish,各 app 再决定何时把 workspace:* 换成固定版本(若你们发 npm 的话)。
6.2 多应用 + 公共 utils / config 场景 :多条产品线、多个前端应用,共享 utils、api-client、eslint-config 等,希望统一版本、统一升级 。
packages/
utils/
api-client/
config-eslint/
apps/
app-a/
app-b/
apps 按需依赖 @my/utils、@my/api-client;config-eslint 被各 app 的 devDependencies 引用。
pnpm-workspace.yaml:同上。
各包用 workspace:* 互引;根 package.json 可放公共 devDependencies,或用 catalog 统一 React、TypeScript 等版本。
根脚本:"build": "pnpm -r --filter './apps/*' run build",只构建 apps。
工作流 :
公共逻辑在 packages/* 改,各 app 自动用到;发版用 changesets 按包发布,各 app 通过 workspace:* 或固定版本消费。
6.3 文档站 + 组件库 场景 :组件库配套一个文档站(如 VitePress、Docusaurus),文档站要直接引用源码 里的组件做 Demo,而不是已发布的 npm 包。
packages/
ui/
apps/
docs/
docs 依赖 @my/ui,workspace:*。
同上,packages + apps;docs 里 "@my/ui": "workspace:*"。
文档站构建配置里保证能解析 packages/ui 的源码(通常 workspace 链接后没问题)。
工作流 :
改组件 → 跑 docs 的 dev,文档里实时看效果;发版时先发 @my/ui,再更新文档站里对版本的说明(若文档站自己也要发)。
6.4 全栈 monorepo(前后端同仓) 场景 :前端 + Node 服务同仓,共享类型、常量或少量 utils,用同一套依赖管理。
packages/
types/
shared-utils/
apps/
web/
api/
api 和 web 都依赖 @my/types、@my/shared-utils,workspace:*。
pnpm-workspace.yaml 包含 packages/*、apps/*。
根 package.json 的 scripts 里分别 --filter web、--filter api 跑 dev/build。
工作流 :
改 types 或 shared-utils,前后端同时生效;各自部署时只构建对应 app,公共逻辑通过 workspace 链进去。
只要你存在「多个包 + 互相依赖 + 要一起开发 」的需求,workspace 就很值得上;上面四种可以组合,比如「组件库 + 多应用 + 文档站」一起做。
七、详细教程:从零搭一个 pnpm workspace 下面按步骤做一遍,每步会写操作、预期结果、常见报错与排查 。路径、包名和上文保持一致,你照抄就能跑通。
7.1 环境准备 corepack enable
corepack prepare pnpm@latest --activate
建议用 pnpm 8.x 或 9.x ,Node 18+ 更省心。
7.2 初始化根项目 mkdir my-workspace && cd my-workspace
pnpm init
会生成根目录 package.json。编辑成类似:
{
"name" : "my-workspace" ,
"version" : "1.0.0" ,
"private" : true ,
"packageManager" : "[email protected] "
}
**private: true**:根包不会被 pnpm publish 发出去,避免误发。
**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:* 引用。
预期 :保存后暂无输出;之后 pnpm install 时 pnpm 会扫描这些目录。
7.4 创建子包目录并初始化 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 等分步创建;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" }
}
{
"name" : "web" ,
"version" : "0.0.1" ,
"private" : true ,
"scripts" : {
"dev" : "echo \"dev placeholder\"" ,
"build" : "echo \"build placeholder\""
}
}
exports :现代 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 里的同名包,追踪源码」;装完依赖后会链接到对应包目录 ,改代码即时生效。
7.6 根目录执行 pnpm install 务必在根目录 执行(若不在根目录,先 cd 到项目根):
根目录出现 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 是否包含对应目录,以及子包 name 是否和依赖里写的一致。
7.7 根 package.json 里加批量脚本 {
"name" : "my-workspace" ,
"version" : "1.0.0" ,
"private" : true ,
"packageManager" : "[email protected] " ,
"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 包里执行同名 script。
**pnpm -r --parallel**:并行跑,适合 dev。
**pnpm --filter web run build**:只对 web 包执行 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)\""
}
}
pnpm --filter web run run:check
预期输出 :3 'from ui'。
若 Cannot find module '@my/ui' :
确认在根目录 执行过 pnpm install;
确认 apps/web 的 dependencies 里有 "@my/ui": "workspace:*";
看看 apps/web/node_modules/@my 下是否有 ui 的链接。
检查 packages/utils、packages/ui 是否有 index.js,以及 package.json 的 main / exports 是否指向它。
验证通过后,可以把 web 的 dev / build 换成真实命令(如 Vite、Next 等),继续开发。
八、配置说明(可查阅手册) 这一节把 pnpm workspace 相关配置 拆开讲:每项是啥、怎么配、适用场景、注意点。方便你以后查。
8.1 pnpm-workspace.yaml
唯一性 :整个仓库只放一个 在根目录;pnpm 只认根目录这份。
packages :
字符串数组,每个元素是一个 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**:根包不发布,避免误 pnpm publish。
**packageManager**:如 "[email protected] ",锁包管理器 + 版本;需 corepack enable。
**scripts**:结合 pnpm -r、--filter 做批量或定向执行(见 8.6)。
catalog(pnpm 9+) :在 **pnpm-workspace.yaml** 里定义(不是 package.json),子包用 catalog: 引用;见下方示例。
pnpm.overrides :强制某依赖在全 workspace 解析成指定版本。
{
"pnpm" : {
"overrides" : {
"lodash" : "4.17.21"
}
}
}
装依赖时 pnpm 会按 overrides 解析,并反映在 lockfile;适合修安全漏洞、解决传递依赖冲突。
catalog 示例 (pnpm-workspace.yaml):
packages:
- 'packages/*'
- 'apps/*'
catalog:
react: ^18.3.1
react-dom: ^18.3.1
{
"dependencies" : {
"react" : "catalog:" ,
"react-dom" : "catalog:"
}
}
升级时只改 catalog 即可,所有用 catalog: 的包一起变。
8.3 workspace: 协议
workspace:* :用当前 workspace 里同名包 的任意版本 ,并链接到源码目录 。开发联调默认用这个。
workspace:^ 、workspace:~ :按 semver 匹配 workspace 内版本;发布时 pnpm 会把它替换成实际版本号 (如 1.0.0),所以发布到 npm 的包不会还带着 workspace:。
'@my/ui@workspace:*':
resolution:
directory: packages/ui
type: directory
日常开发 workspace:* 就够用 ;若你们有严格的 semver 约束再考虑 ^ / ~。
8.4 pnpm-lock.yaml
唯一 :整份 workspace 共用一个 lockfile,放在根目录。
内容 :锁住所有依赖(含 workspace 解析结果)的版本、完整性校验等。
维护 :用 pnpm install、pnpm add 等变更依赖,不要手改 。
CI :务必把 pnpm-lock.yaml 纳入 git;CI 里 pnpm install --frozen-lockfile 可保证和 lockfile 完全一致,复现构建。
8.5 .npmrc(项目级) 配置项 含义 示例 store-dir全局 store 路径 store-dir=D:\pnpm-storenode-linker链接方式 isolated(默认)/ hoistedhoisted已废弃,用 node-linker — public-hoist-pattern哪些包提升到根 node_modules public-hoist-pattern[]=*eslint*shamefully-hoist全部提升,类似 npm true,易幽灵依赖,慎用auto-install-peers自动装 peerDependencies truestrict-peer-dependenciespeer 未满足时报错 true
node-linker=hoisted :切回类 npm 扁平结构;兼容性好,但失去严格依赖。
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 包执行命令,常与 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
pnpm add lodash --filter web
pnpm -r --filter '@my/*' run build
pnpm -r --filter '...^@my/ui' run test
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,方便某些工具查找;提升范围越大,幽灵依赖风险越高。
shamefully-hoist :几乎全部提升,和 npm 类似;不推荐 ,除非你只是临时兼容旧工具。
不提升:根 node_modules 只有直接依赖,子依赖在 .pnpm 里,严格。
提升后:根 node_modules 会出现被提升的包,未声明也可能被引用,所以要想清楚再开。
8.8 只用 pnpm / 锁包管理
全仓库统一用 pnpm ,禁止 npm、yarn,否则 lockfile 和链接会乱。
根 package.json 设 **packageManager**,如 "[email protected] "。
启用 Corepack :corepack enable;CI 里先 corepack enable 再 pnpm install,保证版本一致。
九、和 npm / Yarn workspace 的简单对比 能力 npm workspaces Yarn workspace pnpm workspace 磁盘占用 高,多份拷贝 一般 低,store+硬链接 安装速度 一般 较快 快 node_modules 结构 扁平 扁平或 PnP 非扁平,.pnpm 幽灵依赖 易出现 有 默认严格,无 lockfile 格式 package-lock.jsonyarn.lockpnpm-lock.yamlworkspace 协议 workspace:* 等workspace:* 等workspace:* 等配置方式 package.json workspacespackage.json workspacespnpm-workspace.yamlfilter/scripts 无内置 filter 有 workspaces 脚本 -r、--filter 等CI 缓存友好度 一般 较好 好(store 可复用)
你打算认真搞 monorepo、多包复用,且关注磁盘、安装速度、依赖干净。
愿意统一用 pnpm,并接受一点学习与迁移成本。
现有 npm/Yarn 脚本、CI 已经很成熟,团队不想动。
单仓库、包很少,workspace 收益有限,用 pnpm 单仓也不错,不必非上 workspace。
pnpm 的差异主要来自存储与解析策略 ,而不是「有没有 workspace」本身。
十、进阶与延伸
10.1 发版:按包发布 + changesets
pnpm publish -r :递归发布所有 未 private 的 workspace 包;可加 --filter 只发改动的,例如先 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 发布。
这样多包独立发版、可追溯,很常见。
10.2 任务编排:Turborepo / Nx
根 package.json 的 build、dev 等可以交给 Turbo 或 Nx 跑:他们按依赖图做拓扑排序 ,只跑该跑的,且能做远程/本地缓存 ,加速 CI 和本地构建。
pnpm workspace 只负责依赖安装与链接 ;Turborepo/Nx 负责任务调度 ,两者配合良好。
10.3 参考
十一、小结与 FAQ
11.1 小结
问题 :多包重复安装、幽灵依赖、本地联调麻烦、CI 又慢又占空间 → 本质是多包管理 + 依赖存储/解析 没做好;pnpm workspace 针对这两点设计。
原理 :全局 store + 硬链接省空间、提速;非扁平 node_modules + 严格依赖防幽灵依赖;workspace 包链到源码,改即生效。
架构 :根 pnpm-workspace.yaml + 根 package.json + 唯一 pnpm-lock.yaml + packages/* / apps/*;子包用 workspace:* 互引,禁止循环依赖。
配置 :弄清 pnpm-workspace.yaml、根 package.json、workspace: 协议、.npmrc 常用项、--filter 用法即可上手。
建议 :按第七节亲手搭一遍 ,再在一个小项目里拆一个 utils 包用 workspace:* 引用,跑几天 dev/build,体感会很明显;后续再接 changesets、Turborepo 等。
11.2 FAQ Q:子包的依赖装到根还是装到各自包?
A:各自 package.json 里声明,各自装 ;pnpm 会把实体放在 store、在对应包的 node_modules/.pnpm 下链接。根 package.json 只放全仓库共用 的 devDependencies(如 TS、ESLint)和脚本。
Q:workspace:* 发布到 npm 前要改吗?
A:不用 。pnpm publish 时会把 workspace:* 等替换成实际版本号 再发布,发布出去的 package.json 里是普通版本范围。
Q:Windows 下路径或脚本有问题怎么办?
A:
路径尽量别带中文、空格;store-dir 等用正斜杠或系统可识别的形式。
若在 PowerShell 里 scripts 报错,可试着用 node 写一个小脚本封装 pnpm -r / --filter 等命令,再在 scripts 里调该脚本。
全局 pnpm、Node 建议用官方安装包或 nvm-windows,避免权限、路径异常。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online