深入分析 Monorepo 架构在大型前端项目中的最佳实践,包括代码共享、依赖管理、构建优化和 CI/CD 流程。

好嘞,各位观众老爷,今天咱们聊聊 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 来声明 reactreact-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-apackages/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。 记住,没有银弹,只有最适合你的解决方案。

各位,下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注