先给结论:如果团队正同时维护多个前端应用,并且这些应用之间存在共享代码、共享规范、联动开发、统一交付的诉求,那么基于 pnpm Workspace + Turborepo 的 Monorepo 方案,通常是一次投入相对可控、收益比较明确的工程升级。
在实际业务里,我们遇到的往往不是"会不会搭 Monorepo",而是下面这些更具体的问题:
- 多个前端项目分散在不同仓库,公共代码一改要同步好几份;
- 组件库、工具库、类型定义各自维护,版本升级和联调成本高;
- 每个项目都要单独安装依赖、单独构建,CI 时间长,磁盘占用也高;
- 一次需求经常同时改应用和公共包,但多仓库下联动开发、联调、发布都很重。
我们这次落地 pnpm Workspace + Turborepo,核心目标并不是"追新架构",而是为了解决三个现实问题:降低重复维护成本、提升跨项目协作效率、让构建和发布流程更可控。
这篇文章不只介绍概念,更重点讲清楚三件事:
- 为什么在多应用场景下选择 Monorepo;
- 为什么依赖管理选
pnpm Workspace、任务编排选Turborepo; - 这套方案在日常开发、构建和 CI/CD 中具体解决了什么痛点。
一、为什么团队会从 Multirepo 走向 Monorepo
先说结论:Monorepo 不是为了统一仓库而统一仓库,而是因为多个项目之间已经存在真实的共享关系和联动关系。
如果一个团队同时维护 web、admin、h5、shared、ui 等项目,继续使用 Multirepo,常见痛点通常有:
- 公共逻辑分散在多个仓库,容易复制粘贴,后续逐步漂移;
- 组件库发布一个新版本后,业务项目还要逐个升级、逐个验证;
- 一次需求横跨多个仓库时,分支管理、联调和回归成本都会上升;
- 工程规范不统一,不同项目的 TypeScript、Lint、构建脚本容易越走越偏。
而 Monorepo 的价值就在于:把原本分散的"共享代码、共享规范、共享流程"拉回到一个仓库里统一治理。
单一仓库(Monorepo)架构,可以理解为:利用单一仓库来管理多个 packages 的一种策略或手段;与其相对的是多仓库(Multirepo)架构。
Monorepo 目录中除了会有公共的package.json依赖以外,在每个sub-package子包下面,也会有其特有的package.json依赖。
兄弟模块之间可以通过模块 package.json 定义的 name 相互引用,保证模块之间的独立性
csharp
monorepo-app
├── app/
│ ├─ app-A
│ │ ├─ src # 模块 a 的源码
│ │ ├─ node_modules # 模块 a 的 node_modules
│ │ └─ package.json # 仅模块 a 的依赖
│ └─ app-B
│ ├─ src # 模块 b 的源码
│ └─ package.json # 仅模块 b 的依赖
├── packages/
│ ├── shared-ui/ # UI 组件库
│ ├── shared-utils/ # 工具函数库
├── .eslintrc # 配置文件,对整个项目生效
├── node_modules # 所有子包公共的 node_modules
└── package.json # 所有子包公共的依赖
└── tsconfig.json # 所有子包公共的Typescript配置文件
└── tsconfig.base.json # 基础的Typescript配置文件
现有项目迁移到 Monorepo的步骤:
-
梳理依赖:列出所有项目的公共依赖/私有依赖,统计重复的工具函数/组件;
-
抽离共享包:将重复代码抽离为
packages/*(先抽工具库/类型,再抽UI组件); -
迁移应用:将原有项目移动到
apps/*,清理冗余依赖(删除已抽离到packages的代码); -
统一配置:将ESLint/TSConfig/prettier等配置提至根目录,子包继承;
-
验证链路:本地测试「共享包修改→应用热更」「全量构建」「单独构建某应用」;
-
适配CI/CD:修改Jenkinsfile/Dockerfile,适配Monorepo的构建逻辑;
-
灰度落地:先迁移1-2个核心应用,验证无问题后全量迁移。
二、方案选型:为什么是 pnpm Workspace + Turborepo
在前端界比较流行的 Monorepo 工具有 pnpm Workspaces、yarn Workspaces、npm Workspaces、rush、turborepo、lerna、yalc、和 Nx
实际落地时,不建议把这些工具放在同一个维度里比较,因为它们解决的问题并不完全一样。
pnpm Workspace更偏依赖管理和工作区组织;Turborepo更偏任务编排和构建提效;Lerna、Rush、Nx等则各自覆盖发布、治理、插件生态等不同能力。
我们这次之所以选择 pnpm Workspace + Turborepo,核心原因是:它足够轻量,但又能把 Monorepo 最核心的两个问题解决掉。
- 用
pnpm Workspace统一依赖安装、本地包引用和 workspace 管理; - 用
Turborepo统一构建顺序、增量缓存和并行执行; - 对已有前端项目改造成本相对可控,不需要一次性引入过重的治理体系。
强烈推荐使用pnpm Workspaces 作为 Monorepo 项目的依赖管理工具
Monorepo 与包管理工具(npm、yarn、pnpm)之间是一种怎样的关系?
这些包管理工具与 monorepo 的关系在于,它们可以为 monorepo 提供依赖安装、依赖管理与本地包关联的支持,借助自身对 workspace 的支持,允许在 monorepo 中的不同子包之间互相引用本地代码、共享依赖项,并提供统一管理这些共享依赖的方式,从而简化依赖管理与构建流程、提升开发效率。
三、为什么依赖管理优先选择 pnpm
从实践视角看,依赖管理工具选型的关键,不是"理论上谁更强",而是谁更适合多项目长期协作。
pnpm 的优势主要体现在下面几个方面:
- 安装成本更低:共享依赖复用更充分,多个项目一起维护时更省空间;
- 依赖边界更清晰:减少幽灵依赖,避免"本地没问题、线上出问题";
- workspace 体验更稳定:内部包通过
workspace:*直接联动,联调效率更高; - 更适合长期演进:项目越多、共享包越多,优势越明显。
pnpm 快、小、稳、无幽灵依赖,Workspace 设计最贴合 Monorepo 需求,是现代多包项目非常稳妥的选择。
| 对比维度 | npm / Yarn | pnpm |
|---|---|---|
| 依赖结构 | 扁平安装,易产生幽灵依赖 | 非扁平结构,无幽灵依赖,更严谨 |
| 磁盘占用 | 重复拷贝,Monorepo 下占用大 | 全局缓存 + 硬链接,空间极小 |
| 安装速度 | 较慢,依赖重复安装 | 极快,同版本只装一次 |
| Workspace 支持 | 基础可用,配置与行为较混乱 | 原生专为 Monorepo 设计,稳定可控 |
| 子包引用 | 软链接支持一般,易出问题 | 本地子包自动软链接,引用可靠 |
| 大型项目稳定性 | 依赖冲突、构建异常概率高 | 依赖关系清晰,更适合大型 Monorepo |
| 社区现状 | 早期 Monorepo 常用 | 现代 Monorepo 事实标准首选 |
幽灵依赖:项目未显式安装,但因扁平依赖结构能被引用的依赖,易导致版本不一致、部署报错
实际案例
| 架构类别 | node_modules | 源码 | 合计 |
|---|---|---|---|
| 传统多项目 | 项目A- 1480MB | 项目A- 310MB | 1790MB |
| 项目B- 1170MB | 项目B- 130MB | 项目B- 1300MB | |
| pnpm+Monorepo | 项目A- 链接119k | 项目A- 152.1MB | 项目A- 152.2MB |
| 项目A- 链接98k | 项目A- 74.8MB | 项目A- 74.9MB | |
| 共享依赖存储 | 1370MB |
以上统计为实际磁盘占用大小(节省约50%)
从结果来看,pnpm + Monorepo 带来的收益不只是"安装快一点",更重要的是:
- 团队不再为重复依赖和重复项目结构付出额外成本;
- 公共能力沉淀成共享包后,多个应用可以同步受益;
- 依赖问题和构建问题更容易收敛到统一的工程体系中解决。
四、这次改造带来了哪些具体变化
如果把这次改造理解成一次工程治理升级,那么它带来的变化可以概括为下面几项:
| 改造前 | 改造后 |
|---|---|
| 多个仓库各自维护依赖和脚本 | 统一在根目录管理依赖、脚本和工程规范 |
| 公共代码分散在业务项目里复制粘贴 | 公共能力沉淀到 packages,按包复用 |
| 一次需求跨仓库开发、联调、回归 | 应用和共享包在一个仓库内联动开发 |
| 构建顺序靠人维护,CI 容易全量执行 | 通过 Turbo 自动感知依赖并支持缓存、并行 |
| 版本同步、发包联调成本高 | 内部包通过 workspace:* 直接关联,减少重复发布 |
对团队来说,这些变化最终落到三个结果上:开发效率更高、协作链路更短、工程治理更统一。
五、推荐项目目录
bash
monorepo-app/
├── apps/ # 应用项目
│ ├── web/ # 主 Web 应用
│ ├── admin/ # 管理后台
│ ├── mobile/ # 移动端应用
│ └── docs/ # 文档网站
├── packages/ # 共享包
│ ├── ui/ # UI 组件库
│ ├── utils/ # 工具函数
│ ├── types/ # TypeScript 类型
│ ├── config/ # 共享配置
├── docs/ # 项目文档
├── pnpm-workspace.yaml # pnpm 工作空间配置
├── package.json # 根包配置
├── turbo.json # Turbo 构建配置
└── tsconfig.json # TypeScript 根配置
六、从零开始搭建Monorepo项目
这一部分虽然是"搭建步骤",但建议把它理解成一次工程治理过程,而不只是创建几个目录。
传统的多仓库 Multirepo 模式,通常是一个仓库存放一个项目。项目数量一多,最先暴露的问题不是代码写不动,而是:依赖升级重复做、规范很难统一、跨仓库联动改动成本高。
而单一仓库 Monorepo 模式,就是把多个相关项目放在一个仓库中统一管理。这些项目可以独立运行,也可以相互依赖。通过 Monorepo,多个项目可以共享依赖、共享配置、共享基础能力。比如多个项目都需要 lodash,那我们只需安装和维护一次即可。
我们本次选择pnpm Workspaces 作为 Monorepo 项目的依赖管理工具,本质上是在做三件事:
- 收拢分散的应用和共享包;
- 建立统一的依赖和规范入口;
- 为后续的任务编排、CI/CD 和持续演进打基础。
初始化项目
创建一个新的目录monorepo-app根目录下,运行pnpm init初始化,并创建package.json文件
然后根目录新建一个文件夹 packages,用于存储共享包
然后根目录新建一个文件夹 apps,用于存储应用
然后根目录新建一个文件tsconfig.base.json文件作为基础TS配置,后续应用或者共享包可以以此来继承
此时可以将 dependencies、devDependencies复制到外层 package.json 中当做公共依赖,然后pnpm install 安装一次即可
配置workspace工作空间
根目录新建一个 pnpm-workspace.yaml,将 packages 下所有的目录都作为包进行管理💥💥💥
bash
packages:
# 应用层
- 'apps/*'
# 共享包
- 'packages/*'
子包共享
此时,pnpm-workspace.yaml工作空间下的每个子包都可以共享我们的公共依赖了。还有个问题是,兄弟模块之间如何共享呢? 答案是,子包之间可以通过 **package.json** 定义的 **name** 相互引用。
首先一个完整的子包要包含以下内容
- 自己的依赖;
tsconfig.json- 测试与构建脚本;
- 与主仓库 ESLint、Prettier 一致的约束。
例如 /packages/ui/package.json:
perl
{
"name": "@demo/ui",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsc -w -p tsconfig.json",
"check": "tsc --noEmit -p tsconfig.json",
"clean": "rm -rf dist"
}
}
它既能作为独立 npm 包发布,也能被内部 app 实时引用。
这实现了 模块自治 + 内部共享生态。
- 如何把子包共享出去
首先我们来创建共享包,在packages下新建shared目录
常见目录
bash
packages/shared
├── package.json
├── tsconfig.json
├── src
│ └── index.ts
└── dist
src/index.ts:源码入口
dist/index.js:构建产物
package.json:包定义
tsconfig.json:本包构建配置
编辑共享包的**package.json**
perl
{
"name": "@demo/shared", // 包名,给应用引用时用
"version": "1.0.0",
"type": "module",
"main": "dist/index.js", // 运行时入口,指向编译后的 JS
"types": "src/index.ts", // 类型入口
"exports": { // 明确包对外暴露什么
".": {
"types": "./src/index.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.json", // 把 TS 编译成可运行代码
"dev": "tsc -w -p tsconfig.json",
"check": "tsc --noEmit -p tsconfig.json",
"clean": "rm -rf dist"
}
}
配置共享包的tsconfig.json
json
{
"extends": "../../tsconfig.base.json", // 继承自根目录下公共ts配置
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}
作用:
-
继承根配置
-
指定源码目录是
src -
指定产物目录是
disttsconfig.base.json示例(补充基础配置):json{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, // 根配置不输出产物,子包单独配置 "baseUrl": ".", "paths": { "@demo/*": ["packages/*/src"] // 全局路径映射,可选 } }, "exclude": ["node_modules", "**/dist"] }在
/src/index.ts中导出公共能力typescriptexport interface HealthPayload { app: string; status: 'ok'; timestamp: string; } export function formatAppMessage(appName: string, port: number): string { return `[${appName}] running on http://localhost:${port}`; } export function buildHealthPayload(appName: string): HealthPayload { return { app: appName, status: 'ok', timestamp: new Date().toISOString() }; } export function buildFeatureList(features: string[]): string { return features.map((feature, index) => `${index + 1}. ${feature}`).join('\n'); } export function buildFeatureListv2(features: string[]): string { return features.map((feature, index) => `${index + 1}. ${feature}`).join('\n'); } export function buildFeatureListv3(features: string[]): string { return features.map((feature, index) => `${index + 1}. ${feature}`).join('\n'); } -
共享包的对外 API 最好统一从
src/index.ts导出 -
应用只关心
@demo/shared,不直接引用内部文件路径
在应用中声明依赖(apps目录下的应用)
less
{
"dependencies": {
"@demo/shared": "workspace:*"
}
}
workspace:*表示依赖当前 monorepo 内部包- 包之间直接通过 workspace 链接
在应用的TS配置中建立路径映射
perl
{
"extends": "../../tsconfig.base.json", // 继承自根目录下公共ts配置
"compilerOptions": {
"paths": {
"@demo/shared": ["../../packages/shared/src/index.ts"]
}
}
}
作用:
- 提升开发体验
- 编辑器能正确跳转、提示和类型检查
- 本地开发时更容易直接定位到源码
构建共享包
css
pnpm --filter @demo/shared build
这一步会把:src/index.ts编译成:dist/index.js,你的应用运行时实际消费的是编译后的结果。
-
如何把ui组件包共享出去
重复上面的步骤即可
最终效果如图


