Monorepo 进化论 - 你真的在用公共包吗?

做者:m1henghtml

大力智能前端团队很早便开始在团队内部践行 monorepo,从 2018 年 12 月 7 日的第一个 commit 起,到如今 154 个业务包,11 个公共包以及 7 个工具包,咱们的 monorepo 已经走过了比较长的轨迹。仅以此文抛砖引玉。记录,沉淀与记念 monorepo 的历程。前端

引子

某天,有位同窗 小 B 从一大早就眉头紧缩,午休事后,当你们还在睡眼朦胧之时,他忽然拍案而起:“大家这个 common-A 包是否是有黑科技,为何改 tsconfig 没有用的!”node

这个时候,common-A 包的维护者小 A,默不做声的跑到小 B 的身后小声说道:“你点开你业务包的打包工具配置,是否是把 common-A 包加到 include 里面去了?”webpack

小 B 啪啪两下在 VSCode 中打开了 ****.config.js ,里面赫然写着git

{
   tsLoader: (opts, { addIncludes }) => {
      addIncludes([/(packages|@monorepo_workspace)/]);
    },
}
复制代码

小 B 依旧不解:“这有什么问题吗?”程序员

小 A 摸摸 小 B 的头说道:“你的业务包是源码引用了 common-A 包,那 common-A 包的 tsconfig 固然不生效啦。”github

哪里出了问题?

Monorepo 给开发者提供的一大便利之一就是 —— 抽象公共包不用发版,在 repo 内就能引用。这项便利极大的刺激了团队内对于 common package 落地与迭代的积极性。web

可是在业务包中引用 common 内的逻辑时,广泛采起 alias 与 loader 添加 include 将 common package 内的代码做为业务源码一同给到打包工具,一同编译,视做业务内代码,而非一个正常的 package。typescript

在 monorepo 全 TS 场景的状况下,This works, but with hidden problems。npm

Package.json 被无视了

Common 包内定义的入口其实是不生效的,业务包可以无视包入口引用任意一段 common 包内的逻辑,这给 common 包的维护带来了必定的困难。

TSConfig.json 被无视了

在 TS 的状况下,common 包自带的 tsconfig.json 中的配置将被无视,而是使用了业务包的相关配置。common 包须要适配全部业务包的 tsconfig 而非维护一个自洽的 tsconfig。

Phantom Dependency

Common 包内引用的依赖是仅在 common 包内声明的,业务包使用时并不会去二次声明该依赖。但做为源码打包,实际上存在隐式依赖与依赖版本不肯定的问题。

总的来讲,在直接引用源码的状况下,common 包再也不是一个包,而仅仅是一个文件夹,其中的 package.json 与 tsconfig.json 都仅仅是在自嗨,没有任何用处。

有解决方案吗?

咱们的目的是将 common TS 包变成一个像在 npm 发布的包同样在业务包中被使用,真实的开发场景中咱们每每还会关注如下几点:

  1. 由于现存的业务包较多,新的方案须要对原有业务包的改动较少(但不包括入口生效致使的代码变更)。
  2. 须要同时适配 node 项目与 web 项目。
  3. Dev 时最好支持 common 包的改动即便生效,不须要额外的手动步骤。

其实解决方案有不少种,在与同窗脑暴的过程当中出现过无数天马行空的方案,可是大多数方案都存在 hack 过多或者开发成本太高的问题,综合下来可行性较高的只有两种依赖 git hook 自动编译或者使用 ProjectReferences。

自动编译 - w/ Git Hook

这项方案曾在隔壁组真实的试行过,即在 Git pull hook 中添加全部 common 包编译的脚本。

开发者在每次 git pull 的时候自动触发编译,将全部 common 包在本地编译一次。这对于只开发业务包的开发者来讲,基本知足了平常需求

可是在 common 包与业务包同时开发的场景下,每每须要开两个 terminal 同时运行编译,并且业务包的 dev 进程很难感知到 common 包发生的变化。这就须要开发者频繁的手动重启业务包的 dev 进程,十分影响效率。

