前端 pnpm workspace 架构详解
一、先聊聊:我们到底遇到了啥问题?
做前端久了,多包、monorepo、组件库联调这些事一多,就会踩到一堆具体又磨人的坑。下面把这些痛点拆开说:具体表现 → 典型场景 → 对你有啥影响。搞清楚这些,后面再看 pnpm workspace 解决啥就一目了然。
pnpm workspace 是解决前端多包管理、Monorepo 依赖冲突及磁盘占用问题的方案。通过全局 store 硬链接机制节省空间,非扁平化 node_modules 杜绝幽灵依赖,配合 workspace:* 协议实现本地包源码直链联调。文章详解其底层原理、目录结构、配置方式(pnpm-workspace.yaml)、脚本执行(-r/--filter)及与 npm/Yarn 对比,并提供从零搭建工作区的实战步骤与常见场景应用建议。
做前端久了,多包、monorepo、组件库联调这些事一多,就会踩到一堆具体又磨人的坑。下面把这些痛点拆开说:具体表现 → 典型场景 → 对你有啥影响。搞清楚这些,后面再看 pnpm workspace 解决啥就一目了然。
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。
幽灵依赖的定义:某个包没有在你自己的 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,扁平化之后只能满足一边,另一边可能用了「不对」的版本,运行时才暴露问题,调试成本很高。
npm link 的坑典型场景:你维护一个业务组件库,要在另一个前端项目里联调。通常做法是 npm link:在组件库目录 npm link,在业务项目里 npm link your-components。但经常会遇到:
node_modules/.bin 找可执行文件,link 后路径解析不对,跑不起来。总之,改一下组件库就要反复 link、unlink、重装,体验很差,也容易忘步骤导致联调结果不可靠。
典型场景:每次 CI 全量 npm install,没有跨项目或跨 job 的 store 复用;缓存 key 设计不当(例如只按 package.json 不按 lockfile),导致缓存命中率低,每次都几乎全量装。加上前面说的 node_modules 巨大,流水线耗时长、占用空间大,体验和成本都不好。
上面这些,本质都可以归为两类问题:一是多包怎么组织、怎么一起开发、怎么发布(项目结构 + 工作流);二是依赖怎么存、怎么解析、怎么隔离(存储与解析策略)。pnpm 的 workspace 就是在这两方面同时发力的方案之一:多包管理 + 更合理的依赖存储与解析。下面先把你可能最关心的——pnpm 底层是怎么干的——讲清楚,再回头看 workspace 具体解决了啥。
很多人只记住结论:「pnpm 省磁盘、快、没幽灵依赖」,但不知道它到底咋做到的。这一节把存储模型和 node_modules 结构说透,你后面看配置、看优缺点都会更有数。
pnpm 有一个全局 store,所有安装过的包都会先放进这里,再通过硬链接挂到各个项目的 node_modules 里。
~/.local/share/pnpm/store~/Library/pnpm/store%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。node_modules/.pnpm/...,所以看起来每个项目都有一份,实际磁盘只存 store 里那一份。结果:同 monorepo、同样依赖,用 pnpm 时磁盘占用往往只有 npm 的一半左右(常见 benchmark 结论),二次安装时大量命中 store,pnpm install 明显更快。
node_modules 的真实结构:非扁平 + 严格依赖npm 会把依赖扁平化提升到顶层,所以你能「意外」用到子依赖;pnpm 不这么做,结构是非扁平的。
目录结构示意(精简版):
node_modules/:
dependencies / devDependencies 里的包)。node_modules/.pnpm/<pkg>@<version>/node_modules/<pkg>。node_modules/.pnpm/:
package@version 一个目录,且每个包有自己的 node_modules,里面只装它自己的依赖。node_modules,所以你没法在业务代码里 require('某个未声明的子依赖')。严格依赖就是这样实现的:
只有在 package.json 里显式声明的包,才会出现在你项目的 node_modules 顶层(或子包自己的 node_modules 里)。未声明的包根本不在你可访问的路径下,require / import 会直接报错,从根上杜绝幽灵依赖。
有些老旧工具会假设「所有依赖都在根 node_modules 扁平展开」,在 pnpm 默认结构下会找不到包。这时可以用 public-hoist-pattern 或 node-linker=hoisted 做有限提升,相当于在「兼容旧工具」和「严格依赖」之间做权衡;提升多了,幽灵依赖风险又回来了,所以能窄就窄。
当你在 package.json 里写 "@my/ui": "workspace:*" 时,pnpm 会:
pnpm-workspace.yaml 定义的目录里找到对应包(如 packages/ui);node_modules 里对应位置,不拷贝、不先打包。所以,你改 packages/ui 的源码,消费方(例如 apps/web)立即可见,不用 npm link,也没有双实例、路径错乱那些破事。这就是 workspace 协议 带来的「本地包即源码」的联调体验。
node_modules,省空间、安装快、默认严格依赖。差异主要在存储与解析策略,而不是「有没有 workspace」这个概念。
有了第二节的原理打底,这里直接说 workspace 在「多包管理」场景下,具体帮你解决了啥;每个点都往「能用、能查」上靠。
@my/utils、@my/ui 这种本地包,pnpm 只做链接到源码目录,不往 store 里塞,也不拷贝,改完即生效。pnpm install 在 monorepo 里通常比 npm install 快不少,尤其二次安装、CI 命中 store 时。public-hoist-pattern 有限提升,并清楚这会带来隐性依赖风险。pnpm-lock.yaml 在根目录,所有子包、所有环境的依赖解析都以它为准,版本全仓库一致,复现性高。pnpm-workspace.yaml 里定义 catalog,给常用依赖约定版本(如 react: ^18.3.1),子包用 catalog: 引用,升级时只改一处,避免各包各自为政。package.json 里可配 pnpm.overrides,强制某依赖在全 workspace 解析成指定版本,适合解决传递依赖冲突、安全修复等。pnpm install,所有 workspace 包依赖都装好;用 pnpm -r run build、pnpm --filter ... 批量或定向跑脚本,配合根 package.json 的 scripts,协作流程清晰。pnpm publish -r 可递归发布,结合 --filter 只发布改动的包;配合 changesets 做 version + changelog + publish,适合多包独立发版。下面是一个常见的 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:
scripts,用 pnpm -r、--filter 批量或定向执行子包的 build、dev、test。"private": true,不发布;可加 packageManager、pnpm.overrides 等。pnpm-workspace.yaml:
packages 数组声明哪些目录算 workspace 包(如 packages/*、apps/*),只有这些才能被 workspace:* 引用。package.json 的 workspaces 字段。pnpm-lock.yaml:
pnpm install 结果一致。packages/*:
package.json,通过 workspace:* 相互依赖或被 apps/* 依赖。apps/*:
packages/* 时用 workspace:*,改库即生效。有的项目还会加 tools/* 放脚本、CLI 等,本质上一样:在 pnpm-workspace.yaml 里写上对应 glob 即可。
apps?当你明确有「多个应用 + 共享 packages」时,拆开更清晰;只有一两个 app 时,全放 packages 也没问题,按团队习惯来。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 包,没有误用远端的。
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 默认不按拓扑序,按字母序);循环依赖:
若出现 A → B → C → A,依赖图成环,拓扑排序搞不定,pnpm 会报错;安装、-r 执行都可能挂。所以必须保证 workspace 内无环,设计时就要避免「包互相依赖」。
安装(pnpm install)
在根目录执行 pnpm install 时,大致会做这几步:
pnpm-workspace.yaml,得到所有 workspace 包目录(如 packages/*、apps/*)。package.json,建 name → 目录 映射,并算出整棵依赖树(含对 npm 包的依赖)。workspace:*:遇到 workspace:* 等,按 4.3 的规则匹配到本地包目录,不从 registry 拉包。node_modules 里(符号链接或 junction),不拷贝、不往 store 塞;改源码立即生效。node_modules/.pnpm 等位置。workspace:* 的解析结果)写入根目录的 pnpm-lock.yaml。所以:workspace 包只做链接,不占 store;占磁盘、耗时的主要是外部依赖,而它们仍走 store 复用。
打包 / 构建(pnpm -r run build)
构建不改依赖安装方式,只是按依赖图顺序跑各包的 build 脚本:
package.json 的依赖关系,得到有向图。graph-sequencer 的方式处理)。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 的调度与缓存,两者一起用没问题。
| 点 | 说明 |
|---|---|
| 省磁盘、安装快 | 全局 store + 硬链接,避免重复存包;workspace 包用链接,不复制。 |
| 依赖干净 | 严格依赖,无幽灵依赖;lockfile 唯一,版本一致。 |
| 本地联调友好 | workspace:* 直接链到源码,改即生效,无需 npm link。 |
| monorepo 友好 | 内建 workspace 支持,-r、--filter 过滤、并行跑脚本很方便。 |
| 易于做权限与发布 | 配合 pnpm publish -r、changesets 做按包发布、权限控制。 |
详细说明:
pnpm install 耗时明显下降。public-hoist-pattern 等,要控制范围,否则又引入隐性依赖。packages/ui 立刻在 apps/web 里生效,无需 link;注意跑 dev 的终端要在根目录或对应 app 目录,且已执行过根目录的 pnpm install。pnpm -r、--filter 能力足,再配合 Turborepo/Nx 做任务编排、缓存,体验更好。| 点 | 说明 |
|---|---|
| 和 npm 不完全兼容 | 部分工具假设「所有依赖扁平在根 node_modules」,可能报错,需适配。 |
| 学习与迁移成本 | 团队要搞懂 workspace、workspace:*、pnpm-workspace.yaml、--filter 等。 |
| 部分旧工具兼容性 | 极端老旧的构建/调试工具对 pnpm 的 node_modules 结构可能不友好。 |
| 需统一包管理 | 全 repo 必须用 pnpm,不能混用 npm/yarn,否则 lockfile、链接会乱。 |
详细说明:
node_modules 找包,pnpm 默认非扁平就可能找不到。处理办法:
**node-linker=hoisted**(.npmrc)切回类 npm 扁平结构,会牺牲严格依赖;public-hoist-pattern 把有问题的包提升上来,尽量窄配。pnpm-workspace.yaml、workspace:* 协议、根目录 pnpm install、--filter 与 -r 的用法。可以抽半小时过一遍本文 + 官方文档,再在试点项目跑一遍。npm install / yarn。用 **packageManager** 锁版本,CI 里 corepack enable && pnpm install,避免有人用错包管理器导致 lockfile 或链接关系错乱。适合:中大型前端项目、组件库 + 多应用、多包复用的 monorepo。
不大适合:单应用、没有多包复用需求的小项目;用 pnpm 单仓也能受益,但 workspace 收益有限。
下面按场景拆:谁用、解决啥问题、推荐结构、关键配置、日常工作流。你对照自己项目,能直接套用或微调。
场景:你们有一个业务组件库,要同时支撑 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/*']。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 的话)。
场景:多条产品线、多个前端应用,共享 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:* 或固定版本消费。
场景:组件库配套一个文档站(如 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,再更新文档站里对版本的说明(若文档站自己也要发)。
场景:前端 + 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 -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+ 更省心。
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 使用;可选但推荐。pnpm-workspace.yaml在项目根目录新建 pnpm-workspace.yaml:
packages:
- 'packages/*'
- 'apps/*'
packages/*:packages/ 下每个子目录(如 packages/ui、packages/utils)都算一个 workspace 包。apps/*:同理。workspace:* 引用。预期:保存后暂无输出;之后 pnpm install 时 pnpm 会扫描这些目录。
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 再换成真实命令。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 里的同名包,追踪源码」;装完依赖后会链接到对应包目录,改代码即时生效。
pnpm install务必在根目录执行(若不在根目录,先 cd 到项目根):
pnpm install
预期:
node_modules/、pnpm-lock.yaml;packages/ui、apps/web 的 node_modules 里会有 @my/utils、@my/ui 的链接;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 是否和依赖里写的一致。
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 跑一小段脚本封装命令。
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 相关配置 拆开讲:每项是啥、怎么配、适用场景、注意点。方便你以后查。
pnpm-workspace.yamlpackages:
'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/*'
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: 的包一起变。
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 约束再考虑 ^ / ~。
pnpm-lock.yamlpnpm install、pnpm add 等变更依赖,不要手改。pnpm-lock.yaml 纳入 git;CI 里 pnpm install --frozen-lockfile 可保证和 lockfile 完全一致,复现构建。.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 等可按需查文档。示例(只提升部分工具):
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
--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>。
node_modules 或 .pnpm 下,严格隔离。public-hoist-pattern:把匹配的包额外提升到根 node_modules,方便某些工具查找;提升范围越大,幽灵依赖风险越高。shamefully-hoist:几乎全部提升,和 npm 类似;不推荐,除非你只是临时兼容旧工具。对比:
node_modules 只有直接依赖,子依赖在 .pnpm 里,严格。node_modules 会出现被提升的包,未声明也可能被引用,所以要想清楚再开。npm、yarn,否则 lockfile 和链接会乱。package.json 设 **packageManager**,如 "[email protected]"。corepack enable;CI 里先 corepack enable 再 pnpm install,保证版本一致。| 能力 | 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:
何时继续用 npm / Yarn:
pnpm 的差异主要来自存储与解析策略,而不是「有没有 workspace」本身。
pnpm publish -r:递归发布所有 未 private 的 workspace 包;可加 --filter 只发改动的,例如先 pnpm -r --filter '@my/ui...' run build 再 pnpm publish -r --filter '@my/ui'。changeset 管理 version bump 和 changelog;pnpm changeset 选包、选版本类型、写 changelog → pnpm changeset version 更新版本号 → pnpm publish -r 发布。package.json 的 build、dev 等可以交给 Turbo 或 Nx 跑:他们按依赖图做拓扑排序,只跑该跑的,且能做远程/本地缓存,加速 CI 和本地构建。pnpm --help、pnpm install --help、pnpm add --help 等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 等。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 等用正斜杠或系统可识别的形式。scripts 报错,可试着用 node 写一小段脚本封装 pnpm -r / --filter 等命令,再在 scripts 里调该脚本。
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online