最后完整的工作区目录
gomonorepo-app/ ├── apps/ │ ├── web/ │ │ ├── src/ │ │ ├── package.json │ │ └── tsconfig.json │ └── admin/ │ ├── src/ │ ├── package.json │ └── tsconfig.json ├── packages/ │ └── shared/ │ ├── src/ │ ├── package.json │ └── tsconfig.json ├── package.json ├── pnpm-workspace.yaml ├── turbo.json ├── tsconfig.base.json └── README.md -
公共依赖和独享依赖怎么安装
3.1 公共依赖
全局安装公共依赖 lodash。需要加-w(在工作空间的根目录中启动 pnpm)
pnpm install lodash -w
此时apps中两个应用都可以使用lodash依赖
3.2 局部依赖
如果只有 apps 应用层下某个应用用到来 lodash,我们也可以安装到该项目内部,不作为公共依赖项,有两种方法可以实现
第一种
cd 到 apps/app-A目录下,直接安装
pnpm install lodash
第二种
在任意目录下,使用 --filter 参数进行安装
css
pnpm install lodash --filter vue-demo1
结语
在 pnpm workspace的 monorepo 中,创建共享包的核心思路是:把公共能力抽离到 shared,通过 workspace:* 让多个应用共同依赖,再借助 TypeScript 编译,保证共享包在应用构建前完成产物生成。这样既能统一维护公共逻辑,也能避免重复开发和多项目之间的代码漂移。
七、为什么还需要 Turbo 做工程化调度
它是一个专门为 monorepo 设计的任务编排工具,使用Rust编写,传送门
你可以把它理解成:
- 不是包管理器
- 不是构建工具本身
- 而是"构建流程的调度器 / 指挥官"
它负责告诉多个项目:
- 先构建谁
- 后构建谁
- 哪些任务可以并行
- 哪些任务可以缓存
- 哪些任务没变就不用重复执行
Turbo = 用来管理 monorepo 中多个项目任务执行顺序、依赖关系、缓存和并行调度的工具。
跟pnpm的区别是:
pnpm是包管理器,负责安装依赖、管理workspace,链接本地包
Turbo是任务调度器,负责运行多个 package 的脚本并处理依赖关系
示例:
我们当前项目中有如下目录
- web
- admin
- shared
- ui
这些项目之间往往存在依赖关系:
web依赖sharedadmin依赖shared- 可能多个 app 还依赖
ui
如果没有 Turbo,你可能要自己手动控制:
- 先 build
shared - 再 build
ui - 再 build
web - 再 build
admin
而且每次都要判断:
- 哪些需要先执行
- 哪些可以跳过
- 哪些能并行跑
这很麻烦。
Turbo 的作用就是把这些事情自动化。
接下来看一下它的核心定义,我们拆开四个关键词来逐一分析
-
任务编排
Turbo 会管理多个 package 的脚本执行顺序。
例如:
根目录执行 turbo run build,它会去找每个 workspace 里的 build 脚本,按依赖关系决定执行顺序
-
依赖感知
它知道谁依赖谁。
例如:
web 依赖 @demo/shared,那么构建 web 前,Turbo 会先确保 shared 已经构建完成
json"dependsOn": ["^build"]这句话也是代表当前包执行build前,先执行它依赖的上游包的build,这里的
^可以理解成"依赖树上游"。 -
缓存
如果某个包的源码没变,Turbo 可以直接复用上一次结果,而不是重新跑一遍。
例如:
你只改了 web,shared 没变,那么 Turbo 可以跳过 shared 的重复构建,这样能显著提升 monorepo 的构建速度。
-
并行执行
如果两个任务彼此独立,Turbo 会尽量并行跑。
例如:
web 和 admin 都依赖 shared,当 shared 构建完成后,web 和 admin 的后续任务就可以并行 这比手工串行执行快得多。
接下来看一下实际的turbo.json
kotlin
{
"$schema": "https://turbo.build/schema.json", // 提高自动补全和校验体验,可选
// 定义要管理的任务
"tasks": {
"build": {
// 这是最重要的配置之一。当前包在执行 build 之前,先执行它依赖包的 build
"dependsOn": ["^build"],
// 告诉turbo构建产物在哪,用途主要是缓存
"outputs": ["dist/**"]
"env": ["NODE_ENV", "APP_ENV"] // 透传环境变量(CI/CD常用)
},
// 开发模式
"dev": {
"cache": false, // 是否缓存
"persistent": true // 用来标记一个长期存活的开发任务,比如本地开发服务器。
"inputs": ["src/**/*", "package.json"] // 监听的文件变更
},
// 跟build一样
"check": {
"dependsOn": ["^check"],
"outputs": []
},
// 这个任务本身就属于"重置环境"的动作,所以通常不缓存。删除dist,删除缓存,清理临时文件
"clean": {
"cache": false
}
},
"globalDependencies": [".env", "tsconfig.base.json"], // 全局依赖变更触发所有任务重新执行
"cacheDir": ".turbo/cache" // 自定义缓存目录(方便CI挂载)
}

