查看原文
其他

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

大力智能前端团队 字节前端 ByteFE 2022-11-02

作者:m1heng

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

引子

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

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

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

{
   tsLoader(opts, { addIncludes }) => {
      addIncludes([/(packages|@monorepo_workspace)/]);
    },
}

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

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

哪里出了问题?

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

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

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

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 


 点击阅读原文,快来加入我们吧!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存