Bun workspaces、Monorepo、Turbo、Docker Compose 和 YAML 经常同时出现在现代项目架构中。本文不孤立罗列概念,而是从一个普通 Vue 项目逐渐成长为多应用系统的过程出发,串联讲清每项技术出现的背景、职责边界和协作关系。
刚接触前端时,我们通常只需要面对一个项目:
text
my-vue-app/
├── src/
├── public/
├── package.json
└── vite.config.js
安装依赖、启动开发服务器、打包上线,几个命令就够了:
bash
pnpm install
pnpm dev
pnpm build
但当我们开始阅读一些大型项目的技术方案时,画风会突然发生变化:
eg:项目采用 Bun workspaces 搭建 Monorepo,通过 Turbo 进行任务编排和缓存,再使用 Docker Compose 管理多个服务,配置文件统一采用 YAML。
短短一句话,却同时出现了 Bun、Workspace、Monorepo、Turbo、Docker Compose 和 YAML。
这些技术究竟是什么?为什么一个普通的 Vue 项目长大以后,会逐渐遇到它们?
这篇文章不会孤立地解释每个名词,而是从一个单体项目的成长过程出发,看看这些工具分别是在什么阶段出现、又在解决什么问题。
一、先从最熟悉的 Vue 项目开始
假设我们正在开发一个电商网站。
项目刚开始时,只有一个面向用户的 Vue 应用:
text
shop/
├── src/
├── package.json
└── vite.config.js
其中:
- Vue 负责页面和交互;
- Vite 负责开发服务器和构建;
- pnpm 负责安装依赖;
- Git 负责管理代码版本。
此时项目结构简单,一个仓库就是一个应用。
但随着业务发展,我们可能陆续增加:
- 用户商城;
- 商家管理后台;
- Node.js 后端接口;
- 定时任务;
- 消息通知服务;
- 公共 UI 组件;
- 公共工具函数;
- 公共数据类型。
如果每部分都建立一个独立仓库,项目很快就会变成这样:
text
shop-web
shop-admin
shop-api
shop-components
shop-utils
shop-types
这时会出现一系列协作问题。
例如,我们修改了一个公共用户类型:
ts
interface User {
id: string;
nickname: string;
avatar: string;
}
那么前端、管理后台和后端可能都需要同步升级。
如果这些代码分散在多个仓库中,就要经历:
text
修改公共包
↓
发布新版本
↓
通知其他项目升级
↓
分别更新依赖
↓
分别测试和发布
即使只是增加一个字段,也可能带来一连串版本同步工作。
为了让相互关联的项目更容易共同演进,我们便遇到了第一个概念:Monorepo。
二、Monorepo:把相关项目放回同一个仓库
Monorepo 可以近似读作:
/ˈmɒnoʊ ˌrepoʊ/,可读作"莫诺瑞破"。
它来自两个单词:
- Mono /ˈmɒnoʊ/:单一的;
- Repository /rɪˈpɒzətɔːri/:代码仓库。
Monorepo 是 Monolithic Repository 的简称,意思是:
使用一个代码仓库管理多个相互关联的应用和软件包。
于是,我们可以把刚才分散的项目整理成:
text
shop/
├── clients/
│ ├── web/
│ └── admin/
├── services/
│ ├── api/
│ └── notification/
├── packages/
│ ├── ui/
│ ├── utils/
│ └── types/
└── package.json
这正是很多工程方案中常见的三层结构:
text
clients/ 客户端应用
services/ 后端服务
packages/ 公共软件包
1. clients:客户端应用
客户端是用户能够直接看到或操作的应用,例如:
text
clients/
├── web/ 用户商城
├── admin/ 商家后台
└── desktop/ 桌面客户端
它们主要负责:
- 页面展示;
- 用户交互;
- 调用后端接口;
- 展示业务数据。
我们平时开发的 Vue、React 页面,一般都属于客户端。
2. services:服务进程
服务通常运行在服务器上,用户不会直接看到它们。
例如:
text
services/
├── api/ 业务接口
├── scheduler/ 定时任务
└── notification/ 消息通知
当用户点击"提交订单"时,完整过程可能是:
text
Vue 页面
↓ 发起 HTTP 请求
API 服务
↓
校验商品和库存
↓
创建订单
↓
写入数据库
↓
返回处理结果
其中真正处理订单的程序,就属于 services/。
3. packages:共享软件包
packages/ 用来存放多个应用共同依赖的代码:
text
packages/
├── ui/ 公共组件
├── utils/ 工具函数
├── types/ 数据类型
└── config/ 公共配置
例如,客户端和管理后台都需要格式化金额:
ts
export function formatPrice(price: number) {
return `¥${(price / 100).toFixed(2)}`;
}
这个函数就可以放在公共工具包中,而不是复制到两个项目里。
所以,Monorepo 的三层结构可以概括为:
text
clients 用户直接使用的程序
services 后台处理业务的程序
packages 多个项目共享的代码
不过,把代码放进同一个仓库,只完成了物理上的整理。
接下来我们还需要告诉包管理器:
哪些目录是独立项目?它们之间是什么关系?依赖应该怎么安装?
这就轮到 Workspace 出场了。
三、Workspace:让多个项目成为一个工作区
它的字面意思是"工作空间"或"工作区"。
在 Monorepo 中,Workspace 是一种多项目管理机制。它负责告诉包管理器:
- 哪些目录属于当前仓库;
- 每个目录是不是独立的软件包;
- 子项目之间如何互相依赖;
- 如何在根目录统一安装依赖。
例如:
text
shop/
├── clients/
│ ├── web/
│ │ └── package.json
│ └── admin/
│ └── package.json
├── packages/
│ └── utils/
│ └── package.json
└── package.json
根目录的 package.json 可以这样配置:
json
{
"name": "shop-monorepo",
"private": true,
"workspaces": [
"clients/*",
"services/*",
"packages/*"
]
}
这段配置相当于告诉包管理器:
clients、services和packages下面的子目录,都属于当前工作区。
子项目之间如何互相引用?
假设 clients/web 需要使用 packages/utils。
它可以在自己的 package.json 中声明:
json
{
"dependencies": {
"@shop/utils": "workspace:*"
}
}
这里的 workspace:* 表示:
不要去远程 npm 仓库下载,请直接使用当前工作区里的
@shop/utils。
这样,修改公共工具包后,客户端可以直接使用最新代码,不必每次都先发布一个 npm 版本。
Workspace 并不是 Bun 独有的
常见的 Workspace 实现包括:
- npm workspaces;
- Yarn workspaces;
- pnpm workspaces;
- Bun workspaces。
它们解决的是同一类问题,只是配置方式和实现细节有所不同。
四、Bun:为什么 JavaScript 又出现了一套新工具?
Bun 原本是"小圆面包"的意思,因此它的名字很短,也比较容易记。
不过,技术领域里的 Bun 是一套 JavaScript 和 TypeScript 工具链。
在 Bun 出现之前,一个常见的 JavaScript 项目可能需要组合很多工具:
text
Node.js 运行 JavaScript
npm/pnpm 安装依赖
Jest 执行测试
esbuild 打包代码
ts-node 直接运行 TypeScript
这套生态非常成熟,但工具比较分散。
Bun 的思路是:
能不能提供一个速度更快、整合程度更高的工具,覆盖 JavaScript 项目中的常见工作?
因此 Bun 同时提供了:
text
JavaScript/TypeScript 运行时
包管理器
测试运行器
脚本运行器
打包器
Workspace 管理
对应命令也比较统一:
bash
bun index.ts
bun install
bun add vue
bun test
bun run dev
bun build ./src/index.ts
Runtime 是什么?
中文一般翻译为"运行时"。
JavaScript 本身只是一套语言规范。要让一段 JavaScript 代码真正执行,还需要一个运行环境。
例如:
text
浏览器中的 JavaScript
↓
浏览器负责执行
服务器中的 JavaScript
↓
Node.js 或 Bun 负责执行
Node.js 和 Bun 都属于 JavaScript Runtime。
不过,两者使用的 JavaScript 引擎不同:
text
Node.js 主要使用 V8
Bun 主要使用 JavaScriptCore
V8 是 Chrome 背后的 JavaScript 引擎,JavaScriptCore 则来自 Safari。
Bun 在 Monorepo 中负责什么?
在本文这套架构里,Bun 主要负责:
text
识别 Workspace
安装项目依赖
运行 JavaScript/TypeScript
执行 package.json 脚本
例如,在仓库根目录执行:
bash
bun install
Bun 会识别所有 Workspace,然后统一处理它们的依赖。
这与我们熟悉的:
bash
pnpm install
很接近。
所以,不要因为项目使用了 Bun,就认为它采用了完全不同的编程语言。
开发者写的仍然是:
- JavaScript;
- TypeScript;
- Vue;
- React;
- Node.js 生态中的各种 npm 软件包。
Bun 更像是重新实现了一套运行和开发工具。
五、依赖管理解决了,为什么还需要 Turbo?
现在,我们已经把项目放进了 Monorepo,也用 Bun workspaces 管理了所有子项目。
但新的问题又出现了。
假设仓库中存在以下关系:
text
packages/types
↓
packages/utils
↓
services/api
↓
clients/web
clients/web 依赖后端提供的类型,后端又依赖公共工具。
那么构建时就不能随便执行。
正确顺序应该是:
text
1. 构建 packages/types
2. 构建 packages/utils
3. 构建 services/api
4. 构建 clients/web
如果仓库里有几十个项目,手动安排这些任务会越来越困难。
于是我们需要一个任务编排器。
六、Turbo:给整个仓库安排工作
Turbo 的音标是:
/ˈtɜːrboʊ/,读作"特尔博"。
这里的 Turbo 通常指 Turborepo。它是一个面向 JavaScript 和 TypeScript Monorepo 的构建系统。
Turbo 主要解决两个问题:
- 任务应该按照什么顺序执行?
- 哪些任务已经执行过,可以直接复用结果?
1. 任务编排
Orchestration 的音标是:
/ˌɔːrkɪˈstreɪʃən/,读作"奥克斯吹申"。
中文一般翻译为"编排"。
编排并不是执行某一个具体任务,而是管理多个任务之间的关系。
例如:
text
先构建公共包
↓
再构建后端
↓
最后构建客户端
如果两个项目互不依赖,Turbo 还可以并行执行:
text
clients/web build ─┐
├── 同时执行
clients/admin build ─┘
2. 缓存
缓存的目的,是避免重复工作。
假设第一次构建耗时如下:
text
公共包:10 秒
后端:20 秒
前端:30 秒
第二次构建时,我们只修改了前端代码。
Turbo 发现公共包和后端没有变化,就可以复用上一次的输出:
text
公共包:命中缓存
后端:命中缓存
前端:重新构建
这样就不必把所有项目重新构建一遍。
turbo.json 是什么?
Turbo 通常通过 turbo.json 配置任务:
json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {
"outputs": []
}
}
}
其中:
json
"dependsOn": ["^build"]
大致表示:
构建当前项目之前,先构建它所依赖的项目。
执行:
bash
bunx turbo build
Turbo 就会分析整个仓库,然后安排构建顺序。
七、Bun 和 Turbo 是不是做了同一件事?
它们确实有一点交集,但核心职责不同。
Bun 更关注:
text
依赖怎样安装
代码怎样运行
脚本怎样执行
Workspace 怎样管理
Turbo 更关注:
text
多个项目的任务先执行谁
哪些任务可以并行
哪些结果可以缓存
哪些项目受到了本次修改的影响
可以用装修团队来类比:
Bun 提供工人、材料和工具;Turbo 负责安排水电、木工和油漆的施工顺序。
因此:
bash
bun run build
通常表示运行某个项目的构建脚本。
而:
bash
bunx turbo build
表示让 Turbo 统筹整个 Monorepo 中的构建任务。
八、项目能在电脑上运行,不代表能稳定部署
完成开发以后,我们要把程序放到服务器上。
但不同电脑和服务器的环境可能完全不同:
text
开发电脑使用 Bun 1.x
服务器没有安装 Bun
开发电脑使用 PostgreSQL 17
服务器仍然是 PostgreSQL 14
开发电脑已经安装系统依赖
服务器缺少对应动态库
这就会产生一句程序员非常熟悉的话:
在我电脑上明明可以运行。
问题的根源是:代码虽然一样,但运行环境不同。
为了让开发、测试和生产环境尽量保持一致,我们会使用 Docker。
九、Docker:把应用和运行环境一起封装
Docker 可以把应用需要的代码、系统依赖和启动命令封装成一个标准化运行环境。
这个环境被称为 Container:
Container /kənˈteɪnər/,中文叫"容器"。
在传统部署中,我们可能先登录服务器,然后手动执行:
text
安装 Bun
安装系统依赖
复制代码
安装项目依赖
配置环境变量
启动服务
使用 Docker 后,可以把这些步骤写进 Dockerfile:
dockerfile
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
CMD ["bun", "run", "start"]
它描述了一个标准化过程:
text
1. 使用安装好 Bun 的基础环境
2. 创建应用目录
3. 复制依赖配置
4. 安装依赖
5. 复制项目代码
6. 启动应用
Image 和 Container 有什么区别?
可以这样理解:
text
Dockerfile 装箱说明书
Image 按说明书制作好的模板
Container 根据模板启动的运行实例
镜像本身是静态模板,容器才是真正运行的程序。
一个镜像可以启动多个容器:
text
API 镜像
├── API 容器 1
├── API 容器 2
└── API 容器 3
这也是服务横向扩容的基础之一。
十、为什么一个系统会有多个容器?
一个完整项目通常不只有一个程序。
例如电商系统可能包含:
text
前端应用
后端 API
PostgreSQL 数据库
Redis 缓存
消息队列
定时任务
Docker 推荐让每个容器承担相对明确的职责:
text
容器 1:Web 应用
容器 2:API 服务
容器 3:PostgreSQL
容器 4:Redis
容器 5:定时任务
但这样又会产生新的问题:
- 如何一次启动所有容器?
- 容器之间如何通信?
- 谁应该先启动?
- 数据库密码放在哪里?
- 每个服务使用哪个端口?
- 数据存储在哪个目录?
于是 Docker Compose 出现了。
十一、Docker Compose:一次组织多个服务
Compose 它有"组成、组合"的意思。
Docker Compose 用来描述和管理由多个容器组成的应用。
它通常读取:
text
compose.yaml
或者:
text
docker-compose.yml
例如:
yaml
services:
web:
build: ./clients/web
ports:
- "8080:80"
depends_on:
- api
api:
build: ./services/api
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:password@db:5432/shop
depends_on:
- db
- redis
db:
image: postgres:17
environment:
POSTGRES_DB: shop
POSTGRES_USER: app
POSTGRES_PASSWORD: password
volumes:
- database-data:/var/lib/postgresql/data
redis:
image: redis:8
volumes:
database-data:
执行:
bash
docker compose up
Docker Compose 就会根据配置启动整套环境:
text
Web 容器
API 容器
PostgreSQL 容器
Redis 容器
并为它们建立网络关系。
Docker 和 Docker Compose 的区别
text
Docker
负责构建和运行单个容器
Docker Compose
负责组织和启动多个容器
可以继续用团队做类比:
Docker 负责准备一个标准工作间;Docker Compose 负责让办公室、仓库、前台和后厨一起开工。
十二、YAML:为什么这些配置不是 JSON?
YAML 是为了解决复杂配置难以阅读和维护的问题而诞生的结构化数据格式,它用缩进代替大量括号,常用于 Docker Compose、Kubernetes、CI/CD、自动化流程和应用环境配置。
它与 JSON 表达的信息类似。
JSON 写法:
json
{
"server": {
"host": "localhost",
"port": 3000
}
}
YAML 写法:
yaml
server:
host: localhost
port: 3000
YAML 通过缩进表示层级,减少了括号和引号,因此比较适合人工阅读。当配置包含大量嵌套对象、列表和说明时,JSON/XML 括号与标签过多,导致人难以快速看清层级和修改内容的问题。
例如,要描述一个系统中的多个服务:
yaml
services:
web:
port: 8080
depends_on:
- api
api:
port: 3000
depends_on:
- database
它能清楚表达:
- 有哪些服务;
- 每个服务有哪些配置;
- 服务之间有什么依赖;
- 哪些内容属于同一层级。
YAML 需要特别注意缩进
下面是正确写法:
yaml
services:
api:
ports:
- "3000:3000"
如果层级缩进错误,含义就会改变,甚至无法解析。
因此使用 YAML 时需要注意:
- 使用空格缩进;
- 不要随意混用 Tab;
- 同一层级保持相同缩进;
- 字符串存在特殊含义时使用引号。
十三、把所有技术完整地串起来
现在,我们按照一个项目从开发到运行的顺序,把整套体系连接起来。
第一步:使用 Monorepo 组织代码
text
shop/
├── clients/
│ ├── web/
│ └── admin/
├── services/
│ ├── api/
│ └── notification/
└── packages/
├── ui/
├── utils/
└── types/
解决的问题是:
多个相关应用和公共包应该放在哪里?
第二步:使用 Bun workspaces 管理子项目
json
{
"workspaces": [
"clients/*",
"services/*",
"packages/*"
]
}
解决的问题是:
哪些目录属于当前工作区?依赖如何统一安装?内部包如何互相引用?
安装整个仓库的依赖:
bash
bun install
第三步:使用 Turbo 编排任务
bash
bunx turbo build
Turbo 分析项目依赖:
text
packages/types
↓
packages/utils
↓
services/api
↓
clients/web
解决的问题是:
多个项目应该按照什么顺序构建?哪些任务能够并行?哪些结果可以缓存?
第四步:使用 Docker 封装应用
text
clients/web/Dockerfile
services/api/Dockerfile
解决的问题是:
如何让应用在不同电脑和服务器上获得相对一致的运行环境?
第五步:使用 Docker Compose 启动整个系统
bash
docker compose up
解决的问题是:
如何统一启动客户端、API、数据库和缓存等多个服务?
第六步:使用 YAML 描述配置
yaml
services:
api:
build: ./services/api
db:
image: postgres:17
解决的问题是:
如何用一种方便阅读的格式描述服务、端口、环境变量和依赖关系?
十四、最终形成了怎样的系统?
从用户访问开始,系统可能这样运行:
text
浏览器
↓
clients/web
↓ HTTP 请求
services/api
├── PostgreSQL
├── Redis
└── notification
而从工程管理角度看:
text
Monorepo
负责组织整个代码仓库
↓
Bun workspaces
负责管理项目与依赖
↓
Turbo
负责任务编排与缓存
↓
Docker
负责封装单个服务
↓
Docker Compose
负责启动和连接多个服务
↓
YAML
负责描述配置
这些工具并不处于同一层面,也不是简单的竞争关系。
它们分别回答了不同问题:
| 问题 | 对应技术 |
|---|---|
| 多个项目放在哪里? | Monorepo |
| 多个项目如何统一管理? | Workspace |
| 谁负责安装依赖和运行代码? | Bun |
| 多个任务按什么顺序执行? | Turbo |
| 单个服务如何封装环境? | Docker |
| 多个服务如何统一启动? | Docker Compose |
| 配置使用什么格式描述? | YAML |
十五、普通前端开发者应该怎么学习?
看到一整套架构时,很容易产生一种误解:
是不是要把这些工具全部学会,才能开发现代项目?
其实没有必要。
如果你目前主要开发普通 Vue 项目,可以按照以下顺序理解。
第一阶段:理解单项目
先掌握:
package.json;- dependencies 和 devDependencies;
- npm、pnpm 或 Bun;
- Vite 的开发和构建流程;
- 环境变量;
- Git 基本操作。
第二阶段:理解多项目
然后学习:
- Monorepo;
- Workspace;
- 内部软件包;
- 项目之间的依赖关系;
- 公共组件和公共工具的拆分。
第三阶段:理解工程任务
继续学习:
- build;
- test;
- lint;
- typecheck;
- Turbo 的任务依赖和缓存。
第四阶段:理解运行环境
最后再学习:
- Dockerfile;
- 镜像与容器;
- Docker Compose;
- 端口与网络;
- Volume 数据持久化;
- 开发、测试和生产环境配置。
最好的学习方法不是一次记住全部名词,而是先看清每项技术解决的问题。
结语:工具是随着问题一起出现的
当我们只有一个 Vue 项目时,Monorepo、Turbo 和 Docker Compose 可能显得多余。
但当项目逐渐变成:
text
多个客户端
多个后端服务
多个公共包
多个开发团队
多套运行环境
新的工程问题就会自然出现:
text
代码如何组织?
依赖如何管理?
任务如何执行?
结果如何缓存?
环境如何统一?
服务如何启动?
配置如何维护?
Bun、Monorepo、Turbo、Docker Compose 和 YAML,并不是为了让项目看起来更加"高级",而是为了解决系统规模增长以后出现的真实问题。
因此,理解这套架构最重要的不是记忆命令,而是建立一条清晰的问题链:
Monorepo 管代码,Workspace 管项目,Bun 管依赖和运行,Turbo 管任务,Docker 管环境,Docker Compose 管服务,YAML 写配置。
当你能够说清楚每个工具为什么出现,就已经跨过了这些术语带来的第一道门槛。