天天看點

前端100萬行代碼是怎樣的體驗?開工Why Monorepo?關于 Monorepo 的幾個核心疑問?目前的開發體驗真正需要解決的問題更深層的經驗總結與展望

作者:會影

來源:Alibaba F2E公衆号

前端100萬行代碼是怎樣的體驗?開工Why Monorepo?關于 Monorepo 的幾個核心疑問?目前的開發體驗真正需要解決的問題更深層的經驗總結與展望

近年來,阿裡資料中台産品發展迅速。核心産品之 Quick BI 連續 2 年成為國内唯一入選 Gartner 魔力象限的國産 BI。Quick BI 單一代碼倉庫源碼突破了 100萬行。整個開發過程涉及到的人員和子產品都很多,因為下面講的一些原則,産品能一直保持在快速的開發狀态。

先分享幾個關鍵資料:

  • 代碼:TypeScript 82萬行,樣式 Sass+Less+CSS 18萬行。(cloc 統計,去除自動生成代碼)
  • 協同:Code Review 12,111 次,Commit 53,026 次。
前端100萬行代碼是怎樣的體驗?開工Why Monorepo?關于 Monorepo 的幾個核心疑問?目前的開發體驗真正需要解決的問題更深層的經驗總結與展望

很多人會問,這麼多代碼,為什麼不切分代碼庫?還不趕快引入微前端、Serverless 架構?你們就不擔心無法維護,啟動龜速嗎?

實際情況是,從第一天開始,就預估到會有這麼大的代碼量。啟動時間也從最初的幾秒鐘到後面越來越慢5~10分鐘,再優化到近期的5秒鐘。整個過程下來,團隊更感受到 Monorepo(單一代碼倉庫)的優勢。

這個實踐想說明:

  • 大的 Codebase 可能是好事情,大道至簡。用極其“簡單”的架構更容易支援複雜靈活的業務
  • 要做到簡單的架構,内部需要更明确的規範,更密切的協同,更高效的執行
  • 能通過工程化解決的問題,就不要通過開發規範,能通過規範來解決的不要靠自由發揮

開工

2019年4月30号,晴朗的下午,剛好是喜迎五一的前一天,發揮集體智慧,投票選出滿意的倉庫名。同時借 Quick BI 和 FBI 底座融合的契機,項目開啟。後來底座代碼轉正,把上層業務代碼也吸納進來。

commit 769bf68c1740631b39dca6931a19a5e1692be48d
Date:   Tue Apr 30 17:48:52 2019 +0800

    A New Era of BI Begins           

Why Monorepo?

前端100萬行代碼是怎樣的體驗?開工Why Monorepo?關于 Monorepo 的幾個核心疑問?目前的開發體驗真正需要解決的問題更深層的經驗總結與展望

在開工之前,對單一倉庫(Monorepo)和多倉庫(Polyrepo)團隊内做了很多的讨論。

曾經我也很喜歡 Polyrepo,為每個元件建立獨立 repo 獨立 npm,比如2019年前,單是表單類的編輯器元件就有 43 個:

前端100萬行代碼是怎樣的體驗?開工Why Monorepo?關于 Monorepo 的幾個核心疑問?目前的開發體驗真正需要解決的問題更深層的經驗總結與展望

本以為這樣可以做到 完美的解耦、極緻的複用??

但實際上:

  1. 每次 Babel、React 等依賴整體更新能讓人脫層皮,是以自研了腳手架。造輪子都是被逼出來的,事情做了一點點,但寫腳本能力直線上升
  2. 每次 調試元件,npm link 一下。後來元件跨級,可以做 3 層 npm link,使用過的都知道這是多麼糟糕的體驗
  3. 版本難對齊,每次主倉庫釋出前,元件間版本對齊更是考驗眼力,稍有不慎觸發線上故障
  4. 友善别人複用的優勢呢?最終支援自己業務都捉襟見肘,哪還敢讓别人複用

最終我們把所有這些元件都合并到一個倉庫,其實像 Google/Facebook/Microsoft 這些公司内部都很推崇 Monorepo。

但我們不是原教旨主義的 Monorepo,沒必要把不相關的産品代碼硬放到一起。在實線團隊内部,單個産品可以使用 Monorepo,會極大降低協同成本。但開始的時候,團隊内還是有很多疑問。

