手把手带你玩转 Monorepo,拥抱现代前端开发新范式
AI Summary
你是否曾被这些问题困扰?
- 管理多个相互关联的 Git 仓库(或是复杂的 git submodules),心力交瘁。
- 想在项目 A 中复用项目 B 的组件或工具函数,只能复制粘贴或者发布成 npm 包,流程繁琐。
- 多个项目依赖同一个库,版本不一致导致“依赖地狱”。
- 每次进行一个跨项目的需求,需要在多个仓库中提交代码、创建 PR,联调测试苦不堪言。
如果你对以上场景感同身受,无论是组件库,框架,或是日常的大型项目,不妨试试 Monorepo。
到底什么是 Monorepo?
想必大家听过这个概念无数次了
Monorepo(Monolithic Repository),直译为“单体仓库”,是一种将多个独立的项目、包(package)或应用(app)存放在同一个代码仓库中进行管理的代码组织策略。
与之相对的是 Polyrepo(Multiple Repositories),也就是我们传统的多仓库管理模式,每个项目都有自己独立的 Git 仓库。
一个常见的误解:Monorepo ≠ 单体应用(Monolith)
- Monorepo 是一种 代码组织方式。仓库里可以包含多个独立的、可独立部署的应用。
- 单体应用 是一种 软件架构模式。它指的是将所有功能模块打包成一个单一的、不可分割的部署单元。
例如在 Monorepo 中,我们可以同时管理一个 React 主应用、一个 Vue 管理后台、一个共享的 UI 组件库和一个通用的工具函数库。它们虽然在同一个仓库,但架构上是解耦的,可以独立开发、测试和部署。
为什么选择 Monorepo?
优势
- 极致的代码复用与共享:这是 Monorepo 最核心的优势。UI 组件库、工具函数、TS 类型定义等可以作为本地包,被仓库内的任何应用直接引用,无需发布到 npm。修改后立即生效,开发体验如丝般顺滑。
- 简化的依赖管理:所有项目共享同一个
node_modules
(或其变体),借助pnpm
等工具可以有效解决依赖版本冲突问题,保证环境一致性。 - 原子化的提交(Atomic Commits):当一个功能需要同时修改前端应用和其依赖的组件库时,可以在一次提交中完成所有更改。这让代码历史追溯和回滚变得异常清晰。
- 统一的工具链与标准化:可以在仓库根目录配置一次
ESLint
,Prettier
,TypeScript
,Jest
等,所有子项目共同遵守,确保了代码风格和质量的统一。 - 提升团队协作:代码透明度高,便于团队成员进行跨项目的 Code Review 和知识共享。
挑战
- 工具链复杂度:需要引入 Lerna, Nx, Turborepo 等专门的工具来管理工作区、任务调度和构建缓存,有一定的学习成本。
- 性能问题:当仓库变得非常巨大时,
git clone
,git status
等命令可能会变慢。不过现代工具正在努力解决这个问题。 - 权限控制:默认情况下,所有人都拥有所有代码的访问权限。对于需要精细化权限控制的团队,需要借助如
GitLab/GitHub CODEOWNERS
等功能。
总的来说,对于需要高度协作、代码共享频繁的前端团队,Monorepo 带来的收益远大于其挑战。
选择合适的 Monorepo 工具
工欲善其事,必先利其器。现代 Monorepo 生态已经非常成熟,以下是几个主流工具:
- 包管理器(必须):
- pnpm: 目前 Monorepo 的首选。它通过符号链接(symlinks)和内容寻址存储来高效管理
node_modules
,天生支持workspace
(工作区)协议,完美契合 Monorepo 场景。 npm
(v7+) /yarn
(v2+):也都支持workspace
,但pnpm
在性能和磁盘空间占用上更具优势。- 任务编排与构建系统(强烈推荐):
- Turborepo: 由 Vercel(Next.js 的母公司)出品,主打“高性能构建系统”。它通过智能任务调度和远程缓存,可以极大地提升 CI/CD 和本地开发的速度。简单、快速、易于上手,是目前的热门选择。
- Nx: 功能极其强大且全面的 Monorepo 工具集。除了 Turborepo 的功能外,还提供了代码生成、依赖图可视化、插件生态等企业级功能,但配置也相对复杂。
这篇文章将教你从基础 pnpm workspaces,到引入 truborepo 加速构建,再使用自建缓存代替 Vercel Remote Cache。
熟悉使用
下面是 pnpm
在 Monorepo 中常用的基本操作:
初始化 Monorepo
首先,你需要一个项目根目录。在根目录下创建 pnpm-workspace.yaml
文件,这是 pnpm
识别 Monorepo 的关键。
bash
pnpm-workspace.yaml
文件定义了你的工作区(workspace)包含哪些子包。
pnpm-workspace.yaml
示例:
yaml
创建子包(Packages)
在 pnpm-workspace.yaml
中定义的路径下创建你的子包。例如,如果你设置了 packages/*
,那么可以在 packages
目录下创建 package-a
和 package-b
。
bash
安装依赖
在 Monorepo 根目录运行 pnpm install
会安装所有子包的依赖,并且 pnpm
会自动识别并符号链接(symlink)工作区内的互相依赖。
bash
添加/移除依赖
添加通用依赖(安装到所有子包)
如果你想在所有子包中添加相同的依赖,可以使用 -w
或 --workspace-root
参数在根目录操作,但通常这不常用。更常见的是给特定子包添加依赖。
bash
添加特定子包依赖
进入子包目录,像普通项目一样添加依赖。pnpm
会智能地处理依赖关系。
bash
添加工作区内部依赖
当一个子包需要依赖 Monorepo 内的另一个子包时,可以直接使用子包的名称(即 package.json
中的 name
字段)作为依赖。
假设 package-a
的 name
是 @my-monorepo/package-a
,package-b
的 name
是 @my-monorepo/package-b
。
bash
提示: 使用 workspace:*
或 workspace:^
可以更好地管理内部依赖的版本。pnpm
默认会使用 workspace:^
。
移除依赖
与添加依赖类似,进入子包目录或在根目录使用 -w
。
bash
运行脚本
在 Monorepo 中,你可以从根目录运行特定子包的脚本,也可以运行所有子包的通用脚本。
运行特定子包的脚本
使用 -F
或 --filter
参数指定要运行脚本的子包。
bash
运行所有子包的脚本
pnpm -r
或 pnpm recursive
命令可以在所有工作区包中运行指定的脚本。
bash
发布子包
发布子包时,你需要进入相应的子包目录进行操作。
bash
7. 一些有用的 pnpm
命令
pnpm ls -r
:列出所有工作区包及其依赖。pnpm outdated -r
:检查所有工作区包的过时依赖。pnpm up -r
:更新所有工作区包的依赖。pnpm store prune
:清理本地pnpm
存储,删除未引用的包。
实战:从零搭建一个前端 Monorepo
好吧,光说不做没有任何作用,讲这些东西没啥意思,csdn 分分钟给我抄走,ai 几秒钟就能生成,咱们实践才能出真知,Let's get our hands dirty
我们的目标是建一个 Vue3 组件库,包含 storybook 文档站 单测 cypress 端测。
核心技术栈选择:
- Vue 3: 利用 Composition API 和更好的性能。
- TypeScript: 为组件库提供类型安全和更好的开发体验。
- Vite: 用于组件库的构建和 Storybook 的开发服务器,速度快。
- pnpm / yarn / npm (with workspaces): 推荐 pnpm 或 yarn workspaces 来管理 monorepo。这里以 pnpm 为例,因为它对 monorepo 支持良好且高效。
- Storybook: 用于组件的交互式开发、文档和展示。
- Vitest: 用于单元/组件测试,与 Vite 集成良好。
- Vue Test Utils: Vue 官方的组件测试库。
- Cypress: 用于端到端 (E2E) 测试。
- ESLint & Prettier: 代码规范和格式化。
- Husky & lint-staged: Git 钩子,在提交前自动检查和格式化代码。
- Turborepo: 一个优秀的高性能构建系统,用于 JavaScript/TypeScript monorepos。
bash
环境准备与项目初始化
我们这里就用我自己刚刚开始写的项目 amore-ui
为例。
确保你已经安装了 Node.js
(我建议还是最新 lts v22 吧)。然后全局安装 pnpm:bash
现在,创建我们的项目:
初始化项目和 Monorepo (使用 pnpm):
bash
编辑 pnpm-workspace.yaml
:
yaml
在根目录安装通用开发依赖:
bash
创建主组件库
先从我们的组件库开始!
创建组件库包 (packages/components
):
bash
安装组件库特定依赖:
bash
配置 packages/components/vite.config.ts
(用于库构建):
typescript
配置 packages/components/package.json
:
json
创建 packages/components/src/index.ts
:
typescript
创建示例组件 packages/components/src/components/Button/Button.vue
:
vue
创建 Storybook 应用
Storybook 是组件库开发使用的利器!
作为应用,我们将其放置在 app/
目录,仅需在你建好的文件夹中执行
pnpm create storybook@latest
之后修改 .storybook/main.js
javascript
创建组件的 Story (packages/components/src/components/Button/Button.stories.ts
):
typescript
修改根 package.json
的 scripts:
json
上文的注释提到,这里详细解释一下,-f
,即 filter,意为过滤器,也就是在对应的仓库中执行,-f 之后跟随的仓库名就是你 package.json
中为每个模块配置的名字,利用这种功能,我们可以为主仓库添加很多 模块:命令
的快捷命令
现在可以运行 pnpm dev:storybook
来启动 Storybook。
设置 Vitest (单元/组件测试):
好的项目通常都有高的单元测试覆盖率
在 packages/components
包中安装 Vitest 和 Vue Test Utils:
bash
配置 packages/components/vite.config.ts
(添加 test 配置):
(在现有 defineConfig
内添加 test
字段)
typescript
创建 packages/components/vitest.setup.ts
(可选):
typescript
在 packages/components/package.json
添加测试脚本:
json
写一个测试 (packages/components/src/components/Button/Button.test.ts
):
typescript
运行 pnpm -F amore-ui test
。
设置 Cypress (E2E 测试):
趁热打铁,让我们继续!接下来是端到端测试
bash
配置 cypress/cypress.config.ts
:
typescript
在根 package.json
添加 Cypress 脚本:
json
start-server-and-test
是一个有用的 npm 包,可以帮你启动服务器,等待它响应,然后运行测试,最后关闭服务器。pnpm add -Dw start-server-and-test
。dev:storybook-static
脚本可以是你构建 Storybook 后用http-server
或类似工具启动静态文件的命令,例如:pnpm build:storybook && http-server storybook-static -p 6006
。
创建 E2E 测试 (cypress/e2e/button.cy.ts
):
typescript
确保 Storybook 在 http://localhost:6006
运行,然后执行 pnpm cy:open
。
Okok,到这里大家可能都看累了或者是感觉无聊,我们来小小整理下我们有了什么命令呢:
json
可以休息下,下面我们继续
创建文档站
这部分很简单,我们依然靠 Vite 来实现
就像往常一样建好你的文档站,之后...
typescript
我这里使用了 unplugin-vue-components
,当然你也可以从工作区直接导入打包之后的产物
到此为止,这个 monorepo 已经初具形态了。
ESLint, Prettier, Husky, lint-staged:
好的代码建立在规范之上
我的项目一直都是 eslint error 模式+有 eslint/单测/e2e 测试不过就禁止 commit,也就是受虐模式
- Husky & lint-staged:
bash
.husky/pre-commit
内容:
bash
在根 package.json
添加 lint-staged
配置:
json
TypeScript 配置 (tsconfig.json
):
太多了,贴不过来了,影响正常阅读
具体看我的仓库 amore-ui
引入 Turborepo 提升效率
目前,我们需要手动进入每个目录去运行命令。当项目变多时,这会变得很麻烦。Turborepo
可以帮我们统一管理和加速这些任务。
1. 在根目录安装 Turborepo
bash
2. 配置 turbo.json
在项目根目录创建 turbo.json
文件:
json
3. 在根 package.json
中添加脚本
json
现在,你可以从根目录统一运行命令了!
bash
Turborepo 自建缓存
我对 Vercel 公司不喜欢也不讨厌,但是我希望自建一个缓存。
参考这个项目就好啦:
Monorepo 最佳实践
- 统一配置:将
ESLint
,Prettier
,tsconfig.json
等配置文件放在packages
目录下(如packages/eslint-config-custom
,packages/tsconfig
),然后让各个应用和包去继承这些配置,保持一致性。 - 明确的目录结构:
apps
放应用,packages
放可复用包,是一种广泛采纳的约定。 - 版本管理:使用如 Changesets 这样的工具来管理包的版本发布和生成
CHANGELOG
,它与 Monorepo 配合得非常好。 - 精简根目录:保持根目录
package.json
的dependencies
干净,只存放对整个项目都至关重要的开发依赖(如turbo
,typescript
,prettier
)。
尾声
恭喜你!你已经成功搭建并体验了一个现代化的前端 Monorepo 项目。
感觉这个例子选的不是很好啊,有点太上难度了,直接把我之前研究好久的架子全搬上来了,一般只有组件库或者大框架会这样操作了,不过基础的入门操作还是可以提升效率的🥹
从现在开始拥抱现代前端开发吧!