一、Monorepo 概念与价值
- 什么是 Monorepo
- Monorepo(Monolithic Repository):
用 一个代码仓库 管理多个项目、多个包(apps、服务、组件库、工具库等)。
代码按"目录/包"划分,但物理上都在一个仓库里。 - 与 Polyrepo 对比:
- Polyrepo:每个项目一个仓库(web 一个、server 一个、组件库一个...)。
- Monorepo:一个仓库,内部按 apps/、packages/ 等目录划分。
- 为什么要用 Monorepo(核心是"依赖复用 + 中心化管理")
优点:
-
依赖和代码复用更方便
- 公共组件库、工具函数、类型定义、hooks 等可以做成单独包,比如 packages/ui、packages/utils、packages/types。
- 一个仓库里,子包之间 直接本地依赖,不必频繁发布到 npm 再安装。
-
统一管理 & 中心化思想
- 统一的 lint、test、build、release 流程。
- 统一的工具链和规范(如 ESLint、Prettier、TypeScript 配置)。
- 一处升级依赖,可以影响所有 apps / packages,容易保持版本一致。
-
跨项目联调效率高
- 比如 web 应用依赖本地组件库 @my-org/ui:
- 修改组件库代码 → 立刻在 web 应用跑起来验证。
- 不需要发布版本、切换仓库、重新安装依赖。
- 比如 web 应用依赖本地组件库 @my-org/ui:
-
更好地支撑大型前端体系
- 多个业务线、多种终端(H5、PC、管理后台、小程序...)共用基础能力时,Monorepo 可以作为"前端平台底座"。
缺点 / 需要注意:
- 仓库体积会变大,历史记录更多,CI/CD 流程更复杂(一般要做到"只构建改动影响到的包")。
- 权限粒度不如多个仓库细(一个仓库里默认都能看到全部代码)。
- 需要配套工具(如 pnpm workspace、TurboRepo、Nx 等)协助管理任务和依赖。
二、pnpm:适合 Monorepo 的包管理工具
- pnpm 是什么
- pnpm 是一种 快速、省空间 的包管理工具,可替代 npm、yarn、cnpm。
- 核心特性:内容寻址存储 + 软链接机制。
- 关键特性
-
磁盘占用小
- pnpm 使用 全局的 store(仓库) 保存下载过的依赖。
- 不同项目、不同 workspace 共享同一份依赖内容。
- 每个项目的 node_modules 里不是完全复制依赖,而是大量 软链接 指向全局 store。
-
安装速度快
- 有效利用缓存,重复项目安装时几乎不重新下载。
- 并行安装能力强。
-
严格的依赖隔离
- 与 npm、yarn 相比,pnpm 对依赖的"提升"更严格,有利于暴露错误的依赖声明(比如包 A 其实没在 dependencies 写某个依赖,却依赖了它)。
-
天然支持 Monorepo(Workspace)
- 通过 pnpm-workspace.yaml 管理多个子项目/子包。
- 顶层可以执行 pnpm install、pnpm -r run build 等命令,给所有包统一操作。
- 基本使用(针对 Monorepo)
- 初始化项目(已有项目可跳过)
pnpm init - 配置 workspace
在项目根目录创建 pnpm-workspace.yaml:
packages:
- 'apps/*'
- 'packages/*'
- 统一安装依赖
pnpm install - 在某个包/应用中安装依赖
在 apps/web 中安装 React
pnpm add react react-dom --filter apps/web
在 packages/ui 中安装 antd
pnpm add antd --filter packages/ui
- 对所有包执行命令(递归)
对所有包执行 lint
pnpm -r run lint
对所有包执行 build
pnpm -r run build
这里的 -r 就是 recursive(递归)的意思。
三、典型 Monorepo 项目结构(前端为主)
假设你现在这个仓库是 React + SSR + H5,这里给出一个 通用的、相对理想的 项目结构,你可以对照自己的情况调整。
- 根目录结构
-
package.json
- 存放根依赖(很通用的 devDependencies 可以放这里,比如 typescript、eslint、prettier、turbo 等)。
- 配置统一的脚本,例如:
- "lint": "pnpm -r run lint"
- "build": "pnpm -r run build"
- "test": "pnpm -r run test"
-
pnpm-workspace.yaml
- 声明 workspace 包含哪些目录:
packages:
-
'apps/*'
-
'packages/*'
-
统一配置文件(建议都放在根目录)
- tsconfig.base.json:基础 TypeScript 配置,供各子包继承。
- .eslintrc.cjs:统一 ESLint 配置。
- .prettierrc:代码风格统一。
- .editorconfig:编辑器统一规则。
- turbo.json 或 nx.json(如果使用 TurboRepo 或 Nx 做任务编排和缓存)。
-
CI / 配置文件
- .github/workflows/*(GitHub Actions)。
- Dockerfile / docker-compose.yml(如果有容器部署)。
- scripts/:放一些 Node 脚本,比如自动发布、版本管理等。
- apps/:最终可运行应用
一般放"真正对外运行的应用",例如前端站点、后端服务。
-
apps/web:前端 H5/SSR 应用(React)
- src/
- pages/:页面级组件(路由对应)。
- components/:业务级或通用组件。
- hooks/:应用级 hooks(可视情况提炼到 packages/hooks)。
- store/:应用级状态管理(也可以放在 packages/store,供多应用共享)。
- routes/:路由配置(React Router / 自研路由)。
- entry.client.tsx / entry.server.tsx:SSR 入口文件。
- package.json:
- 声明依赖(如 react、react-dom、react-router-dom 等)。
- 定义脚本:
- "dev": "vite dev" / "start": "webpack-dev-server"
- "build": "vite build"
- "lint": "eslint src --ext .ts,.tsx"
- src/
-
apps/admin(可选):后台管理系统
-
apps/server(可选):Node 服务(如 SSR 服务、BFF、API 网关)
- packages/:可复用的库和模块
这里是 Monorepo 的"精华部分",专门放 可以被多个 apps 共用 的东西。
-
packages/ui:组件库
- 放通用 UI 组件(按钮、表单、列表、布局组件等)。
- 对上层应用暴露统一的组件接口。
- 可选择使用 storybook 来做组件文档。
-
packages/utils:工具库
- 日期、格式化、URL 解析、请求封装等与 UI 无关的逻辑。
- 提供纯函数或通用 helper。
-
packages/types:类型定义库(如果用 TS)
- 接口响应数据类型、领域模型类型、枚举等。
- 被 UI、服务端、工具库共同引用,确保类型统一。
-
packages/store:状态管理库(如果你希望多个应用共享 store 能力)
- 封装 Redux / Zustand / Recoil / Jotai / 自研 store 等。
- 暴露 hooks,比如 useUserStore、useTodoStore 等,给各个 apps/* 使用。
-
packages/config(可选)
- 打包配置、Lint 规则、TS 配置等复用封装。
- 比如 @my-org/eslint-config,在各个包中只需 "extends": "@my-org/eslint-config"。
-
packages/api(可选)
- 请求封装(fetch/axios)、接口定义、API 客户端等。
每个 packages/* 子包一般都有自己的 package.json,像这样:
{
"name": "@my-org/ui",
"version": "0.0.1",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsup src/index.tsx --dts",
"lint": "eslint src --ext .ts,.tsx"
},
"dependencies": {
"react": "^18.0.0"
}
}
-
典型 Monorepo 总体结构示例
.
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── tsconfig.base.json
├── .eslintrc.cjs
├── .prettierrc
├── apps
│ ├── web
│ │ ├── package.json
│ │ └── src
│ │ ├── pages
│ │ ├── components
│ │ ├── store
│ │ ├── hooks
│ │ └── entry.client.tsx
│ └── server
│ ├── package.json
│ └── src
│ └── index.ts
└── packages
├── ui
│ ├── package.json
│ └── src
├── utils
│ ├── package.json
│ └── src
├── types
│ ├── package.json
│ └── src
└── store
├── package.json
└── src
四、Monorepo + pnpm 的常见工作流
- 新增一个包 / 应用
-
新增应用:apps/admin
- 创建目录 apps/admin。
- 在其中 pnpm init 或手动写 package.json。
- 在 pnpm-workspace.yaml 的 packages 中已经包含 apps/* 的话,不需要额外修改。
- 使用 pnpm add ... --filter apps/admin 安装依赖。
-
新增包:packages/hooks
- 创建目录 packages/hooks。
- 初始化 package.json,指定 "name": "@my-org/hooks"。
- 编写业务无关的 React hooks。
- 在 apps/web 中通过 pnpm add @my-org/hooks --filter apps/web 使用本地包。
- 跨包开发
- 在 packages/ui 新增组件。
- 在 apps/web 中直接 import { Button } from '@my-org/ui'。
- 一起运行:
同时在多个 app 中启动开发
pnpm --filter apps/web dev
pnpm --filter apps/admin dev
或者使用 Turbo/Nx 做统一的 task 管理。
- 统一执行测试/构建/检查
- 在根 package.json 中定义脚本:
{
"scripts": {
"lint": "pnpm -r run lint",
"test": "pnpm -r run test",
"build": "pnpm -r run build"
}
}
- 使用时:
pnpm lint
pnpm test
pnpm build
pnpm 会在所有包含对应脚本的 apps/* 和 packages/* 里依次执行。
五、如何从"简单项目"演进到 Monorepo
结合现在的仓库,可以按下面顺序演进:
- 先引入 pnpm
- 切换包管理器,生成 pnpm-lock.yaml。
- 抽离公共代码到 packages
- 把复用性强的部分(组件、工具、类型)从 src 中提炼到 packages/*。
- 拆出多应用到 apps(如果未来有多端)
- 如 SSR 服务、管理后台、BFF 服务等。
- 统一配置 & 任务
- 把 ESLint / TS / Prettier 配置提到根目录,apps 和 packages 尽量共享。
- 根目录用 pnpm 脚本统一 lint/test/build。