从一个 Vue 项目到一套工程体系:Bun、Monorepo、Turbo 与 Docker 到底在解决什么?

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/*"
    ]
}

这段配置相当于告诉包管理器:

clientsservicespackages 下面的子目录,都属于当前工作区。

子项目之间如何互相引用?

假设 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. 任务应该按照什么顺序执行?
  2. 哪些任务已经执行过,可以直接复用结果?

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 写配置。

当你能够说清楚每个工具为什么出现,就已经跨过了这些术语带来的第一道门槛。

相关推荐
布兰妮甜20 小时前
暗黑模式一键切换完整方案(CSS 变量 + 本地存储)
javascript·css·web开发·用户体验·前端工程化
布兰妮甜1 天前
原子化 CSS 深度解析:Tailwind 原理、自定义配置、大型项目利弊
css·性能优化·tailwind·前端工程化·设计系统
labixiong2 天前
Rspack 2.0 正式发布 — 前端构建工具格局再变天
webpack·前端工程化·turbopack
Flynt2 天前
我用Biome替换ESLint+Prettier在项目上踩了不少坑
rust·eslint·前端工程化
Revolution613 天前
页面更新后为什么出现 Loading chunk failed:旧页面如何请求了已删除的构建产物
前端·面试·前端工程化
Revolution613 天前
首屏变慢后,应该先查资源下载、脚本执行还是接口请求
前端·性能优化·前端工程化
AINative软件工程3 天前
LLM 应用的 Eval 数据工程实践:Golden Set 构建、版本管理与生产 Trace 回放
llm·测试·前端工程化
至乐活着4 天前
Vite 构建工具原理解析与实战:从 ES Module 到极速 HMR
vite·热更新·构建工具·前端工程化·es module
京东云开发者4 天前
拆解海博 AI-Native 落地保障:Harness、双 Loop、知识库与技能自主迭代实践
llm·ai编程·前端工程化