【架构】前端 pnpm workspace详解

前端 pnpm workspace 架构详解

一篇帮你搞懂 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.jsondependencies / devDependencies 里声明,你却能在代码里 importrequire 到它。常见原因就是 npm 的扁平化 :你装了 A,A 依赖 B,B 被提升到了项目根 node_modules,于是你的代码「意外」地能直接用 B。

典型场景 :你习惯性 import _ from 'lodash',但从没在 package.json 里加过 lodash,因为它是某个依赖的子依赖,被提升上来了。后来你升级了那个依赖,人家不再依赖 lodash,或者换了版本,你这边没改一行业务代码 就报错:找不到 lodash。更坑的是「本地能跑、CI 挂」:本地可能还有别的路径残留或缓存,CI 干净安装就炸。同理,删了某个你以为没用的依赖,结果别的地方一直隐式用着,一删就挂。

版本冲突:A 包要 React 18,B 包要 React 17,扁平化之后只能满足一边,另一边可能用了「不对」的版本,运行时才暴露问题,调试成本很高。

典型场景 :你维护一个业务组件库,要在另一个前端项目里联调。通常做法是 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。可通过 .npmrcstore-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-patternnode-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 buildpnpm --filter ... 批量或定向跑脚本,配合根 package.jsonscripts,协作流程清晰。
  • 按需发布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,不发布;可加 packageManagerpnpm.overrides 等。
  • pnpm-workspace.yaml

    • 唯一,只能放在根目录。
    • 通过 packages 数组声明哪些目录算 workspace 包(如 packages/*apps/*),只有这些才能被 workspace:* 引用。
    • pnpm 官方推荐用这个文件,而不是 package.jsonworkspaces 字段。
  • 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/uiname 要是 @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。例如 utilsuiweb,顺序就是 utilsuiweb。同一层之间(比如多个 app 互不依赖)谁先谁后不保证,但层级不会乱。

默认行为

  • pnpm -r run build (以及 pnpm -r run <script>):按依赖图拓扑排序 ,再依次执行;没有 -r 时则只跑当前包。
  • pnpm -r --parallel run build不管顺序 ,所有包并行 跑;跑 devtest 时常用 --parallel,但 build 一般要保证顺序,所以慎用 --parallel

怎么知道谁依赖谁?

  • 看各包 package.jsondependencies / 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 会忽略拓扑顺序 ,所有包一起跑;适合 devtest 等不严格要求「被依赖的先跑」的场景,但 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.yamlworkspace:* 协议、根目录 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-adminweb-h5web-docs 都依赖 @my/ui,用 workspace:*

关键配置

  • pnpm-workspace.yamlpackages: ['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

场景 :多条产品线、多个前端应用,共享 utilsapi-clienteslint-config 等,希望统一版本、统一升级

推荐结构

复制代码
packages/
  utils/
  api-client/
  config-eslint/
apps/
  app-a/
  app-b/

apps 按需依赖 @my/utils@my/api-clientconfig-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/uiworkspace:*

关键配置

  • 同上,packages + appsdocs"@my/ui": "workspace:*"
  • 文档站构建配置里保证能解析 packages/ui 的源码(通常 workspace 链接后没问题)。

工作流

改组件 → 跑 docs 的 dev,文档里实时看效果;发版时先发 @my/ui,再更新文档站里对版本的说明(若文档站自己也要发)。

6.4 全栈 monorepo(前后端同仓)

场景:前端 + Node 服务同仓,共享类型、常量或少量 utils,用同一套依赖管理。

推荐结构

复制代码
packages/
  types/
  shared-utils/
apps/
  web/
  api/          # Node 服务

apiweb 都依赖 @my/types@my/shared-utilsworkspace:*

关键配置

  • pnpm-workspace.yaml 包含 packages/*apps/*
  • package.jsonscripts 里分别 --filter web--filter api 跑 dev/build。

工作流

typesshared-utils,前后端同时生效;各自部署时只构建对应 app,公共逻辑通过 workspace 链进去。


只要你存在「多个包 + 互相依赖 + 要一起开发」的需求,workspace 就很值得上;上面四种可以组合,比如「组件库 + 多应用 + 文档站」一起做。


七、详细教程:从零搭一个 pnpm workspace

下面按步骤做一遍,每步会写操作、预期结果、常见报错与排查。路径、包名和上文保持一致,你照抄就能跑通。

7.1 环境准备

  • 安装 pnpm

    bash 复制代码
    npm install -g pnpm

    或用 Corepack(Node 16.9+):

    bash 复制代码
    corepack enable
    corepack prepare pnpm@latest --activate

    建议用 pnpm 8.x 或 9.x ,Node 18+ 更省心。

  • 校验

    bash 复制代码
    pnpm -v
    node -v

    看到版本号即成功。

7.2 初始化根项目

bash 复制代码
mkdir my-workspace && cd my-workspace
pnpm init

会生成根目录 package.json。编辑成类似:

json 复制代码
{
  "name": "my-workspace",
  "version": "1.0.0",
  "private": true,
  "packageManager": "pnpm@9.0.0"
}
  • private: true :根包不会被 pnpm publish 发出去,避免误发。
  • packageManager :锁死 pnpm 版本,配合 corepack enable 使用;可选但推荐。

7.3 配置 pnpm-workspace.yaml

项目根目录 新建 pnpm-workspace.yaml

yaml 复制代码
packages:
  - 'packages/*'
  - 'apps/*'
  • packages/*packages/ 下每个子目录(如 packages/uipackages/utils)都算一个 workspace 包。
  • apps/*:同理。
  • 只有被列出来的目录才会被 pnpm 当成 workspace 成员,才能被 workspace:* 引用。

预期 :保存后暂无输出;之后 pnpm install 时 pnpm 会扫描这些目录。

7.4 创建子包目录并初始化

bash 复制代码
mkdir -p packages/ui packages/utils apps/web

然后逐个初始化(Windows 用户 可用 PowerShell,mkdir -p 若不可用就分步 mkdir):

bash 复制代码
cd packages/utils && pnpm init && cd ../..
cd packages/ui   && pnpm init && cd ../..
cd apps/web      && pnpm init && cd ../..

Windows :若 mkdir -p 报错,可改为 mkdir packages\uimkdir packages\utilsmkdir apps\web 等分步创建;cd ../.. 在 PowerShell 中同样适用。)

每个子包会多一个 package.json。接下来改包名、入口、exports

packages/utils/package.json

json 复制代码
{
  "name": "@my/utils",
  "version": "0.0.1",
  "main": "index.js",
  "exports": {
    ".": "./index.js"
  }
}

packages/ui/package.json

json 复制代码
{
  "name": "@my/ui",
  "version": "0.0.1",
  "main": "index.js",
  "exports": {
    ".": "./index.js"
  }
}

apps/web/package.json

json 复制代码
{
  "name": "web",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "dev": "echo \"dev placeholder\"",
    "build": "echo \"build placeholder\""
  }
}
  • exports:现代 Node 和打包器都认,用来明确入口,避免多余文件被引用;对 ESM、TS 等更友好。
  • webdev/build 先占位,后面验证完 workspace 再换成真实命令。

7.5 用 workspace:* 做包间依赖

packages/ui/package.json 里加依赖 @my/utils

json 复制代码
{
  "name": "@my/ui",
  "version": "0.0.1",
  "main": "index.js",
  "exports": { ".": "./index.js" },
  "dependencies": {
    "@my/utils": "workspace:*"
  }
}

apps/web/package.json 里加依赖 @my/ui

json 复制代码
{
  "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 到项目根):

bash 复制代码
pnpm install

预期

  • 根目录出现 node_modules/pnpm-lock.yaml
  • packages/uiapps/webnode_modules 里会有 @my/utils@my/ui 的链接;
  • lockfile 里能看到对 workspace: 的解析,例如:
yaml 复制代码
packages:
  '@my/utils@workspace:*':
    resolution: { directory: packages/utils, type: directory }
  '@my/ui@workspace:*':
    resolution: { directory: packages/ui, type: directory }

(省略其他字段;实际 lockfile 还有 nameversion 等。)

若报 ERR_PNPM_NO_MATCHING_PACKAGE :检查 pnpm-workspace.yamlpackages 是否包含对应目录,以及子包 name 是否和依赖里写的一致。

7.7 根 package.json 里加批量脚本

根目录 package.json 增加:

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 包里执行同名 script。
  • pnpm -r --parallel :并行跑,适合 dev
  • pnpm --filter web run build :只对 web 包执行 build

Windows :若使用 PowerShell,scripts 里的双引号、&& 等和 Unix 略有差异,一般上述写法没问题;若遇解析错误,可改为 node 跑一小段脚本封装命令。

7.8 验证 workspace 链路

  • packages/utils/index.js 写:
js 复制代码
module.exports = { add: (a, b) => a + b };
  • packages/ui/index.js 写:
js 复制代码
const { add } = require('@my/utils');
module.exports = { add, hello: 'from ui' };
  • apps/web 里加个临时脚本验证。给 apps/web/package.jsonscripts 增加一行 "run:check",例如:
json 复制代码
"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)\""
}

保存后,在根目录执行:

bash 复制代码
pnpm --filter web run run:check

预期输出3 'from ui'

Cannot find module '@my/ui'

  • 确认在根目录 执行过 pnpm install
  • 确认 apps/webdependencies 里有 "@my/ui": "workspace:*"
  • 看看 apps/web/node_modules/@my 下是否有 ui 的链接。

ENOENT 等路径类错误:

  • 检查 packages/utilspackages/ui 是否有 index.js,以及 package.jsonmain / exports 是否指向它。

验证通过后,可以把 webdev / 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.jsonworkspaces :pnpm 官方推荐用 pnpm-workspace.yaml 定义 workspace,不用 workspaces 字段;若同时存在,以 pnpm-workspace.yaml 为准。

示例

yaml 复制代码
packages:
  - 'packages/*'
  - 'apps/*'
  - 'tools/*'

8.2 根目录 package.json

  • private: true :根包不发布,避免误 pnpm publish

  • packageManager :如 "pnpm@9.0.0",锁包管理器 + 版本;需 corepack enable

  • scripts :结合 pnpm -r--filter 做批量或定向执行(见 8.6)。

  • pnpm.overrides :强制某依赖在全 workspace 解析成指定版本。

    json 复制代码
    {
      "pnpm": {
        "overrides": {
          "lodash": "4.17.21"
        }
      }
    }

    装依赖时 pnpm 会按 overrides 解析,并反映在 lockfile;适合修安全漏洞、解决传递依赖冲突。

  • catalog(pnpm 9+) :在 pnpm-workspace.yaml 里定义(不是 package.json),子包用 catalog: 引用;见下方示例。

catalog 示例pnpm-workspace.yaml):

yaml 复制代码
packages:
  - 'packages/*'
  - 'apps/*'

catalog:
  react: ^18.3.1
  react-dom: ^18.3.1

子包 package.json

json 复制代码
{
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:"
  }
}

升级时只改 catalog 即可,所有用 catalog: 的包一起变。

8.3 workspace: 协议

  • workspace:* :用当前 workspace 里同名包任意版本 ,并链接到源码目录。开发联调默认用这个。

  • workspace:^workspace:~ :按 semver 匹配 workspace 内版本;发布时 pnpm 会把它替换成实际版本号 (如 1.0.0),所以发布到 npm 的包不会还带着 workspace:

  • 锁文件里的表现

    yaml 复制代码
    '@my/ui@workspace:*':
      resolution: { directory: packages/ui, type: directory }

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

日常开发 workspace:* 就够用 ;若你们有严格的 semver 约束再考虑 ^ / ~

8.4 pnpm-lock.yaml

  • 唯一 :整份 workspace 共用一个 lockfile,放在根目录。
  • 内容:锁住所有依赖(含 workspace 解析结果)的版本、完整性校验等。
  • 维护 :用 pnpm installpnpm 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_modules public-hoist-pattern[]=*eslint*
shamefully-hoist 全部提升,类似 npm true,易幽灵依赖,慎用
auto-install-peers 自动装 peerDependencies true
strict-peer-dependencies peer 未满足时报错 true
  • node-linker=hoisted:切回类 npm 扁平结构;兼容性好,但失去严格依赖。
  • public-hoist-pattern:只把匹配的包提升,例如 ESLint、Prettier 等工具常见需求;能窄就窄,减少幽灵依赖。
  • resolution-mode :依赖解析策略(如 lowest-direct);lockfile-include-tty 等可按需查文档。

示例(只提升部分工具):

ini 复制代码
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*

8.6 --filter 完整语法

--filter 用来限定 要对哪些 workspace 包执行命令,常与 pnpm -rpnpm 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

示例

bash 复制代码
# 只给 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 ,禁止 npmyarn,否则 lockfile 和链接会乱。
  • package.jsonpackageManager ,如 "pnpm@9.0.0"
  • 启用 Corepackcorepack enable;CI 里先 corepack enablepnpm install,保证版本一致。

九、和 npm / Yarn workspace 的简单对比

能力 npm workspaces Yarn workspace pnpm workspace
磁盘占用 高,多份拷贝 一般 低,store+硬链接
安装速度 一般 较快
node_modules 结构 扁平 扁平或 PnP 非扁平,.pnpm
幽灵依赖 易出现 默认严格,无
lockfile 格式 package-lock.json yarn.lock pnpm-lock.yaml
workspace 协议 workspace:* workspace:* workspace:*
配置方式 package.json workspaces package.json workspaces pnpm-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 buildpnpm publish -r --filter '@my/ui'
  • changesets
    • changeset 管理 version bumpchangelog
    • 流程大致:改代码 → pnpm changeset 选包、选版本类型、写 changelog → pnpm changeset version 更新版本号 → pnpm publish -r 发布。
      这样多包独立发版、可追溯,很常见。

10.2 任务编排:Turborepo / Nx

  • package.jsonbuilddev 等可以交给 TurboNx 跑:他们按依赖图做拓扑排序 ,只跑该跑的,且能做远程/本地缓存,加速 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.jsonworkspace: 协议、.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,避免权限、路径异常。

如果你有具体的目录结构或 package.json 想优化,可以贴出来,按你现在的项目一步步改也行。

相关推荐
小马_xiaoen2 小时前
WebSocket与SSE深度对比与实战 Demo
前端·javascript·网络·websocket·网络协议
摇滚侠2 小时前
html,生成一个五行五列的表格,第三列边框是红色,其余列边框是黑色
前端·html
GISer_Jing2 小时前
从工具辅助到AI开发前端新范式
前端·人工智能·aigc
美狐美颜SDK开放平台2 小时前
从抖音到私域直播:抖动特效正在重塑直播美颜sdk
前端·人工智能·第三方美颜sdk·视频美颜sdk·美狐美颜sdk
云飞云共享云桌面2 小时前
SolidWorks如何实现多人共享
服务器·前端·数据库·人工智能·3d
晚霞的不甘2 小时前
Flutter for OpenHarmony《智慧字典》 App 底部导航栏深度解析:构建多页面应用的核心骨架
前端·经验分享·flutter·ui·前端框架·知识图谱
h7ml2 小时前
电商返利系统中佣金计算的幂等性保障与对账补偿机制实现
服务器·前端·php
EndingCoder2 小时前
高级项目:构建一个 CLI 工具
大数据·开发语言·前端·javascript·elasticsearch·搜索引擎·typescript
RFCEO2 小时前
HTML元素+网页布局区块概念汇总表
前端·html·html编程基础课·html元素汇总表·html元素位置展示