關于 Monorepo 的幾個核心疑問?

1.單一倉庫,體積會很大吧?

100 萬行

代碼的體積有多大?

先來猜一下:1GB?10GB?還是更多?

首先,按照公式計算一下:

代碼的體積 = 源碼的體積 + .git 的體積 + 資源檔案(音視訊、圖檔、其他檔案)

我們一起來算一下源碼的體積:

一般建議每行小于 120 字元,我們取每行 100 個字元來算,100 萬行就是:

100 * 1000,000 = 100,000,000 B
轉換之後就是 100 MB!           

那我們的倉庫實際多大呢?

隻有 85 MB!也就是平均每行 85 個字元。

2.再來算一下

.git

的體積:

.git

裡記錄了所有代碼的送出曆史、branch 和 tag 資訊。會很大體積吧?

實際上 Git 底層做了很多的優化:1. 所有 branch 和 tag 都是引用;2. 對變更是增量存儲;3. 變更對象存儲的時候會使用 zlib 壓縮。(對于重複出現的樣闆代碼隻會存儲一次,對于規範化的代碼壓縮比例極高)。

按照我們的經驗,

.git

記錄

10,000

次 commit 送出隻需要額外的

1~3

個代碼體積即可。

3.資源檔案大小

Git 做了很多針對源碼的優化,但視訊和音頻這類資源檔案除外。我們最近使用 BFG 把另一個産品的倉庫從 22GB 優化到 200MB,降低 99%!而且優化後代碼的送出曆史和分支都得到了保留(因為 BFG 會編輯 Git 送出記錄,部分 commit id 會變化)。

以前 22 GB 是因為倉庫裡存放視訊、釋出的 build 檔案和 sourcemap 檔案,這些都不應該放到源碼倉庫。

小結一下,百萬行代碼體積一般在 200MB ~ 400MB 之間。那來估算

1000

萬行代碼占用體積是多少?

乘以十也就是

2GB ~ 4GB

之間。這對比

node_modules

随随便便幾個 G 來說,并不算什麼,很容易管理。補充個案例,Linux 核心有 2800 萬行,使用 Monorepo,數千人協同。據說當時 Linus 就是為了管理 Linux 的源碼而開發出 Git。

2. 啟動很慢吧?5分鐘還是10分鐘?

聽到有些團隊講,代碼十幾萬行,啟動 10+分鐘,典型的“巨石”項目,已經很難維護了。趕緊拆包、或者改微前端。可能團隊才 3 個人卻拆了 5 個項目,協同起來非常麻煩。

我們做法有3個:

  1. 按照頁面來拆分多 Entry,每次隻需啟動一個 Entry
  2. 梳理子包廂的依賴關系,追求極緻的 Lazy loading,Tree-Shaking
  3. Webpack 切換到 Vite

尤其是 Webpack 切換到 Vite 以後,最終項目冷啟動時間由 2-5分鐘 優化到 5秒 内。熱編譯時間由原來 5秒 優化到 1秒 内,Apple M1 電腦基本都是 500ms 以内。

3. 代碼複用怎麼辦?Monorepo 複用的時候是否要引入全部?

傳統的軟體工程思想追求 DRY,但并不是越 DRY 越好。

每寫一行代碼,都産生了相應代價:維護的成本。為了減少代碼,我們有了可複用的子產品。但是代碼複用有一個問題:當你以後想要修改的時候它就會成為一個障礙。

對于像 Quick BI 這樣長期疊代的産品,絕大部分需求都是對原有功能的擴充,是以寫出易維護的代碼最重要。是以,團隊不鼓勵使用 magic 的特技寫法;不單純追求代碼複用率,而是追求更易于修改;鼓勵在未來子產品下線的時候易于删除的編碼方式。

對于确實存在複用的場景,我們做了拆包。Monorepo 内部我們拆了多個 package(後面有截圖),比如其他産品需要 BI 搭建,可以複用

@alife/bi-designer

,并借助于 Tree-Shaking 做到依賴引入的最小化。

目前的開發體驗

