🧭 基于 pnpm Workspace 和 Turborepo 的 Monorepo 最佳实践

先给结论:如果团队正同时维护多个前端应用,并且这些应用之间存在共享代码、共享规范、联动开发、统一交付的诉求,那么基于 pnpm Workspace + Turborepo 的 Monorepo 方案,通常是一次投入相对可控、收益比较明确的工程升级。

在实际业务里,我们遇到的往往不是"会不会搭 Monorepo",而是下面这些更具体的问题:

  • 多个前端项目分散在不同仓库,公共代码一改要同步好几份;
  • 组件库、工具库、类型定义各自维护,版本升级和联调成本高;
  • 每个项目都要单独安装依赖、单独构建,CI 时间长,磁盘占用也高;
  • 一次需求经常同时改应用和公共包,但多仓库下联动开发、联调、发布都很重。

我们这次落地 pnpm Workspace + Turborepo,核心目标并不是"追新架构",而是为了解决三个现实问题:降低重复维护成本、提升跨项目协作效率、让构建和发布流程更可控。

这篇文章不只介绍概念,更重点讲清楚三件事:

  • 为什么在多应用场景下选择 Monorepo;
  • 为什么依赖管理选 pnpm Workspace、任务编排选 Turborepo
  • 这套方案在日常开发、构建和 CI/CD 中具体解决了什么痛点。

一、为什么团队会从 Multirepo 走向 Monorepo

先说结论:Monorepo 不是为了统一仓库而统一仓库,而是因为多个项目之间已经存在真实的共享关系和联动关系。

如果一个团队同时维护 webadminh5sharedui 等项目,继续使用 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的步骤:

  1. 梳理依赖:列出所有项目的公共依赖/私有依赖,统计重复的工具函数/组件;

  2. 抽离共享包:将重复代码抽离为 packages/*(先抽工具库/类型,再抽UI组件);

  3. 迁移应用:将原有项目移动到 apps/*,清理冗余依赖(删除已抽离到packages的代码);

  4. 统一配置:将ESLint/TSConfig/prettier等配置提至根目录,子包继承;

  5. 验证链路:本地测试「共享包修改→应用热更」「全量构建」「单独构建某应用」;

  6. 适配CI/CD:修改Jenkinsfile/Dockerfile,适配Monorepo的构建逻辑;

  7. 灰度落地:先迁移1-2个核心应用,验证无问题后全量迁移。

二、方案选型:为什么是 pnpm Workspace + Turborepo

在前端界比较流行的 Monorepo 工具有 pnpm Workspacesyarn Workspacesnpm Workspacesrushturborepolernayalc、和 Nx

实际落地时,不建议把这些工具放在同一个维度里比较,因为它们解决的问题并不完全一样。

  • pnpm Workspace 更偏依赖管理和工作区组织;
  • Turborepo 更偏任务编排和构建提效;
  • LernaRushNx 等则各自覆盖发布、治理、插件生态等不同能力。

我们这次之所以选择 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配置,后续应用或者共享包可以以此来继承

此时可以将 dependenciesdevDependencies复制到外层 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 实时引用。

这实现了 模块自治 + 内部共享生态。

  1. 如何把子包共享出去

首先我们来创建共享包,在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

  • 指定产物目录是 dist

    tsconfig.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中导出公共能力

    typescript 复制代码
    export 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,你的应用运行时实际消费的是编译后的结果。

  1. 如何把ui组件包共享出去

    重复上面的步骤即可

    最终效果如图

    最后完整的工作区目录

    go 复制代码
    monorepo-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
  2. 公共依赖和独享依赖怎么安装

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 依赖 shared
  • admin 依赖 shared
  • 可能多个 app 还依赖 ui

如果没有 Turbo,你可能要自己手动控制:

  • 先 build shared
  • 再 build ui
  • 再 build web
  • 再 build admin

而且每次都要判断:

  • 哪些需要先执行
  • 哪些可以跳过
  • 哪些能并行跑

这很麻烦。

Turbo 的作用就是把这些事情自动化。

接下来看一下它的核心定义,我们拆开四个关键词来逐一分析

  1. 任务编排

    Turbo 会管理多个 package 的脚本执行顺序。

    例如:

    根目录执行 turbo run build,它会去找每个 workspace 里的 build 脚本,按依赖关系决定执行顺序

  2. 依赖感知

    它知道谁依赖谁。

    例如:

    web 依赖 @demo/shared,那么构建 web 前,Turbo 会先确保 shared 已经构建完成

    json 复制代码
    "dependsOn": ["^build"]

    这句话也是代表当前包执行build前,先执行它依赖的上游包的build,这里的 ^ 可以理解成"依赖树上游"。

  3. 缓存

    如果某个包的源码没变,Turbo 可以直接复用上一次结果,而不是重新跑一遍。

    例如:

    你只改了 web,shared 没变,那么 Turbo 可以跳过 shared 的重复构建,这样能显著提升 monorepo 的构建速度。

  4. 并行执行

如果两个任务彼此独立,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 同时支持 webadmin 两个应用,从而减少重复维护成本。

例如,下面这段

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 的价值尤其明显,因为它不仅可以提示代码本身的潜在问题,还能帮助识别共享包变更带来的影响范围,例如某次修改是否会波及 webadmin 两个应用,或者是否会引入构建和部署层面的兼容性风险。

一个简化后的 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 组件库、类型定义、接口封装。

  • 需要统一工程规范

希望统一管理 TypeScriptLint、测试、构建和 CI/CD 流程。

  • 项目之间经常联动修改

一次需求会同时改 app 和公共包,放在一个仓库里更方便协同。

  • 组件库和业务项目一起演进

适合设计系统、基础组件库和业务应用强绑定的场景。

  • 团队想提升复用和协作效率

希望减少多仓库之间的版本同步和重复维护成本。

❌ 不适合的场景:

  • 项目彼此独立

    没有共享代码,也很少联动修改。

  • 团队边界完全分开

    不同团队各自维护、各自发布,放一起收益不大。

  • 仓库会非常大,但缺少治理能力

    没有 workspace、任务编排、统一规范时,容易变乱。

  • 权限隔离要求高

    不同项目需要严格控制代码可见范围时,不适合放在一个仓库。

  • 发布节奏完全不同

每个项目独立演进、独立上线,多仓库可能更自然。

十一、常见问题与避坑

11.1 子包引用报错:找不到包 / 无法导入

原因

  • 没有在 pnpm-workspace.yaml 正确声明包路径
  • 子包 package.jsonname 与引用名不一致
  • 没有执行 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 cleanturbo 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 不是所有团队的标准答案,但对于多个项目长期协同演进的团队来说,它往往是更符合实际的工程化选择。

相关推荐
广州华水科技1 小时前
单北斗形变监测一体机在大坝安全监测中的应用与技术优势
前端
老末1 小时前
ATP|搭建Docker+Flask+mysql框架
架构
传说之后1 小时前
Go 网络编程:从 TCP 字节流到自定义协议设计
后端·架构
沙漠1 小时前
Vue总结系列一
前端
渐儿1 小时前
React Native 实操开发文档
前端
Nturmoils1 小时前
书签真正难的不是收藏,而是找回来:我是怎么做这个 Chrome 插件的
javascript·后端·浏览器
HYCS1 小时前
用pixijs实现fabricjs(三):对象继承链和自定义对象
前端·javascript·canvas
biubiubiu_LYQ1 小时前
萌新小白基础篇之JS预编译
javascript
渐儿1 小时前
Electron 实操开发文档
前端