ProjectReferences

  • TypeScript 在 3.0 中引入了新特性 Project References

  • 为较为细分的 TS 项目提供了细粒度 tsc 的能力。从 TS 的官方文档看,这项功能本意是为了知足同一个项目下对细分小模块进行独立编译提效例如单例测试的场景。从咱们的视角来讲,这很惊喜的知足了 monorepo 下 common TS 包的自动编译功能。

  • 包含处理多个 tsconfig.json 链路依赖的能力。当 tsconfigA 中有 projectReferences 字段时,tsc 会先编译 projectReferences 中指向的 tsconfig,再最终编译 tsconfigA,同时也支持链路依赖,如 tsconfigA -> tsconfigB -> tsconfigC。

  • TSLoader 也支持了 ProjectReferences

  • TSLoader 也从 5.2.0❤️ 开始支持了 projectReferences 能力,并在后续的几个迭代中显著的提高了其性能。基于 Webpack 的 TS 项目在使用 TSLoader 时,TSLoader 将会识别 tsconfig 中的 projectReferences 并将其交给 TSInstance 一并编译。

咱们能够发现,在使用 ProjectReferences 的状况下,不管是一个须要 tsc 编译的 node server 项目或者是一个须要 webpack 打包的 web 项目,均可以被很好的支持。

如何实现呢?

首先须要确保 common 包自己的配置正确

  1. Common 包与业务包的 tsconfig 须要符合 TS 的相关要求。common 包能够根据不一样的使用状况配置两套 tsconfig,如 tsconfig.es.json + tsconfig.lib.json。
  2. 在 common 包的 package.json 中配置好一个正常的包应有的入口属性。
// pacakge.json
{
  "name": "@monorepo_workspace/common-a",
  "version": "1.0.0",
  "description": "一个common包",
  "sideEffects": false,
  "exports": {
    ".": {
      "import": "./es/index.js",
      "require": "./lib/index.js"
    }
  },
  "main": "./lib/index.js",
  "module": "./es/index.js",
  "typings": "./es/index.d.ts"
}
复制代码
  1. 在业务包中调整一些配置
  • 打包工具中删除相关 include,打开 tsloader,并打开 projectReferences。
// some js config
{
  tsLoader: (config) => {
    config.projectReferences = true;
    config.compilerOptions = undefined;
  }
}
复制代码

这里多说一句,这里之因此将 compilerOptions 设置为 undefined 是由于某些框架会默认配置一些 compilerOptions,这些在 tsloader config 中的 compilerOptions 将会覆盖 projectReferences 的包的 tsconfig,会引起一些奇怪的问题,因此这里设置 undefined 用来覆盖默认配置。

  • package.json 的依赖中确保 common 包的声明,且包管理工具能帮正确以包的形式找到 common 包。
  • tsconfig.json 的 projectReferences 中配置好对应要找的 common 包的 tsconfig 路径。
// tsconfig.json
{
  "references": [
    {
      "path": "../../common/common-a/tsconfig.es.json"
    },
    {
      "path": "../../common/common-b/tsconfig.json"
    }
  ]
}
复制代码

Path 能够写具体 tsconfig.json 的地址,也能够写包的路径,会自动读取文件夹路径下的 tsconfig.json

配置结束,去 Run Dev 一下,你就会看到在跑业务代码前,projectReferences 中的 common 包会被 TS build 一遍,而后在真正的打包过程当中,你的打包工具终于把 common 包做为一个 REAL 包去对待。

是否能够更进一步?

上述 projectReferences 的方案已经在咱们的 monorepo 中落地并跑了一段时间了,整体的评价仍是不错的,并且组内的同窗也能很快的理解并可以本身改造本身项目去使用 projectReferences。

可是“懒”是程序员的本质,在 tsconfig 中添加两行 references 也是一项额外的负担。

理论上在 ts-loader 以前加一个 webpack 插件或者是在 ts-loader 中提供 autoReference 的能力,是能够知足将本地包自动视做 projectReference 的。这个 idea 还在咱们讨论的初期,若是有同窗有兴趣,欢迎私聊共建。

未完待续

除了解决 common 包的引用问题,monorepo 内经历过诸如 node 部署,yarn.lock review 地狱,resolution 过多等问题,敬请期待咱们的总结分享。


欢迎关注「 字节前端 ByteFE 」

简历投递联系邮箱「 tech@bytedance.com

相关文章
相关标签/搜索