跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScriptNode.js大前端

前端 pnpm workspace 架构详解

前端 pnpm workspace 架构,涵盖多包管理、依赖存储问题及底层原理(全局 store、硬链接)。内容包括配置、目录结构、依赖解析机制、优缺点、应用场景及从零搭建教程。通过对比 npm/Yarn,阐述 pnpm 在磁盘、速度及依赖隔离的优势,提供配置命令与最佳实践,助力高效 monorepo 开发。

RefactorPro发布于 2026/4/6更新于 2026/10/2596 浏览

前端 pnpm workspace 架构详解

一、先聊聊:我们到底遇到了啥问题?

做前端久了,多包、monorepo、组件库联调这些事一多,就会踩到一堆具体又磨人的坑。下面把这些痛点拆开说:具体表现 → 典型场景 → 对你有啥影响。搞清楚这些,后面再看 pnpm workspace 解决啥就一目了然。

1.1 node_modules 膨胀,磁盘和时间都遭殃

具体表现:用 npm 搞 monorepo 时,根目录一个 node_modules,每个子包再来一个;或者多个独立项目各自一份。每个 node_modules 里,npm 会做扁平化:把子依赖提升到顶层,同一份包可能在不同项目的 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 会:

  1. 在 pnpm-workspace.yaml 定义的目录里找到对应包(如 packages/ui);
  2. 把该包所在目录(源码目录)链接到 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 # 声明哪些目录是 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/ # 前端应用 ├── 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 装一个。

