好嘞,各位观众老爷,今天咱们聊聊 Monorepo,这玩意儿听起来高大上,其实就是把所有代码放一个仓库里,有点像古代皇帝的后宫,三千佳丽…咳咳,跑题了,咱们还是说代码。
Monorepo 架构在大型前端项目中的最佳实践
一、 什么是 Monorepo?它香在哪儿?
Monorepo,顾名思义,就是 "Mono"(单一) + "Repository"(仓库)。它是一种代码管理策略,将多个项目或模块的代码放在同一个代码仓库中进行管理。
好处嘛,那可多了:
- 代码共享更容易: 组件、工具函数,想怎么用就怎么用,直接
import
,告别npm publish
的烦恼。 - 依赖管理更简单: 升级依赖,一次搞定,不用在多个仓库里折腾。想想升级 React,一个仓库升级完事,其他仓库自动享受,多爽!
- 原子性变更: 多个项目同时修改,可以一起提交,保证一致性。比如,修改一个组件库,同时更新使用它的所有项目,保证兼容性。
- 更容易的代码审查: 所有代码都在一个地方,方便审查,也更容易发现潜在问题。
- 协作更高效: 团队成员可以更容易地参与到不同的项目中,促进知识共享。
当然,Monorepo 也有缺点:
- 仓库体积大: 所有代码都在一个地方,仓库体积肯定比单个项目大。
- 构建时间长: 如果不进行优化,构建所有项目会非常耗时。
- 权限管理复杂: 需要更精细的权限管理,避免误操作。
所以,Monorepo 并不是银弹,要根据实际情况选择。如果你的项目规模不大,或者团队协作比较少,可能没必要用 Monorepo。但如果你的项目规模很大,模块很多,团队协作很频繁,那么 Monorepo 绝对值得考虑。
二、 Monorepo 的常用工具
搭建 Monorepo,需要一些工具来辅助:
工具 | 作用 | 特点 |
---|---|---|
Lerna | 管理多个 package,可以发布、版本控制等。 | 比较老牌,功能完善,但性能稍差。 |
Yarn Workspaces | Yarn 内置的 Monorepo 支持,可以管理依赖,提升安装速度。 | 简单易用,性能好,但功能相对简单。 |
Nx | 强大的 Monorepo 构建工具,可以进行增量构建、代码生成等。 | 功能强大,性能好,但学习成本较高。 |
pnpm | 性能极佳的包管理器,支持 Monorepo,可以节省磁盘空间。 | 速度快,节省空间,但兼容性可能存在问题。 |
Bazel | Google 出品的构建工具,可以构建各种类型的项目,包括前端项目。 | 功能强大,但配置复杂,学习成本非常高。 |
Turborepo | Vercel 出品的增量构建工具,专注于提升构建速度。 | 速度快,配置简单,但功能相对简单。 |
选择哪个工具,取决于你的需求和团队的技术栈。如果你的项目比较简单,Yarn Workspaces 或 pnpm 足够了。如果你的项目比较复杂,需要更强大的构建工具,可以考虑 Nx 或 Turborepo。
三、 Monorepo 的目录结构
Monorepo 的目录结构非常重要,一个清晰的目录结构可以提高开发效率,降低维护成本。
常见的目录结构:
my-monorepo/
├── packages/ # 存放各个 package 的代码
│ ├── ui-components/ # UI 组件库
│ │ ├── src/
│ │ ├── package.json
│ │ └── ...
│ ├── utils/ # 工具函数库
│ │ ├── src/
│ │ ├── package.json
│ │ └── ...
│ ├── app-a/ # 应用 A
│ │ ├── src/
│ │ ├── public/
│ │ ├── package.json
│ │ └── ...
│ └── app-b/ # 应用 B
│ ├── src/
│ ├── public/
│ ├── package.json
│ └── ...
├── tools/ # 存放构建工具、脚本等
│ ├── scripts/
│ ├── ...
├── package.json # 根目录的 package.json,用于管理整个 Monorepo
├── lerna.json # Lerna 的配置文件
├── yarn.lock # Yarn 的 lock 文件
└── ...
packages/
目录:存放所有的 package,每个 package 都是一个独立的模块,可以单独发布。tools/
目录:存放构建工具、脚本等,用于自动化构建、测试、发布等流程。- 根目录的
package.json
:用于管理整个 Monorepo 的依赖和脚本。 lerna.json
:Lerna 的配置文件,用于配置 Lerna 的行为。yarn.lock
:Yarn 的 lock 文件,用于锁定依赖版本。
四、 Monorepo 的最佳实践
接下来,咱们聊聊 Monorepo 的最佳实践,这些都是血泪教训总结出来的经验。
1. 代码共享
代码共享是 Monorepo 的核心优势之一,要充分利用这个优势。
- 组件库: 将通用的 UI 组件、业务组件封装成组件库,供所有项目使用。
- 工具函数: 将常用的工具函数封装成工具函数库,例如日期格式化、字符串处理等。
- 公共类型定义: 将公共的 TypeScript 类型定义放在一个地方,避免重复定义。
- 共享配置: 将共享的 ESLint、Prettier、Webpack 等配置放在根目录,供所有项目使用。
例如,创建一个 ui-components
package:
packages/ui-components/src/Button.tsx
import React from 'react';
interface ButtonProps {
children: React.ReactNode;
onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ children, onClick }) => {
return <button onClick={onClick}>{children}</button>;
};
export default Button;
然后在其他项目中使用:
packages/app-a/src/App.tsx
import React from 'react';
import Button from '@my-monorepo/ui-components/src/Button';
const App: React.FC = () => {
return (
<div>
<h1>Hello Monorepo!</h1>
<Button onClick={() => alert('Button clicked!')}>Click me</Button>
</div>
);
};
export default App;
2. 依赖管理
Monorepo 的依赖管理比较复杂,需要注意以下几点:
- 统一依赖版本: 尽量统一所有 package 的依赖版本,避免版本冲突。
- 使用
peerDependencies
: 如果一个 package 是一个库,需要使用peerDependencies
来声明依赖,让使用者自己安装依赖。 - 避免循环依赖: 循环依赖会导致构建失败,要尽量避免。
- 使用
nohoist
: 如果某些依赖需要单独安装,可以使用nohoist
来阻止 Yarn Workspaces 将其提升到根目录。
例如,ui-components
package 的 package.json
:
{
"name": "@my-monorepo/ui-components",
"version": "1.0.0",
"description": "A UI component library",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"peerDependencies": {
"react": "^17.0.0",
"react-dom": "^17.0.0"
},
"devDependencies": {
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0"
}
}
这里使用了 peerDependencies
来声明 react
和 react-dom
的依赖,让使用者自己安装。
3. 构建优化
Monorepo 的构建时间通常比较长,需要进行优化。
- 增量构建: 只构建修改过的 package,避免构建所有 package。
- 并行构建: 同时构建多个 package,提高构建速度。
- 缓存: 缓存构建结果,避免重复构建。
- 代码分割: 将代码分割成多个 chunk,减少初始加载时间。
- Tree Shaking: 移除未使用的代码,减小 bundle 体积。
可以使用 Nx 或 Turborepo 来进行增量构建和缓存。例如,使用 Nx:
nx build app-a
这条命令只会构建 app-a
package,如果 app-a
依赖了 ui-components
,那么 Nx 会自动构建 ui-components
。
4. CI/CD 流程
Monorepo 的 CI/CD 流程也比较复杂,需要考虑以下几点:
- 测试: 运行所有 package 的测试,确保代码质量。
- Lint: 运行所有 package 的 Lint,保持代码风格一致。
- 构建: 构建所有需要发布的 package。
- 发布: 发布修改过的 package。
- 版本控制: 自动更新 package 的版本号。
可以使用 Lerna 来进行版本控制和发布。例如,使用 Lerna:
lerna version
lerna publish from-package
lerna version
会自动更新 package 的版本号,lerna publish from-package
会发布修改过的 package。
5. 权限管理
Monorepo 的权限管理非常重要,要避免误操作。
- 代码审查: 所有代码都要经过审查才能提交。
- 权限控制: 限制开发人员对某些 package 的访问权限。
- 自动化: 使用自动化工具来管理权限。
可以使用 Git 的权限管理功能,或者使用专门的权限管理工具。
五、 实战案例
咱们来一个简单的实战案例,搭建一个包含 UI 组件库和两个应用的 Monorepo。
1. 初始化项目
mkdir my-monorepo
cd my-monorepo
yarn init -y
yarn add -D lerna
2. 初始化 Lerna
lerna init
3. 创建 packages
mkdir packages
cd packages
mkdir ui-components app-a app-b
cd ui-components
yarn init -y
cd ../app-a
yarn init -y
cd ../app-b
yarn init -y
cd ../..
4. 修改根目录的 package.json
{
"name": "my-monorepo",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"start:app-a": "yarn workspace app-a start",
"start:app-b": "yarn workspace app-b start",
"build:ui-components": "yarn workspace ui-components build",
"test": "lerna run test"
},
"devDependencies": {
"lerna": "^4.0.0"
}
}
private: true
:防止根目录被发布到 npm。workspaces
:指定 Monorepo 的工作区,Yarn Workspaces 会自动管理这些工作区的依赖。scripts
:定义一些常用的脚本,方便执行。
5. 修改 lerna.json
{
"packages": [
"packages/*"
],
"version": "independent"
}
packages
:指定 Lerna 管理的 package。version
:指定版本管理模式,independent
表示每个 package 独立管理版本号。
6. 安装依赖
yarn install
7. 添加代码
在 packages/ui-components
中添加一个 Button 组件:
packages/ui-components/src/Button.tsx
import React from 'react';
interface ButtonProps {
children: React.ReactNode;
onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ children, onClick }) => {
return <button onClick={onClick}>{children}</button>;
};
export default Button;
packages/ui-components/package.json
{
"name": "@my-monorepo/ui-components",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"peerDependencies": {
"react": "^17.0.0",
"react-dom": "^17.0.0"
},
"devDependencies": {
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"typescript": "^4.0.0"
},
"scripts": {
"build": "tsc",
"test": "echo "Error: no test specified" && exit 1"
}
}
在 packages/app-a
和 packages/app-b
中使用 Button 组件:
packages/app-a/src/App.tsx
import React from 'react';
import Button from '@my-monorepo/ui-components/src/Button';
const App: React.FC = () => {
return (
<div>
<h1>Hello App A!</h1>
<Button onClick={() => alert('Button clicked in App A!')}>Click me</Button>
</div>
);
};
export default App;
packages/app-a/package.json
{
"name": "@my-monorepo/app-a",
"version": "1.0.0",
"dependencies": {
"@my-monorepo/ui-components": "1.0.0",
"react": "^17.0.0",
"react-dom": "^17.0.0"
},
"devDependencies": {
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"typescript": "^4.0.0"
},
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production",
"test": "echo "Error: no test specified" && exit 1"
},
"main": "src/index.tsx",
"license": "MIT"
}
packages/app-b
同理,只是内容略有不同。
8. 构建和运行
yarn build:ui-components
yarn start:app-a
yarn start:app-b
这个简单的案例演示了如何使用 Monorepo 共享代码,管理依赖,构建和运行项目。
六、 总结
Monorepo 是一种强大的代码管理策略,可以提高开发效率,降低维护成本。但是,Monorepo 也有缺点,需要根据实际情况选择。
希望今天的讲座能帮助你更好地理解 Monorepo,并在实际项目中应用 Monorepo。 记住,没有银弹,只有最适合你的解决方案。
各位,下课!