前端 Monorepo 实践

6,512 阅读4分钟

什么是 Monorepo

作为前端工作者,可能听说过微前端,也听说过 Monorepo。有时候我们会混为一谈。

简单的说,微前端是「聚」,Monorepo 是「分」。

微前端解决的问题是,将多个以不同技术完成,但是拥有相同业务逻辑的 app 聚合到一起。而 Monorepo 却相反,是将一个 app 中相对比较独立的逻辑,拆分成独立的 package,减少相互的耦合和制约,并且使用同一个 Repo。

Monorepo 和普通 Repo 的关系

通常情况下,我们会在一个 Repo 下使用一个 package。类似如下的项目结构。

src
  -feature1
  -feature2
  -feature3
  -feature4
package.json

这样的项目结构在项目还比较小的情况下还好。但是随着项目的不断扩大,就会出现一些弊端:

  • 项目越来越大,每一次很小的改动都会影响整个项目。(build 时间长,或者业务逻辑的影响)
  • 业务逻辑直接的耦合,无法拆分清楚。
  • 可能会多个 team 维护一个 Repo,team 直接也不好合作。
  • 不同的业务逻辑可能希望分开发布来避免互相影响。

解决方法当然是 Monorepo,我们看一下 Monorepo 的项目结构:

projects
  -feature1
    -src
    -package.json
  -feature2
    -src
    -package.json
  -feature3
    -src
    -package.json
package.json

对应相面的问题,我们来看一看 Monorepo 的优势:

  • 项目上功能的增加,转换为子 package 的增加,每一个独立的功能也只需要关心自己当前的 package 即可。(包括编译)
  • 因为 package 之间的隔绝,需要我们将 package 直接的依赖关系理清楚。业务逻辑的依赖关系转换为了 package 直接的依赖关系,更为清晰。
  • 每个 team 维护自己的 package 就可以的。甚至可以指定属于自己的编码规范,lint,build,都可以自己做自己的配置。
  • 按照 build 分开 deploy 即可。

Monorepo 需要解决的问题

  • 每个 package 的依赖如何避免重复安装。比如一个 Angular 项目,每个 package 可能都需要安装 angular,如何避免不同 package 重复安装。
  • 相同 Repo 下不同 package 直接依赖如何处理。
  • 单 package 下我们可以简单的使用,npm script 处理各种问题,在多 package 下我们该如何使用?

下面我们来看看常用的几种解决方案:

方案一,Angular 默认的解决方案:

  • 利用 node 的向上查找策略,将所有依赖收敛到根 package。package 内只安装自己独有的依赖。
  • 使用 npm link 或者 tsconfig 中的 path,将编译后的目录连接到每个项目。
  • Angular 通过 angular cli 提供。

方案二,Lerna + yarn workspaces

  • Lerna 提供的 lerna add xxx --scope=xxxxx 来安装依赖。
  • lerna add 同时支持,安装当前项目的其他 package,如果本地存在当前版本,优先加载本地(类似 npm link),如果本地不存在则从远端加载。
  • lerna 可以在任意 folder 执行,加上,--scope 即是某个 package。Lerna 还集成了一系列其他常用的命令。
    • lerna clean 清除所有的 node_modules
    • lerna bootstrap install all packages
    • lerna version

以上两种方案其实并不算完美:

  • 如果每一个 package 的依赖都互相独立,则可能出现同一个 repo 下对于相同依赖的多次安装,不仅会增加项目的体积,也会因为相同项目对于同一个依赖的不同引用出现使用上的问题。Node 中的 node_module hell 也会因为 Monorepo 而成倍的放大。
  • 如果项目中的多数依赖都放在 package 中,则会导致每个 package 的依赖不够清晰(Angular 推荐使用,peerdependency 的折中方案)。假设某一天,我希望将某一个 package 独立成一个 repo,发现,很难写清楚,这个 package 真实的依赖有哪些。
  • 内部项目的依赖关系仍然没有办法指定清楚。

方案三:pnpm

归根结底,以上问题还是因为包管理工具不够强大。pnpm 的出现,是为了解决,node_modules 打平的问题而出现的。

  • pnpm 的出现,使得,子 package 可以像一个独立的 app 一样去指定自己的依赖。pnpm install xxx --filter=xxxxx,因为 pnpm 中 node_module 会 link 到 pnpm 的根目录,所以,完全不用担心,同一个 package 在不同子 package 的重复安装。
  • pnpm 提供了 workspace* 来制定当前项目的 package, * 会在 pnpm publish 前替换成真实的版本号。
  • pnpm 提供了一系列支持 monorepo 的命令可以让我们减少使用 lerna.