引言
当组织内有多个前端应用(管理后台、官网、营销页),且它们共享组件库、工具函数、类型定义时,Monorepo 是公认的工程解。但 Monorepo + Vite 组合也有自己的坑:共享包是「源码」还是「产物」?依赖预构建如何避免重复?多应用并行构建如何隔离与缓存?
本文从 pnpm workspace 搭建讲起,覆盖共享包的两种形态(源码直引 vs 产物构建)、依赖预构建与 resolve.dedupe 去重、多应用构建隔离与 CI 缓存、以及一个完整的 Monorepo 项目结构模板。读懂本文,你将能自信地为一个多应用团队搭建可扩展的 Vite Monorepo。
前置:https://plumephp.com/vite-config-guide/(resolve/build 配置)与 https://plumephp.com/vite-build-optimization/(构建优化基础)。
目录
- 1. 为什么 Monorepo 适合 Vite 项目
- 2. pnpm workspace 搭建
- 3. 共享包:源码直引 vs 产物构建
- 4. 依赖预构建与去重
- 5. 多应用构建隔离与缓存
- 6. 典型 Monorepo 项目结构
- 7. 共享包的 HMR 联动
- 8. CI 中的 Monorepo 构建
- 9. 总结:Monorepo 的取舍与路径
- 延伸阅读
1. 为什么 Monorepo 适合 Vite 项目
1.1 痛点对比
| 方案 | 共享代码 | 版本同步 | 开发体验 |
|---|---|---|---|
| 多仓库(multi-repo) | 靠 npm 包发布 | 版本漂移 | 改包要发版 |
| 单仓多目录 | 硬编码拷贝 | 不可控 | 重复代码 |
| Monorepo | workspace 直连 | 同步锁定 | 源码联动 + HMR |
1.2 Vite 与 Monorepo 的契合
- 原生 ESM + 依赖预构建:对 workspace 内共享包的源码加载友好。
- 别名与 resolve 灵活:可让不同应用解析到共享包的源码或产物。
- 构建独立:每个应用有独立
vite.config.ts,互不干扰。
1.3 不适合的场景
- 团队边界强、技术栈分歧大的组织,Monorepo 会放大冲突。
- 共享包极度稳定、极少改动时,直接发 npm 包更省事。
2. pnpm workspace 搭建
2.1 初始化
mkdir my-monorepo && cd my-monorepo
pnpm init
# 创建 pnpm-workspace.yaml
2.2 pnpm-workspace.yaml
packages:
- 'apps/*' # 应用
- 'packages/*' # 共享包
2.3 创建应用与共享包
# 应用
pnpm create vite apps/admin --template react-ts
pnpm create vite apps/web --template react-ts
# 共享包(手动创建 packages/ui)
mkdir -p packages/ui/src
// packages/ui/package.json
{
"name": "@my/ui",
"version": "0.0.0",
"main": "src/index.ts", // 源码直引
"types": "src/index.ts",
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
2.4 在应用里引入共享包
pnpm --filter @my/admin add @my/ui@workspace:*
// apps/admin/src/App.tsx
import { Button } from '@my/ui'
3. 共享包:源码直引 vs 产物构建
3.1 两种形态对比
| 形态 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 源码直引 | main 指向 src/index.ts | 开发零构建、HMR 直接 | 生产需 Vite 转译源码 |
| 产物构建 | 先 pnpm build 生成 dist | 生产更稳 | 改包要先构建、HMR 延迟 |
3.2 源码直引时的配置
// apps/admin/vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
resolve: {
// 让 Vite 直接处理共享包源码中的 TS/JSX
alias: {
'@my/ui': '/packages/ui/src/index.ts',
},
},
build: {
commonjsOptions: {
include: [/packages/, /node_modules/],
},
},
})
3.3 共享包源码中的依赖
源码直引时,共享包的依赖需在应用侧可解析。pnpm 的 workspace 提升 + peerDependencies 能覆盖大部分场景;必要时用 resolve.dedupe 强制版本统一。
4. 依赖预构建与去重
4.1 预构建去重的意义
Monorepo 中,不同应用可能安装同一依赖的不同版本,导致:
同一库打多份 -> 体积膨胀
React 两份实例 -> hooks 状态断裂
4.2 resolve.dedupe
// 各应用 vite.config.ts
export default defineConfig({
resolve: {
dedupe: ['react', 'react-dom'],
},
})
4.3 optimizeDeps 与共享包
export default defineConfig({
optimizeDeps: {
include: ['react', 'react-dom', '@my/ui'], // 预构建共享包
exclude: ['@my/ui'], // 或排除以走源码
},
})
| 配置 | 场景 |
|---|---|
include | 明确告知哪些依赖要预构建 |
exclude | 让源码直引的共享包跳过预构建 |
dedupe | 强制统一依赖版本 |
5. 多应用构建隔离与缓存
5.1 各自独立构建
// 根 package.json
{
"scripts": {
"dev:admin": "pnpm --filter @my/admin dev",
"dev:web": "pnpm --filter @my/web dev",
"build:all": "pnpm -r --filter './apps/*' build"
}
}
5.2 构建产物隔离
apps/admin/dist/ # 管理后台产物
apps/web/dist/ # 官网产物
5.3 缓存策略
# 用 pnpm 的 build 缓存
pnpm --filter @my/ui build
# 或配合 CI 缓存 node_modules 与 vite 缓存
export default defineConfig({
cacheDir: '.vite-cache', // 自定义缓存目录(默认 node_modules/.vite)
})
6. 典型 Monorepo 项目结构
6.1 完整目录
my-monorepo/
├── apps/
│ ├── admin/ # 管理后台
│ │ ├── src/
│ │ └── vite.config.ts
│ └── web/ # 官网
│ ├── src/
│ └── vite.config.ts
├── packages/
│ ├── ui/ # 共享 UI 组件库
│ │ └── src/
│ ├── utils/ # 共享工具函数
│ └── types/ # 共享类型定义
├── pnpm-workspace.yaml
├── package.json
└── tsconfig.base.json # 共享 TS 配置
6.2 tsconfig.base.json 共享
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@my/ui": ["packages/ui/src/index.ts"]
}
}
}
6.3 共享包的构建配置(产物模式)
// packages/ui/vite.config.ts(库模式)
import { defineConfig } from 'vite'
import { resolve } from 'node:path'
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyUI',
fileName: (format) => `my-ui.${format}.js`,
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
})
7. 共享包的 HMR 联动
7.1 源码直引的天然 HMR
源码直引时,编辑共享包源码会触发引用它的应用热更新——这是 Monorepo 开发体验的核心优势。
7.2 用 link 加速(无需每次 pnpm install)
# 将共享包 link 到各应用
pnpm --filter @my/admin link @my/ui
# 或直接 workspace:* 依赖,pnpm 自动建立链接
7.3 HMR 边界
编辑共享包 .ts 文件 -> 应用到 Vite transform -> HMR 触发
注意:共享包的 CSS 文件、JSON 同样会联动
8. CI 中的 Monorepo 构建
8.1 并行构建
# .github/workflows/build.yml
name: Build Monorepo
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
# 共享包先构建
- run: pnpm --filter @my/ui build
# 应用并行构建
- run: pnpm -r --filter './apps/*' build
8.2 依赖关系感知的构建
# pnpm 支持按拓扑顺序构建
pnpm -r build
# 会先构建被依赖的包,再构建依赖方
8.3 缓存优化
- CI 缓存
node_modules(pnpm 默认) - 缓存 Vite 缓存目录
node_modules/.vite - 共享包产物缓存(无变化则跳过 rebuild)
9. 总结:Monorepo 的取舍与路径
9.1 一句话框架
workspace 连接(源码直引) -> 依赖去重(dedupe) -> 构建隔离(独立 config) -> CI 拓扑构建
9.2 关键决策点
| 决策 | 选择 | 理由 |
|---|---|---|
| 包管理器 | pnpm | workspace 原生支持 + 硬链接省空间 |
| 共享包形态 | 开发期源码直引 | HMR 最佳体验 |
| 共享包发布 | 产物构建(lib 模式) | 生产稳定、peer 清晰 |
| 依赖去重 | resolve.dedupe | 避免多实例与体积膨胀 |
9.3 自检清单
| 检查项 | 是否掌握 |
|---|---|
| 能搭建 pnpm workspace | ☐ |
| 能解释源码直引 vs 产物构建 | ☐ |
| 能配置 resolve.dedupe 去重 | ☐ |
| 能设计 CI 拓扑构建 | ☐ |
| 能处理共享包 HMR 联动 | ☐ |
延伸阅读
- https://plumephp.com/vite-config-guide/ — resolve/dedupe/optimizeDeps 详解
- https://plumephp.com/vite-build-optimization/ — 共享包产物与构建优化
- https://plumephp.com/vite-plugin-development/ — 为共享包写插件的扩展
- pnpm workspace 文档 — 官方 workspace 参考
- Vite 库模式文档 — 共享包构建配置
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。