从团队协作视角看,pnpm 解决的是"项目怎么组织",而 Turbo 解决的是"组织好之后,开发、构建、检查、发布怎么高效跑起来"。
如果没有 Turbo,随着应用和共享包增多,团队很快会遇到这些问题:
- 根目录命令越来越多,但执行顺序全靠人记;
- 改一个共享包,经常不知道哪些应用要先重建;
- CI 每次全量构建,耗时越来越长;
- 本地开发和流水线执行逻辑不一致,排查成本高。
所以引入 Turbo 的核心原因不是"炫技",而是为了把 Monorepo 从"能用"推进到"好用、稳定、可持续演进"。
八、日常开发常用命令和参数汇总
| 命令 | 作用 |
|---|---|
pnpm install |
安装整个 monorepo 依赖 |
pnpm dev |
启动所有开发任务 |
pnpm dev:web |
启动 web 应用 |
pnpm dev:admin |
启动 admin 应用 |
pnpm build |
构建整个 monorepo |
pnpm check |
统一类型检查 |
pnpm clean |
清理构建产物 |
pnpm --filter @demo/shared build |
单独构建 shared 包 |
pnpm --filter @demo/shared dev |
监听 shared 包 |
pnpm --filter @demo/ui build |
单独构建 ui 包 |
pnpm --filter @demo/ui dev |
监听 ui 包 |
pnpm --filter @demo/web build |
构建 web 应用 |
pnpm --filter @demo/admin build |
构建 admin 应用 |
pnpm preview:web |
预览 web 生产构建 |
pnpm preview:admin |
预览 admin 生产构建 |
| 参数 | 全称 | 作用 |
|---|---|---|
-w |
--workspace-root |
安装到根目录(公共依赖) |
--filter |
- | 对指定子包执行命令 |
--force |
- | 强制执行,忽略 Turbo 缓存 |
-D |
--save-dev |
安装为开发依赖 |
-v |
--version |
查看版本 |
九、CI/CD中结合AI的使用
当 Monorepo 真正落地后,团队很快会发现,收益不只体现在本地开发,还会体现在 CI/CD。
因为一旦应用、共享包、构建规则都被收拢到一个仓库里,部署流程也就有了统一治理的基础:同一套依赖安装逻辑、同一套任务编排方式、同一套交付标准。
目前可以采用Docker镜像部署
在当前的 Monorepo 项目中,部署方式采用了 Jenkins + Docker 的组合。整体思路可以概括为:代码仓库负责提供构建规则,Jenkins 负责调度流水线与注入凭据,Docker 镜像则作为最终的标准化交付物。相比直接拷贝前端构建产物到服务器,这种方式的优势在于流程更统一、镜像更易复用,也更方便后续扩展到多环境部署和自动化质量控制。
在代码仓库层,通常会维护 Dockerfile、.dockerignore 和 Jenkinsfile。其中 Dockerfile 用于定义镜像构建逻辑,例如基于 Node 镜像安装依赖、执行 pnpm build,再将构建后的静态资源复制到 Nginx 运行时镜像中;.dockerignore 用于减少构建上下文,避免 node_modules、dist 和 .git 等无关内容进入镜像构建过程;而 Jenkinsfile 则用于描述整个 CI/CD 流程,使流水线配置与代码一起纳入版本管理。对于 Monorepo 场景,还可以通过构建参数让一份Dockerfile 同时支持 web 与 admin 两个应用,从而减少重复维护成本。
例如,下面这段
Dockerfile 就体现了这种思路:先在构建阶段安装依赖并根据参数构建指定应用,再在运行阶段使用 Nginx 提供静态资源服务。
bash
FROM node:20-alpine AS builder
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json tsconfig.base.json ./
COPY apps ./apps
COPY packages ./packages
ARG APP_NAME=web
ENV CI=true
RUN pnpm install --frozen-lockfile
RUN if [ "$APP_NAME" = "web" ]; then \
pnpm --filter @demo/ui build && pnpm --filter @demo/web build; \
elif [ "$APP_NAME" = "admin" ]; then \
pnpm --filter @demo/ui build && pnpm --filter @demo/admin build; \
else \
echo "Unsupported APP_NAME: $APP_NAME" && exit 1; \
fi
FROM nginx:1.27-alpine AS runtime
ARG APP_NAME=web
COPY --from=builder /app/apps/${APP_NAME}/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
在 Jenkins 层,通常会创建一个参数化 Pipeline,用于控制构建哪个应用、使用哪个镜像标签、是否推送镜像到远端仓库。与此同时,Docker 仓库账号、AI 接口密钥等敏感信息不直接写入代码仓库,而是统一保存在 Jenkins Credentials 中,在流水线运行时按需注入。这种做法既能保持部署脚本的可维护性,又符合 CI/CD 场景下的安全最佳实践。
除了基础的构建和发布流程,实践中还可以加入 AI Review 作为进阶能力。它通常放在 Jenkins 流水线的检查阶段之后、镜像构建之前,主要负责分析本次提交的变更内容,并自动输出代码审查建议。对于 Monorepo 项目来说,AI Review 的价值尤其明显,因为它不仅可以提示代码本身的潜在问题,还能帮助识别共享包变更带来的影响范围,例如某次修改是否会波及 web 和 admin 两个应用,或者是否会引入构建和部署层面的兼容性风险。
一个简化后的 Jenkins 流水线片段可以写成下面这样:
typescript
pipeline {
agent any
parameters {
choice(name: 'APP_NAME', choices: ['web', 'admin'], description: 'Choose app to build')
booleanParam(name: 'RUN_AI_REVIEW', defaultValue: true, description: 'Run AI review before docker build')
}
stages {
stage('Install') {
steps {
sh 'pnpm install --frozen-lockfile'
}
}
stage('Check') {
steps {
sh 'pnpm check'
}
}
stage('AI Review') {
when {
expression { return params.RUN_AI_REVIEW }
}
steps {
sh 'node scripts/ai-review.mjs'
}
}
stage('Build Image') {
steps {
sh 'docker build --build-arg APP_NAME=${APP_NAME} -t demo/${APP_NAME}:latest .'
}
}
}
}
从工程角度看,AI Review 并不一定一开始就要作为强制门禁,更合理的方式是先让它生成审查报告,例如输出变更摘要、潜在风险点和建议测试项,再逐步演进到 PR 评论甚至质量门禁。这样既能发挥大模型在代码理解和影响分析上的优势,又能避免因为误判而影响团队的正常开发效率。
AI Review 极简示例
javascript
import { execSync } from 'child_process';
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// 获取本次提交的diff内容
const getGitDiff = () => {
try {
return execSync('git diff HEAD~1 HEAD', { encoding: 'utf8' });
} catch (e) {
return execSync('git diff', { encoding: 'utf8' }); // 未提交时取工作区diff
}
};
// 调用AI分析diff
const runAiReview = async () => {
const diff = getGitDiff();
if (!diff) {
console.log('无代码变更,跳过AI Review');
return;
}
const completion = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'system',
content: '你是前端Monorepo代码审查专家,分析以下代码变更,指出潜在问题(如类型错误、依赖使用不当、构建风险、跨包引用问题),并给出优化建议。'
},
{ role: 'user', content: `代码变更内容:\n${diff}` }
]
});
// 输出审查结果(可同步到PR评论/日志)
console.log('AI Review结果:\n', completion.choices[0].message.content);
};
runAiReview().catch(err => {
console.error('AI Review失败:', err);
process.exit(1);
});
总体来说,Jenkins + Docker + AI Review 可以看作是前端 Monorepo 工程的一种进阶化部署实践:Jenkins 负责流程编排,Docker 负责标准化交付,而 AI Review 则在传统 CI/CD 之上补充了自动化代码审查能力。这样的组合不仅提高了部署流程的规范性,也让整个工程体系在"构建、交付、质量保障"三个维度上更加完整。
十、什么场景值得做,什么场景不建议做
✅ 适合使用的场景:
- 多应用共享代码
比如多个项目共用 shared 工具包、ui 组件库、类型定义、接口封装。
- 需要统一工程规范
希望统一管理 TypeScript、Lint、测试、构建和 CI/CD 流程。
- 项目之间经常联动修改
一次需求会同时改 app 和公共包,放在一个仓库里更方便协同。
- 组件库和业务项目一起演进
适合设计系统、基础组件库和业务应用强绑定的场景。
- 团队想提升复用和协作效率
希望减少多仓库之间的版本同步和重复维护成本。
❌ 不适合的场景:
-
项目彼此独立
没有共享代码,也很少联动修改。
-
团队边界完全分开
不同团队各自维护、各自发布,放一起收益不大。
-
仓库会非常大,但缺少治理能力
没有 workspace、任务编排、统一规范时,容易变乱。
-
权限隔离要求高
不同项目需要严格控制代码可见范围时,不适合放在一个仓库。
-
发布节奏完全不同
每个项目独立演进、独立上线,多仓库可能更自然。
十一、常见问题与避坑
11.1 子包引用报错:找不到包 / 无法导入
原因
- 没有在
pnpm-workspace.yaml正确声明包路径 - 子包
package.json的name与引用名不一致 - 没有执行
pnpm install生成软链接 - 版本写死成
1.0.0,没写workspace:*
避坑
- 内部包必须用:
"@demo/shared": "workspace:*" - 改完配置一定跑:
pnpm install - 检查包名、路径、
exports配置是否正确
11.2 公共依赖 vs 局部依赖混乱
避坑
- 全局共用(react、vue、typescript、eslint)→ 根目录
-w安装 - 单个应用独有 →
--filter安装到对应包 - 不要把业务依赖提到根目录,会污染所有应用
11.3 修改了共享包,但应用没更新
原因
- 共享包没构建,应用读的是旧
dist - TypeScript 路径映射没配
- Turbo 缓存了旧产物
避坑
- 开发时共享包必须启动
dev(监听编译) - 应用
tsconfig.json必须配paths指向源码 - 怀疑缓存就执行:
pnpm clean或turbo run build --force
11.4 dev 模式热更失效 / 任务卡住
避坑
dev任务必须:"cache": false+"persistent": true- 不要同时用多个包监听同一个端口
- 共享包先
dev,再启动应用dev
11.5 目录越来越乱,分不清 apps/packages
黄金规则
apps应用层(web、admin、h5)packages共享层(ui库、config配置、types类型、utils)
十二、总结
这次落地 pnpm Workspace + Turborepo,本质上不是做一次简单的目录调整,而是一次前端工程协作方式的升级。
它主要解决了三类痛点:
- 代码复用难:共享包沉淀后,工具、类型、组件和配置可以统一维护;
- 跨项目协作重:应用和公共包在同一个仓库联动开发,减少来回发版和同步成本;
- 构建发布慢:
pnpm负责稳定依赖管理,Turbo负责增量、并行和缓存,整体交付效率更高。
当前前端项目的复杂度,已经不只是"写一个页面"这么简单,而是要面对多应用并行、共享能力沉淀、统一规范治理和持续交付提效。Monorepo 不是所有团队的标准答案,但对于多个项目长期协同演进的团队来说,它往往是更符合实际的工程化选择。