具体流程:

  1. 读 pnpm-workspace.yaml,收集所有匹配 packages 的目录(如 packages/*、apps/*);
  2. 逐个读这些目录下的 package.json,拿到 name,建成一张 「name → 目录」 的映射;
  3. 解析依赖时,遇到 workspace:*、workspace:^ 等,用依赖里的包名去这张表里查;
  4. 查到了 → 用该包所在目录做链接目标,链到当前包的 node_modules 里;
  5. 查不到 → 报错(例如 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 时,大致会做这几步:

  1. 读 workspace 定义:解析 pnpm-workspace.yaml,得到所有 workspace 包目录(如 packages/*、apps/*)。
  2. 收集包信息:逐个读这些目录下的 package.json,建 name → 目录 映射,并算出整棵依赖树(含对 npm 包的依赖)。
  3. 解析 workspace:*:遇到 workspace:* 等,按 4.3 的规则匹配到本地包目录,不从 registry 拉包。
  4. 链接 workspace 包:把匹配到的本地包目录链到各包的 node_modules 里(符号链接或 junction),不拷贝、不往 store 塞;改源码立即生效。
  5. 装外部依赖:对 npm 上的包,按平时那套来:store + 硬链接,装到 node_modules/.pnpm 等位置。
  6. 写 lockfile:把所有依赖(含 workspace:* 的解析结果)写入根目录的 pnpm-lock.yaml。

所以:workspace 包只做链接,不占 store;占磁盘、耗时的主要是外部依赖,而它们仍走 store 复用。

打包 / 构建(pnpm -r run build)
构建不改依赖安装方式,只是按依赖图顺序跑各包的 build 脚本:

  1. 算依赖图:根据各包 package.json 的依赖关系,得到有向图。
  2. 拓扑排序:排出「被依赖的在前、依赖别人的在后」的顺序(pnpm 内部用类似 graph-sequencer 的方式处理)。
  3. 依次执行:按该顺序对每个 workspace 包执行 pnpm run build(或你配的其它 script)。
  4. 若某包没有 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/ # Node 服务 

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 环境准备

校验:

pnpm -v
node -v

看到版本号即成功。

安装 pnpm:

npm install -g pnpm

或用 Corepack(Node 16.9+):

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"}
}

apps/web/package.json:

{
  "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 到项目根):

pnpm install

预期:

  • 根目录出现 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 里加批量脚本

根目录 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 的链接。

若 ENOENT 等路径类错误:

  • 检查 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

子包 package.json:

{
  "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}

表示解析为本地 packages/ui 目录。

日常开发 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-store
node-linker链接方式isolated(默认)/ hoisted
hoisted已废弃,用 node-linker—
public-hoist-pattern哪些包提升到根 node_modulespublic-hoist-pattern[]=*eslint*
shamefully-hoist全部提升,类似 npmtrue,易幽灵依赖,慎用
auto-install-peers自动装 peerDependenciestrue
strict-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

示例:

# 只给 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,方便某些工具查找;提升范围越大,幽灵依赖风险越高。
  • 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 workspacesYarn workspacepnpm workspace
磁盘占用高,多份拷贝一般低,store+硬链接
安装速度一般较快快
node_modules 结构扁平扁平或 PnP非扁平,.pnpm
幽灵依赖易出现有默认严格,无
lockfile 格式package-lock.jsonyarn.lockpnpm-lock.yaml
workspace 协议workspace:* 等workspace:* 等workspace:* 等
配置方式package.json workspacespackage.json workspacespnpm-workspace.yaml
filter/scripts无内置 filter有 workspaces 脚本-r、--filter 等
CI 缓存友好度一般较好好(store 可复用)

何时选 pnpm workspace:

  • 你打算认真搞 monorepo、多包复用,且关注磁盘、安装速度、依赖干净。
  • 愿意统一用 pnpm,并接受一点学习与迁移成本。

何时继续用 npm / Yarn:

  • 现有 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 参考
  • pnpm 官方文档
  • pnpm workspace
  • pnpm-workspace.yaml
  • CLI:pnpm --help、pnpm install --help、pnpm add --help 等

十一、小结与 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,避免权限、路径异常。

目录

  1. 前端 pnpm workspace 架构详解
  2. 一、先聊聊:我们到底遇到了啥问题?
  3. 1.1 node_modules 膨胀,磁盘和时间都遭殃
  4. 1.2 依赖版本乱成一锅粥:幽灵依赖与冲突
  5. 1.3 本地包联调贼麻烦:npm link 的坑
  6. 1.4 CI 又慢又占空间
  7. 二、pnpm 底层原理:为啥能省空间、装得快、依赖还干净?
  8. 2.1 全局 store:content-addressable + 硬链接
  9. 2.2 node_modules 的真实结构:非扁平 + 严格依赖
  10. 2.3 workspace 包怎么被链接进来?
  11. 2.4 和 npm / Yarn 的存储对比(简要)
  12. 三、pnpm workspace 解决了什么问题?(深化版)
  13. 3.1 磁盘与安装
  14. 3.2 依赖隔离与一致性
  15. 3.3 多包协作与发布
  16. 四、pnpm workspace 架构长什么样?
  17. 4.1 目录树与职责
  18. 4.2 命名与布局约定
  19. 4.3 workspace 包的解析与匹配机制
  20. 4.4 依赖图与构建顺序
  21. 4.5 安装与打包:workspace 如何工作
  22. 五、优缺点一览(够直白版)+ 逐条详解
  23. 5.1 优点总览
  24. 5.2 缺点 / 注意点总览
  25. 六、应用场景(什么时候上 workspace?)
  26. 6.1 UI 组件库 + 多个业务项目
  27. 6.2 多应用 + 公共 utils / config
  28. 6.3 文档站 + 组件库
  29. 6.4 全栈 monorepo(前后端同仓)
  30. 七、详细教程:从零搭一个 pnpm workspace
  31. 7.1 环境准备
  32. 7.2 初始化根项目
  33. 7.3 配置 pnpm-workspace.yaml
  34. 7.4 创建子包目录并初始化
  35. 7.5 用 workspace:* 做包间依赖
  36. 7.6 根目录执行 pnpm install
  37. 7.7 根 package.json 里加批量脚本
  38. 7.8 验证 workspace 链路
  39. 八、配置说明(可查阅手册)
  40. 8.1 pnpm-workspace.yaml
  41. 8.2 根目录 package.json
  42. 8.3 workspace: 协议
  43. 8.4 pnpm-lock.yaml
  44. 8.5 .npmrc(项目级)
  45. 8.6 --filter 完整语法
  46. 只给 web 装 lodash
  47. 只给名字匹配 @my/* 的包跑 build
  48. 只给依赖了 @my/ui 的包跑 test(不含 @my/ui 自身,例如 web、docs)
  49. 只给 web 及其依赖的 workspace 包跑 build(含 web 自身)
  50. 8.7 依赖提升(hoisting)
  51. 8.8 只用 pnpm / 锁包管理
  52. 九、和 npm / Yarn workspace 的简单对比
  53. 十、进阶与延伸
  54. 10.1 发版:按包发布 + changesets
  55. 10.2 任务编排:Turborepo / Nx
  56. 10.3 参考
  57. 十一、小结与 FAQ
  58. 11.1 小结
  59. 11.2 FAQ

更多推荐文章

查看全部
  • Moon VR Video Player 使用指南:支持 8K/12K 多音轨及外挂字幕
  • 2026 年各大高校 AIGC 检测政策汇总
  • AM32 无人机电调源码架构与工作原理解析
  • Gemma-3-12b-it 显存管理:动态分段分配与 OOM 预防机制
  • Open Duck Mini v2 智能行走机器人构建指南
  • Linux 系统安装 libwebkit2gtk-4.1-0 实战指南
  • 高校 AIGC 检测新规解读:AI 率标准与应对策略
  • Qwen-Image-2512 免费本地部署实战指南
  • FPGA 快速傅里叶变换(FFT)实现与配置
  • DeerFlow 2.0 深度解析:字节跳动开源的超级 AI 智能体框架
  • Krita AI 绘画插件本地部署与使用指南
  • 将Microi低代码引擎集成到React项目中
  • 金融场景里的自然语言处理怎么落地
  • 大模型的上游与下游:AI 流水线全貌解析
  • Python 爬虫逆向:网易易盾滑块验证码请求参数分析
  • Stable Diffusion 提示词编写指南:结构、权重与反向提示词
  • C++ 继承入门:从基础概念到默认成员函数
  • Python 移动应用开发实战:Kivy 与 BeeWare 框架详解
  • OpenClaw.ai:Agentic AI 时代的 Spring Framework 时刻
  • Python 使用 Tesseract 实现 OCR 文字识别全流程指南

相关免费在线工具

  • 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