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

相關文章
相關標籤/搜索