工作区 Workspaces
本教程共 34 篇 · 第 13 篇 · 更新于 2026-08-06
本节目标:
- 理解 Workspaces 解决的场景:在一个仓库里开发多个相互独立的包(monorepo)。
- 掌握根
package.json里workspaces字段的写法,包括 glob 与负向排除。- 会用
workspace:协议在包之间互相引用,并理解发布时版本如何被替换。- 了解
--filter选择性安装/运行、依赖去重,以及用 Catalogs 统一管理多包版本。
13.1 什么是 Workspaces
Workspaces(工作区)是 npm 生态里的一套约定,Bun 也完整支持。它的核心诉求是:把多个彼此独立、却又相互依赖的包,放在同一个 Git 仓库里一起开发——也就是常说的 monorepo。
一个典型的工作区目录结构长这样:
<root>
├── README.md
├── bun.lock
├── package.json
├── tsconfig.json
└── packages
├── pkg-a
│ ├── index.ts
│ ├── package.json
│ └── tsconfig.json
├── pkg-b
│ ├── index.ts
│ ├── package.json
│ └── tsconfig.json
└── pkg-c
├── index.ts
├── package.json
└── tsconfig.json
根目录的 package.json 通过 "workspaces" 字段声明哪些子目录是工作区成员,按惯例放在 packages/ 下:
{
"name": "my-project",
"version": "1.0.0",
"workspaces": ["packages/*"],
"devDependencies": {
"example-package-in-monorepo": "workspace:*"
}
}
13.2 glob 与负向排除
workspaces 字段支持完整的 glob 语法,包括负向(排除)模式。这在”大部分都纳入、个别目录例外”时很方便:
{
"name": "my-project",
"version": "1.0.0",
"workspaces": ["packages/**", "!packages/**/test/**", "!packages/**/template/**"]
}
上面这行表示:把所有 packages/ 下的子目录都作为工作区,但排除各自的 test/ 与 template/ 目录。
13.3 用 workspace: 协议引用本地包
每个工作区都有自己的 package.json。当包 A 依赖同仓库里的包 B 时,不要从 npm 下载,而是用 workspace: 协议(可带版本范围)来指向本地包:
// packages/pkg-a/package.json
{
"name": "pkg-a",
"version": "1.0.0",
"dependencies": {
"pkg-b": "workspace:*"
}
}
运行 bun install 时,Bun 会把你本地的 packages/pkg-b 目录”安装”到 node_modules 里(而不是去 registry 拉取),从而让 pkg-a 直接引用本地的源码。如果 pkg-a 和 pkg-b 都依赖同一个第三方包,Bun 会把它**提升(hoist)**到根 node_modules,节省磁盘、减少版本碎片。
workspace: 协议支持几种写法,含义不同:
"workspace:*" -> 发布时替换为 pkg 当前版本,如 "1.0.1"
"workspace:^" -> 替换为 "^1.0.1"
"workspace:~" -> 替换为 "~1.0.1"
"workspace:1.0.2" -> 替换为指定的 "1.0.2"(即使当前版本是 1.0.1)
Note当你用
bun pm pack或发布包时,Bun 会自动把workspace:版本替换成目标包package.json里真实的版本号。这让”本地用源码、发布用版本”的体验非常顺滑,无需手动改来改去。
13.4 选择性安装与脚本:—filter / —workspaces
在 monorepo 里,你可能只想给某几个包装依赖,或只在这些包里跑脚本。用 --filter 即可:
# 给所有以 pkg- 开头、但排除 pkg-c 的工作区安装依赖
bun install --filter "pkg-*" --filter "!pkg-c"
# 也可以用路径表达,等价
bun install --filter "./packages/pkg-*" --filter "!pkg-c"
--filter 既能作用于 bun install,也能作用于 bun run(运行脚本)、bun outdated 等命令,让你精确控制作用范围;如果不加过滤、想对所有工作区统一跑脚本,用 --workspaces 即可。
实际工作中,--filter 用得最多的场景其实不是安装,而是跑脚本:
bun run --filter "pkg-a" build # 只构建 pkg-a
bun run --filter "@myorg/*" build # 构建 @myorg 作用域下的所有包
bun run --filter "*" test # 所有工作区都跑一遍 test
bun run --filter "./apps/web" dev # 按路径过滤,只启动 apps/web
过滤器既接受包名(子包 package.json 里的 name 字段,支持 * 通配与 ! 取反),也接受路径(以 ./ 开头)。两种写法可以混用;多个 --filter 之间是叠加关系——先用正向模式选出候选集合,再用负向模式从中剔除。这样你就能表达”除了这两个包之外全部构建”这类需求,而不必把包名一个个列出来。
13.5 依赖去重的好处
Workspaces 带来的几个实际收益:
- 代码可拆分成逻辑单元:包与包之间用
package.json的依赖字段显式声明关系,bun install自动把本地包接进来。 - 依赖去重:A 和 B 共享的第三方依赖会被提升到根
node_modules,既省磁盘,也减少”同一个包装了多个版本”的依赖地狱。 - 多包脚本编排:配合
--filter/--workspaces,一次性在多包之间运行 build、test、lint 等任务。
Tip官方文档给出的性能参考:Bun 在 Linux 上安装 Remix 的 monorepo 大约只要
500ms,相比npm install约快 28 倍、相比yarn install(v1)约 12 倍、相比pnpm install约 8 倍。大仓库里安装速度的优势同样明显。
13.6 用 Catalogs 统一版本
当很多包需要同一个依赖的同一版本时,重复书写既啰嗦又容易漂移。Bun 支持在根 package.json 里定义一份 catalog(目录),各工作区用 catalog: 协议引用它:
// 根 package.json
{
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"react": "^18.0.0",
"typescript": "^5.0.0"
},
"catalogs": {
"build": {
"webpack": "^5.0.0",
"babel": "^7.0.0"
}
}
}
}
// 子包 package.json
{
"dependencies": {
"react": "catalog:",
"webpack": "catalog:build"
}
}
catalog: 引用根目录的默认目录,catalog:build 引用名为 build 的命名目录。以后只要改根目录里的版本,所有引用它的包都会跟着更新——这在维护几十个包的 monorepo 时尤其省心。
Warningpnpm 的
catalog:协议在从 pnpm 迁移时会被保留(见第 12 章),但语义需以 Bun 文档为准。若你同时维护多套包管理器配置,建议以bun install实际生成/更新后的bun.lock为真相来源。
13.7 一个最小可运行的跨包示例
光看配置可能还不够直观,下面给出一组能直接落地的文件,演示”包 A 引用本地包 B”。
根 package.json 声明工作区:
{
"name": "my-monorepo",
"version": "1.0.0",
"workspaces": ["packages/*"]
}
packages/pkg-b/package.json——它是一个被依赖的库:
{
"name": "pkg-b",
"version": "1.0.0",
"module": "index.ts"
}
packages/pkg-a/package.json——它依赖本地 pkg-b:
{
"name": "pkg-a",
"version": "1.0.0",
"dependencies": {
"pkg-b": "workspace:*"
}
}
运行一次 bun install,Bun 会把 packages/pkg-b 这个本地目录接入 pkg-a 的 node_modules,于是 pkg-a 的代码里 import { ... } from "pkg-b" 就能直接解析到同仓库的源码,而不是去 registry 下载。你在 pkg-b 里改一行,pkg-a 立刻就能看到效果——这正是 monorepo 开发体验的核心:多包共享一份源码、一处修改全局生效,发布时再各自换成版本号。
Tip想确认某个工作区到底被解析到了哪里,可以配合
bun pm ls查看已安装包的来源与版本;怀疑链接关系不对时,删掉node_modules重新bun install通常能解决大部分”改了不生效”的问题。
13.8 常见坑与问答
工作区配置出问题时,报错信息往往不够直白。下面几个是新手最容易踩的。
坑一:子包漏写 name,或者两个子包重名。
Bun 是通过每个子目录 package.json 里的 name 字段来识别工作区成员的,workspaces 里的 glob 只负责”去哪里找”。漏写 name,这个目录就不会被当成工作区;两个子包重名,则会导致链接指向错误的目录。每个工作区包都必须有唯一的 name。
坑二:glob 写错,包根本没被纳入。
"packages/*" 只匹配一层子目录。如果你的结构是 packages/group/pkg-a 这样两层,它不会被选中,需要写成 "packages/*/*" 或 "packages/**"。不确定配置有没有生效时,跑一次 bun install 后用 bun pm ls 看看本地包有没有被链接进来,比盯着 glob 反复猜要快得多。
坑三:根包忘记标记 "private": true。
monorepo 的根 package.json 通常只是个容器,并不打算发布。习惯上应该把它标为私有,避免某次手滑把整个仓库发布到 registry 上去。
Q:monorepo 默认用哪种 linker?
新建的工作区项目默认使用 isolated(类似 pnpm 的严格隔离)。这意味着某个包只能引用它自己声明过的依赖,从根本上杜绝了”幽灵依赖”——即某个包能跑起来只是因为它的兄弟包碰巧装了同一个库,一旦兄弟包移除依赖它就莫名其妙挂掉。旧项目为了向后兼容会保持 hoisted。想切换用 --linker 或写进 bunfig.toml 都可以。
Q:monorepo 安装太慢有什么办法?
从 Bun v1.3.14 起,isolated 策略新增了实验性的全局虚拟存储:同一个包在全局的 links 目录里只落一份实体,各项目的 node_modules 用符号链接指过去。这样热安装(锁文件在、缓存在、只是 node_modules 被清空,也就是最典型的 CI 路径)时,每个包只需要一次 symlink(),而不是逐个文件拷贝。在 bunfig.toml 里打开即可:
[install]
linker = "isolated"
globalStore = true
它默认关闭且仍在实验阶段,请先在非关键流水线上验证。收益在 CI 场景最明显,记得把缓存目录一并做持久化。
Q:整个仓库是一份锁文件还是每个包一份?
只有根目录会生成一份 bun.lock,它覆盖所有工作区。所以无论你要给哪个子包增删依赖,都应该回到仓库根目录执行 bun install,并把根锁文件一起提交。在某个子包目录里单独安装,容易让锁文件与真实依赖图脱节,这也是 CI 里冻结安装失败的常见来源。
13.9 小结
- Workspaces 让你在单一仓库里管理多个独立包,适合 monorepo 场景。
- 根
package.json的workspaces字段支持 glob 与负向排除;包间用workspace:协议引用本地源码。 bun install会自动接入本地包并做依赖去重;--filter/--workspaces用于选择性安装与脚本编排。- 发布时
workspace:版本会被替换为真实版本;多包共享版本可用 Catalogs 统一管理。
下一章我们转向更进阶的依赖管理话题:全局缓存、生命周期脚本、overrides/resolutions 与为依赖打补丁。