monorepo架构设计方案
目录
- 一、理论知识
- 1. 定义
- 2. 优势
- 3. 解决了什么问题(传统架构痛点)
- 二、实现一个简单的monorepo项目
- 1. 项目目录结构
- 2. 安装pnpm
- 3. 用pnpm在根目录工作区安装vite
- 4. 给子项目安装子包工具
- 5.在子项目运行
- 6.在根目录运行
- 三、最佳实践
- 1. 项目结构设计
- 2. 关键配置文件详解
- 3. 工作流命令详解(PNPM 核心命令)
- 4. 高级工作流
- 5.补充:现代 Monorepo 工具链
- 四、常见问题解决方案
- 1. 循环依赖问题:
- 2. 幽灵依赖问题:
- 3.TypeScript 跨项目引用:
- 4. Docker 多阶段构建:
一、理论知识
1. 定义
英文为 Monorepo (Monolithic Repository),译为单一代码仓库,是一种项目管理策略:
- 核心特征:多个相关项目/包存储在同一个代码仓库中
- 本质:工程化架构方案,解决多项目管理问题
- 对比:传统 Multirepo(多仓库) vs Monorepo(单仓库)
2. 优势
- 统一项目结构:混合式管理,所有工程形成有机整体
- 技术栈统一:团队基建、业务项目、子项目保持一致性,提高代码的复用性和团队协作的便捷性
- 高效共享机制:规范化、自动化、流程化项目间共享
- 依赖管理:依赖提升(Dependency hoisting),版本一致性保证,避免重复安装
- 统一开发体验:单点代码提交,全局代码搜索,统一CI/CD流程
3. 解决了什么问题(传统架构痛点)
问题维度 | 传统架构(Multirepo) | Monorepo解决方案 |
---|---|---|
项目结构 | 独立仓库,碎片化管理 | 统一仓库,结构化组织 |
技术栈 | 各项目独立,升级困难 | 统一技术栈,平滑升级 |
代码复用 | 复制粘贴或私有包,效率低下 | 直接引用,实时同步 |
依赖管理 | 版本冲突,重复安装,磁盘膨胀 | 依赖提升,单实例安装 |
协作效率 | 跨仓库修改困难,MR流程复杂 | 单仓库修改,原子提交 |
工具链 | 各项目独立配置,维护成本高 | 统一配置,集中管理 |
二、实现一个简单的monorepo项目
1. 项目目录结构
|- monorepo(项目名字)|- apps(子项目目录)|- project1(子项目1目录)|- package.json(配置文件)(见下方代码)|- index.html(入口文件)|- src(源码目录)|- index.js|- project2(子项目2目录)|- packages(公共工具包/组件库目录)|- ui(UI组件库目录)|- tools(工具组件库目录)|- package.json(配置文件)(见下方代码)|- index.js(入口文件)|- src(源码目录)|- date.js|- cli(脚手架工具目录)|- package.json(根目录配置文件)(见下方代码)|- pnpm-workspace.yaml(pnpm配置文件)(见下方代码)
根目录package.json
{"name": "monorepo", // 项目的名字,建议使用 kebab-case(短横线分隔)【推荐命名:<组织名>-<项目名>】"version": "1.0.0", // 如果根包不发布,这个字段也不是必须的"private": true, // 表示这个包是私有的,不会被发布到npm(或其他包管理器)上。在monorepo的根目录设置`"private": true`是一个最佳实践,因为根目录通常不是要发布的包,而是包含多个子包的工作区。"scripts": { // 一些npm/pnpm脚本"dev": "vite"},"devDependencies": {"vite": "^7.1.1"}
}
# pnpm-workspace.yaml
packages:- 'packages/*' # 子包存放路径- 'apps/*' # 应用存放路径
在子包的package.json中设置入口main,这里以monorepo/packages/tools举例
# package.json
{"name": "@monorepo-demo/tools","private": true,"main": "index.js", #入口文件"version": "1.0.0"
}
date.js
function getNow(){return new Date();
}export {getNow,
}
apps/project1/package.json
{"name": "@monorepo-demo/project1","private": true,"version": "1.0.0","scripts": {"dev": "vite"},
}
2. 安装pnpm
通过npm全局安装
- 确保已安装Node.js(版本≥18.12)。
- 执行命令:npm install -g pnpm
- 验证安装:pnpm --version
为什么使用pnpm?
特性 | 说明 |
---|---|
软链接机制 | node_modules 中通过符号链接直接指向工作区包,创建虚拟存储,避免文件复制,节省磁盘空间。 |
依赖缓存 | 下载过的依赖会存储在全局存储中,其他项目再次安装时直接硬链接,极快。 |
原生 Workspace | 内置 workspace: 协议,轻松链接本地包。 |
磁盘空间优化 | 相同依赖只存储一份,大幅减少磁盘占用。 |
依赖隔离 | 避免幽灵依赖(未声明却能引用)和非法访问(隔离 node_modules 结构)。 |
总结:pnpm 通过中心化存储和软链接,完美解决 Monorepo 中依赖复用和隔离问题。
3. 用pnpm在根目录工作区安装vite
pnpm add -w -D vite
然后根目录的node_modules就有了vite
4. 给子项目安装子包工具
cd进入子项目project1中,执行:
pnpm add @monorepo-demo/tools --workspace
workspace:工作区中
@monorepo-demo/tools:子包的package.json中定义的名字
然后我们子包的package.json就更新为:
同时node_modules中就出现了我们的工具子包库。
5.在子项目运行
在project1目录下,运行npm run dev(需要如上配置script)可以成功运行:
6.在根目录运行
在根目录下,运行
pnpm run --F @monorepo-demo/project1 dev
# -- F时filter筛选的意思
成功运行,和上面一样的结果。
三、最佳实践
1. 项目结构设计
monorepo/
├── apps/ # 应用项目
│ ├── web-app/ # 前端应用
│ └── mobile-app/ # 移动端应用
├── packages/ # 共享包
│ ├── core/ # 核心工具库
│ ├── ui/ # UI组件库
│ └── configs/ # 共享配置(ESLint/Prettier等)
├── package.json # 根配置
├── pnpm-workspace.yaml # 工作区配置
└── turbo.json # 任务编排配置(可选)
2. 关键配置文件详解
packages:- 'apps/*' # 应用项目- 'packages/*' # 共享包- 'internal/*' # 内部工具(可选)
根目录package.json
{"name": "company-monorepo", // 推荐命名:<组织名>-monorepo"version": "1.0.0","private": true, // 防止意外发布"scripts": {"dev": "turbo run dev", // 使用任务编排工具"build": "turbo run build"},"devDependencies": {"turbo": "^2.0.0", // 推荐添加任务编排工具"typescript": "^5.0.0"},"packageManager": "pnpm@8.15.0" // 锁定包管理器版本
}
3. 工作流命令详解(PNPM 核心命令)
pnpm add -wD <pkg> # 安装到根目录,共享开发依赖(Vite/ESLint)
pnpm --filter <package> add <pkg> # 指定包安装,给特定子包添加依赖
pnpm add workspace:<package> # 链接本地包,项目引用共享包
pnpm run --parallel <script> #并行执行,同时启动多个应用
pnpm exec -- <command> # 执行命令,运行全局工具
4. 高级工作流
- 全局启动所有应用
# 使用 TurboRepo 并行执行
pnpm turbo run dev# 纯 PNPM 方式
pnpm run --parallel --recursive dev
- 过滤特定项目
# 启动 web-app 项目
pnpm --filter @company/web-app dev# 启动所有以 "admin-" 开头的项目
pnpm --filter "admin-*" dev
5.补充:现代 Monorepo 工具链
-
任务编排:
Turborepo:增量构建,云缓存
Nx:智能任务调度,依赖图可视化 -
变更管理:
Changesets:自动化版本管理和 CHANGELOG
Lerna:传统 Monorepo 发布工具 -
开发环境:
Vite:极速开发服务器
TS-Path:跨项目 TypeScript 引用 -
代码质量:
统一 ESLint/Prettier 配置
共享 Vitest/Jest 测试配置
四、常见问题解决方案
1. 循环依赖问题:
- 使用 pnpm ls --depth 100 检测依赖环
- 架构设计:提取公共模块到核心包
2. 幽灵依赖问题:
启用 pnpm 严格模式:
# .npmrc
strict-peer-dependencies=true
auto-install-peers=false
3.TypeScript 跨项目引用:
// tsconfig.json
{"compilerOptions": {"baseUrl": ".","paths": {"@company/*": ["packages/*/src"]}}
}
4. Docker 多阶段构建:
# 第一阶段:安装依赖
FROM node:18 AS installer
WORKDIR /app
COPY . .
RUN pnpm install --frozen-lockfile# 第二阶段:构建特定应用
FROM installer AS builder
RUN pnpm turbo run build --filter=web-app...# 第三阶段:生产镜像
FROM nginx:alpine
COPY --from=builder /app/apps/web-app/dist /usr/share/nginx/html