【架构】前端 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 -v node -v 

看到版本号即成功。

安装 pnpm

npminstall -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/uipackages/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\uimkdir packages\utilsmkdir 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 等更友好。
  • webdev/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 到项目根):

pnpminstall

预期

  • 根目录出现 node_modules/pnpm-lock.yaml
  • packages/uiapps/webnode_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 还有 nameversion 等。)

若报 ERR_PNPM_NO_MATCHING_PACKAGE:检查 pnpm-workspace.yamlpackages 是否包含对应目录,以及子包 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.jsonscripts 增加一行 "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/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 为准。

示例

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 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_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 -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

示例

# 只给 web 装 lodashpnpmadd lodash --filter web # 只给名字匹配 @my/* 的包跑 buildpnpm -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,如 "[email protected]"
  • 启用 Corepackcorepack enable;CI 里先 corepack enablepnpm 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 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 想优化,可以贴出来,按你现在的项目一步步改也行。

Read more

真寻机器人完整部署指南:从零搭建智能聊天助手

真寻机器人完整部署指南:从零搭建智能聊天助手 【免费下载链接】zhenxun_bot基于 Nonebot2 和 go-cqhttp 开发,以 postgresql 作为数据库,非常可爱的绪山真寻bot 项目地址: https://gitcode.com/GitHub_Trending/zh/zhenxun_bot 基于Nonebot2和go-cqhttp开发的绪山真寻机器人,是一款功能丰富的智能聊天助手。它采用PostgreSQL作为数据库,具备插件化架构和Web管理界面,能够满足各种聊天场景需求。本文将为你提供从环境准备到功能配置的完整实践指导。 项目架构与技术栈 真寻机器人采用了现代化的Python异步框架Nonebot2,配合go-cqhttp实现QQ平台对接。整个系统包含以下核心模块: * 机器人核心:基于Nonebot2的插件系统 * Web管理后台:可视化配置和监控界面 * 数据库层:PostgreSQL存储用户数据和配置信息 * 插件生态:支持功能扩展和自定义开发 如图所示,真寻机器人的Web管理界面提供了完整的监控功能,包括在线状态、资源使用情

x86-64 Memory Architecture and mov Instructions: Deep Dive into Addressing Mechanisms, Stack Operati

x86-64 Memory Architecture and mov Instructions: Deep Dive into Addressing Mechanisms, Stack Operati

本文为纯手打原创硬核干货,适合学习计算机组成、汇编、CSAPP 的同学,欢迎真实阅读、交流。 Based on the x86-64 architecture, this article starts with the matrix-based physical implementation of main memory, systematically breaks down the memory addressing mechanism, the family of data transfer instructions, and the logic of stack operations. It will help you fully grasp the underlying

万字长文:重点区域低空安全防御系统(反无人机)深度实战方案 | 从0到1构建立体安防体系(WORD)

万字长文:重点区域低空安全防御系统(反无人机)深度实战方案 | 从0到1构建立体安防体系(WORD)

摘要:随着低空经济爆发式增长,无人机"黑飞"已成为国家重点区域安防的重大威胁。本文基于真实政务项目案例,深度解析一套覆盖"探测-识别-定位-反制-溯源"全链条的低空安全防御系统建设方案。全文8000+字,涵盖TDOA无源定位、相控阵雷达、导航诱骗等核心技术,以及等保2.0合规、电磁频谱安全等实施细节,为安防系统集成商、智慧城市建设者提供保姆级技术参考。 一、项目背景与战略价值:低空经济背后的安全缺口 1.1 低空经济崛起的"双刃剑"效应 近年来,随着《"十四五"数字经济发展规划》的深入推进,低空经济已被纳入国家战略性新兴产业序列。无人机在物流配送、电力巡检、应急救援、城市测绘等领域的应用呈现爆发式增长。据统计,截至2025年初,我国民用无人机保有量已突破500万架,年飞行时长超过数千万小时。 然而,

LangBot:企业级即时通讯 AI 机器人平台 介绍篇

LangBot:企业级即时通讯 AI 机器人平台 介绍篇

LangBot:企业级即时通讯 AI 机器人平台 介绍篇 “专为企业打造的即时通讯 AI 机器人平台,无缝集成飞书(Lark)、钉钉、企业微信等企业通讯工具,与 Dify 等 AI 应用平台深度整合,让企业 AI 应用快速落地。” LangBot项目地址LangBot项目官网LangBot项目社区我的博客LangBot项目文档 LangBot是一款专为企业设计的开源 AI 机器人平台,立项于 2021 年中旬。它专注于帮助企业将 AI 能力无缝集成到现有的工作流程中,特别针对使用飞书(Lark)和 Dify 的企业用户,提供了完整的解决方案,让企业能够快速部署智能客服、知识库助手、工作流自动化等 AI 应用。 为什么企业选择 LangBot? 🏢 企业级功能设计 LangBot 从设计之初就考虑了企业级应用的需求,提供了完整的企业级功能: * 企业级安全:支持 SSO、