1.冷啟動 5秒,熱編譯 1秒内。以前是 5~10分鐘。

  1. 改一行代碼能解決的問題,真正改一行且釋出一次。而不是改 10+ 個項目,按依賴釋出 N 次。
  2. 新人 10分鐘 搭建好環境,上手開發

a.相比于以前每個元件一個 Repo,包賦權都要搞很久

4.避免了版本不對齊的問題

a.對于 2C 産品,不需要多版本多主幹分支,但多個 npm 依賴對齊版本也不容易

b.對于 2B 産品,由于多環境、多版本,會更加複雜,複雜度極高。Monorepo 通過分支來統一内部依賴的版本

5.工程化更新隻需要一次。目前是基于 Lerna 開發的 Pri Monorepo 方案。

這樣的體驗要保持并不容易,開發中還有很多問題要解決。

真正需要解決的問題

并不是把代碼放到一起就完了,背後複雜的問題是 協同、技術方案、穩定性(如何避免一個人送出代碼導緻整個産品崩潰?)

1. 包依賴管理

内部拆分多個子包,每個子包是子檔案,可以單獨釋出 npm,見下圖:

前端100萬行代碼是怎樣的體驗?開工Why Monorepo?關于 Monorepo 的幾個核心疑問?目前的開發體驗真正需要解決的問題更深層的經驗總結與展望

内部包管理的核心原則是:

  • 從左向右單向依賴,隻能右邊引用左邊。避免循環依賴
  • 規範還不夠,開發插件來自動檢測,如果左邊依賴右邊直接報錯

對于開源 npm 的引入,應該更慎重。大部分 npm 的維護時長不超過x年,即使像 Moment.js 這樣曾經标配的工具庫也會終止維護。可能有 20% 的 npm 是沒人維護。但未來如果你的線上使用者遇到問題,你就需要靠自己啃源碼,陷入被動。是以我們的原則是,引入開源 npm 要三人線下評審通過才行。

2. Code Review 文化

互相 Code Review 能幫助新人快速成長,同時也是打造團隊技術文化的方式。

過去幾年一直在團隊内推行 100% CR,但這還不夠。機械的執行很容易把 CR 流于形式,還要分場景來做。

Monorepo 有個風險是一旦有問題就可能是整體的問題。

目前我們的 Code Review 主要分為3個場景:

  1. 線上 MR Code Review【1對1】
  2. 主題式 Code Review【3-5個人】
  3. 大版本釋出前集體 Code Review【All】

12,111 次 Code Review 的經驗很多,主要是:

及時 Review,鼓勵小顆粒度的 MR,不必等整個功能開發完成

代碼是寫給人看的,鼓勵白話文一樣的代碼,而不是文言文

建立最佳實踐(目錄樹結構、命名規範、資料流規範)。開發一個功能可以有 10 種方法,但團隊需要選 1 種并推廣

不鼓勵炫技,為了未來可維護性。能用簡單技術實作,不要用“高深”冷門的技術

強調開發潔癖,追求優雅代碼的文化。(命名是否易于了解、注釋是否完整、是否有性能隐患等)

3. 工程化建設

這個過程首先要感謝淘系前端 DEF 工程化團隊的支援,在這麼多代碼的情況下,不斷挑戰極限更新 DEF 支援我們。

除了制定文檔的規範之外,能夠自動化工具檢查的規範才是好規範。

檢查器:ESLint、TS 類型校驗、Prettier

文法檢查器是推動規範落地的重要方法,ESLint 可以做增量,優化後 git commit 的 pre-hooks 依舊很快。但 TS type check 因為不支援增量就比較慢了,需要搭配 CI/CD 來使用。

Webpack vs Vite

釋出使用 Webpack,開發使用 Vite。

開發環境使用 Vite 快速調試,生産環境依舊使用 Webpack 打包。

風險是開發和生産編譯産物不一緻,這一塊需要上線前回歸測試避免。

4. 性能優化

對于資料産品而言,性能的挑戰除了來自于 Monorepo 後資源包的變大,還有大資料量對渲染計算帶來的挑戰。

性能優化可以分為3個環節:

  • 資源加載:精細化 Tree Shaking,難在精細。Webpack 本身的 Tree-Shaking 做的并不好,不支援 Class method 做 Tree Shaking,是以有時候需要修改代碼。Lazy Loading 子產品做到按需加載,尤其是圖表、SQL 編輯器這類大元件。合理的接口預加載,不要讓網絡閑下來。
  • 視圖渲染:讓元件渲染次數降到最低,表格類元件虛拟滾動優化,閑時預加載預渲染。
  • 取數請求:資源本地化緩沖方案,移動端使用 PWA 将 JS 等資源檔案和資料緩存到本地。

另外還有性能檢測工具,定位性能卡點。計劃做代碼性能門闩,代碼送出前如果發現包體積增大發出提醒。

5. 資料化驅動架構優化

身在資料中台,我對資料的業務價值深信不疑。但對于開發本身而言,很少深度使用過資料。

是以 S1 重點探索了開發體驗的數字化。通過采集大家的開發環境和啟動耗時資料來做分析【不統計其他資料避免内卷】。發現很多有意思的事情,比如有個同學熱編譯 3~5 分鐘,他以為别人也是這樣慢,嚴重影響了開發效率,當從報表發現資料異常後十分鐘幫他解決。

另外一個例子,為了保持線上打包産物的一緻性,推動團隊做 Node.js 版本統一,以前都是靠釘,釘多少次都無法知道效果如何。有了報表以後就一目了然。

前端100萬行代碼是怎樣的體驗?開工Why Monorepo?關于 Monorepo 的幾個核心疑問?目前的開發體驗真正需要解決的問題更深層的經驗總結與展望

更深層的經驗

效率最高的方式就是一次最好

每行代碼都會留下成本。長遠考慮,效率最高的方法就是一次做好。

蘇世民說“做大事和做小事的難度是一樣的。兩者都會消耗你的時間和精力”。既然如此,不妨把代碼一次寫好。代碼中如果遺留 “TODO” 可能就永遠 TO DO。客觀來講,一次做好比較難,首先是每個人認為的“好”标準不同,背後是個人的技術能力、體驗的追求、業務的了解。

組織文化技術 相輔相成

技術架構群組織結構有很大關系,選擇适合組織的技術架構更重要。

如果一個組織是分散的,使用 Monorepo 會有很大的協同成本。但組織如果是内聚的,Monorepo 能極大提效。

工程化和架構底座是團隊的事情,靠個人很難去推動。

短期可以靠戰役靠照搬,長期要形成文化才能持續疊代。

組織溝通成本高應該通過組織來解,通過技術來解的力量是渺小的。技術可以做的是充分發揮工具的優勢,讓變化快速發生。

簡單不先于複雜,而是在複雜之後

對于一個簡單的架構,總有人會想辦法把它做複雜。踩了坑,下決心重構,成功則回歸簡單,失敗就會被新的簡單模式颠覆。踩坑本身也是有價值的,不然新人總是按捺不住還會再踩一次。做複雜很容易,但保持簡單需要遠見和克制。沒有經曆過過程的磨練,别人的解藥對你可能是毒藥。

架構不可能一成不變的,我們的圖表最開始直接使用 D3、ECharts 很簡單,後來定制很多逐漸複雜到難以維護,于是基于 G2 自研 bi-charts 後架構又一次變簡單,前後的開發體驗可能是差不多的,但背後的技術完全變了。

總結與展望

百萬行代碼沒什麼可怕,是一個正常的節點,仍然可以像幾萬行代碼那樣靈活。

現在 Quick BI 已經向千萬行邁進,向世界一流 BI 的目标邁進。以上内容更多是工程化相關,把工程化做好目的是想讓開發者更專注于業務,沒講的業務挑戰其實更多,因為資料分析天生就要與海量資料打交道,性能優化有長期的實踐;洞察豐富異樣的資料,有很多可視化及複雜表格方面的沉澱,可視化不僅是技術,也是業務本身;手機平闆電視等多端展示,跨端适配的挑戰。未來還希望能夠把資料分析打造成一個引擎,能夠快速內建到辦公和商業流程中。

目前的開發模式并不完美,在疊代的過程中,不可避免會産生技術債,架構的優化本質就是在保持可維護性和減少技術債。最近團隊在醞釀一次 Redux-Toolkit 的引入,會對取數和資料流有大的更新,有進展再分享。