大家好,我是若川。持續組織了5個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。
復制此鏈接 https://www.yuque.com/seeconf/2022/keynote?或者點擊文末閱讀原文可查看下載SEE Conf PPT和錄播。
編者按:本文為 2022.1.8 在 SEE Conf 分享的文字稿,介紹了 Umi 4 的一些設計思路,時間原因,只聊 4 個,包含編譯時框架、依賴預打包、默認快、約束與開放。這幾天 colors?和 faker.js 鬧得前端社區沸沸揚揚,但 Umi 卻能獨善其身,希望其中「依賴預編譯」的部分能給大家一些啟發。
背景
大家好,我是來自螞蟻集團的云謙。
Umi 開發了 3 個版本,并且即將發布第四個大版本,這中間踩了很多坑,也有不少有意思的設計思路和思考,今天會找一些典型的來和大家展開聊下。
Umi 是什么
如左圖所示,在 pages 下新建一個文件,里面導出組件,經過 Umi 做一層魔法轉換,即可產出右圖的頁面。之所以能做到這樣,是因為 Umi 在背后構建、路由、運行態等等的事情,把開發者的上手門檻降低最低。
Umi 是螞蟻集團出品的前端開源框架,基于 React。我們在內部服務了 10000+ 項目,在去年(2021)中文社區的某個調研報告中,也有 25% 以上的受訪者采用 Umi 進行項目開發。Umi 4 目前研發中,其中包含很多新特性。
這是 Umi 到螞蟻內部開發者的鏈路圖。Umi 和最佳實踐開源開發,直接服務社區。但是這在內部使用還遠遠不夠,所以加上內部業務和平臺的處理后,再形成更高層的框架 Bigfish,用于服務內部開發者。
Umi 從 1 到 4,遇到很多問題,趟過很多坑,也總結了很多經驗。圖中這些,部分是設計上的思路,部分是遇到坑后的方案總結,包含著螞蟻項目和開發者的🩸和💧。時間原因,今天從其中挑了 4 個和大家展開聊聊。
編譯時框架
不知大家有沒有發現,相比 10 年前前端編碼有了很多變化,以下這些是和編譯時相關的。
1、代碼寫少了。少了很多腳手架代碼,比如數據流、國際化、模塊加載、路由等,從而讓開發者有更多精力專注在業務視圖和邏輯上
2、報錯提前了。比如模塊不存在,使用了不存在的變量,之前是運行時才能發現,現在報錯提前了,在命令行里就能看到。除了 DX 的提升,額外的好處的此類問題不會再被帶到線上
3、產物變小了。之前用一個 button 要引入整個 antd,用一個 isEqual 要引入整個 lodash,現在通過 tree-shaking 或 babel-plugin-import 或 TailwindCSS 的 JIT 引擎,能準確知道你用了啥,然后做按需打包,讓產物變小
4、功能配置化了。現在很多標準化的功能都配置化了,比如想要兼容 ie11,做個配置,框架就會在背后加補丁,轉 es 5,比如想要用 external 自動提速,想要高清方案,想要埋點,想要用 esbuild 壓縮,都是一個配置的事情
5、配置約定化了。有些場景配置化還是繁瑣了,比如路由、數據流 model、國際化語言文件,可以通過約定的方式,就沒必要做配置了
為啥會有這些變化?哪有什么歲月靜好,是框架在背后默默做了很多事。
那么編譯時框架和非編譯時框架的區別是啥?非編譯時比如非常流行的 create-react-app,把源碼簡單直接地交給 webpack 就完成使命;編譯時框架則會自己加很多戲,比如拿到源碼后做 ast 分析,拿到依賴圖譜,做檢查,生成臨時文件,等等,最后把編譯后的源碼交給 webpack,這中間的很多事,本來是需要開發者手動處理或編碼的。
社區有很多編譯時的嘗試。比如 Angular 的 AOT 和 JIT,可以簡單理解 AOT 為編譯時,JIT 為運行時,AOT 可以讓產物更小,同時運行更快;比如 facebook 之前出的 prepack,也是編譯時優化的嘗試,在保證結果一致的前提下,改變源碼,讓性能更快;還有最近的 React Forget 更是編譯時優化的典型。
Umi 做了很多編譯時的事,如果你用過 umi,應該了解 src 下有個 .umi 臨時目錄,這里存放的文件本是需要開發者自己寫的,現在由框架或插件在編譯時自動生成。比如在 pages 目錄下新建文件即是路由,新建 access.ts 文件即是權限,在 locales 目錄下新建文件即是國際化語言,等等。
這部分的 One More Thing 是 Low Import 開發模式,他會隨著 Umi 4 發布,但默認不開啟。左圖是開啟前,右圖是開啟后,區別是大部分 import 語句不用寫,交給框架自動補全。這個方案很有爭議,喜歡的很喜歡,不喜歡的很不喜歡,但不管如何,這也是編譯時領域的一次嘗試。
依賴預打包
不知大家是否有此經歷。睡一覺醒來,很多事發生了變化。比如 dev 或 build 跑不起來,啥都沒干迭代發布后線上白屏還背了個故障,npm i 時出現某人的求職廣告,依賴庫被黑客掛馬,等等。
然后你打開 package.json 一看,只有 10 個依賴呀,我還寫死了版本,這是為啥?因為你忽略了成千上萬的間接依賴,而這些依賴總有一個會發生點意外,比如某個依賴的不兼容更新,就會導致你項目掛掉。
問題的根源是 semver。理想的 semver 是 breaking.feat.bugfix,現實的 semver 是 breaking.breaking.breaking。并發 breaking 的 bugfix 版本是社區的常規操作。
是問題就有解,社區已有不少。臨時的比如 cnpm 提供的 bug-versions,npm 提供的 resolutions,侵入式改代碼的 patch-package 等;長期的比如 npm、yarn 和 pnpm 具備的 lock 能力,tnpm/cnpm 目前暫不支持,但可以用 yarn mode。
還有個思路是「中間商鎖依賴,定期更新,并對此負責」。框架是開發者的倒數第二道防線,自然而然就應該是這個中間商。
這個思路在 Umi 里的實現是依賴預打包。打包前,umi 通過 dependency 依賴 webpack、babel 等,這時如果 babel 出現 bug,會導致 umi 掛,然后用戶項目也掛,睡不好,😴;打包后,umi 通過 devDependency 依賴 webpack、babel 等,如果 babel 又出現 bug,umi 會不會受影響,umi 用戶的項目也不會受影響,睡得香,😄。
通過預打包,Umi 把依賴的 node 數從 1309 降到 314,這帶來的不僅有安全和穩定,還有安裝提速、node_modules 目錄瘦身、命令行啟動提速、無 peerDependency 警告等等。
簡單介紹下如何預打包,分代碼和類型定義兩部分,分別通過 ncc 和 dts-packer 實現。比如 webpack,借助兩個工具,會分別在 compiled/webpack 目錄生成 index.js 和 index.d.ts,以實現預打包的目的。
這部分的 One More Thing 是 Father 的下個版本 V4,他是基于 Umi 的組件打包工具,在 V4 里,除了其他 nb 的特性外,還有個重要的點就是前面我們介紹的依賴預打包功能,大家可以期待下。
還有另一個 Two More Thing 是 browser 側依賴的鎖。前面我們聊的其實只適用于 node 側依賴,比如 webpack、babel 這些,那像 antd 這些 browser 依賴呢?他們不能被預打包。原因包括:1、尺寸問題,browser 要考慮尺寸,預打包會讓 tree-shaking 失效 2、browser 庫直接影響線上,風險更高,人肉回歸成本高。有一個解法是「importmaps 鎖 + 灰度 + 定期人肉更新的中間依賴」,時間原因,具體不展開。
默認快
大家在日常工作中,應該多少都經歷過各種類型的慢。比如 Lint 慢、依賴安裝慢、構建慢到 OOM、CI 慢、本地啟動慢、提交慢、node_modules 大、測試慢等,一些具體的數據比如 ant-design-pro 腳手架啟動時間在 30s,改完代碼后熱更新時間在 3s,而螞蟻某中后臺較慢應用的啟動時間在 5 分鐘,熱更新時間半分鐘。改完代碼去趟廁所回來,可能還沒好...
關于提速我們之前整理了三個法寶,緩存、延遲處理和 Native Code。緩存能提速是因為做過的事情不過第二遍,比如 webpack 5 的物理緩存,babel 緩存,預編譯依賴作為緩存等;延遲處理能提速是因為把不重要的事情拆出去后,關鍵進程就快了,比如各種按需和延遲編譯都屬于此類;Native Code 主要指 esbuild、swc 此類,能提速是利用語言特性進行降維打擊,主要用在壓縮和編譯上,效果顯著。
這是我們整理個各個階段的提速方案,兩個維度,時間和方法。可以看到,1、利用緩存的方案很多 2、現在和未來的方案大量基于 Native Code 3、效果好的方案是多個方法結合使用,比如 esbuild 雖然單體快,但純用的效果卻不一定好。
Umi 在這部分的第一個解是 MFSU,基于 webpack 5 Module Federation 特性的提速方案。1、基于 webpack,解決我們既要 webpack 的功能和生態,又要 Vite 的速度的問題 2、在螞蟻內部已服務 1000+ 應用 3、快是他的主要特點,除了啟動快、熱更快、頁面打開也快,注意頁面打開也快,這是 esm bundless 方案所不具備的 4、可上生產,除了本地快,CI & CD 流程也要快。
這是 MFSU 兩個版本和 webpack 對比的效果。V2 在二次啟動和熱更方面相比 webpack 都有大幅提升。V3 在 V2 的基礎上,對首次啟動也做了改進,有一點在圖上沒體現的是,V3 在頁面打開速度上也做了改進,不會有通常 esm bundless 的大量請求問題。
介紹下 MFSU 的原理。項目源碼會走到 babel/swc 插件,插件會做兩件事,1、修改源碼,從 remote 獲取資源 2、收集依賴到依賴圖譜;然后依賴圖譜會通知 dep builder 做依賴的預編譯,這里可以選 esbuild 或 webpack,產出的格式都是 module federation;最后修改后的源碼會加載這份預編譯后的依賴,形成 BI 環。
這是 MFSU 的時間線。到目前已經迭代了 3 個大版本。V1 版本是最理想的版本,依賴預編譯走 cdn,讓首次啟動也快,但覆蓋率有限,所以效果有限,并且維護成本高;被打擊后 V2 版本回歸現實,依賴預編譯走本地,和 Vite 的模式類似,覆蓋率 95%+,效果很好,但也留了些邊界場景;V3 是 V2 的優化,解了目前遇到的所有問題,不僅首次啟動快,頁面打開也快;V4 在路上,2022 年做,主要是關于協作的。
MFSU 這么好,怎么用呢?😄 為大家準備了兩種方式。1、umi 4 默認啟用了 mfsu,兩行命令即可嘗鮮 2、大家的項目可能不是基于 umi,也可以用,基于底層庫,適用于任意 webpack 5 項目,我特意準備了一個例子,帶上 antd 等庫之后,空緩存首次啟動也是 1s 內。
Umi 的第二個解是多構建引擎。不止支持 webpack,也支持 vite,還有試驗性的 esbuild,照顧朋友們的不同偏好。Umi 通過配置在不同模式之間切換,并盡可能保證功能的一致性。大家是否有感受到,現在社區有一種趨勢是,dev 用 vite 提速,build 用 webpack 提速。
Umi 4 對于默認快還有更多解。源碼編譯用 swc、依賴編譯用前面介紹的 MFSU、然后把 esbuild 用到 js 壓縮、css 壓縮、依賴編譯、jest 編譯、以及配置文件和 MOCK 文件的編譯上。此外,還有右下角的 fast refresh、lazy import、remote cache、code splitting 策略等。總之,要默認快,是個細節多又體系化的活。
這部分的 One More Thing 是 ESMi,時差原因,大家在 D2 上可能已經聽過他的介紹,這里再介紹下他的原理。ESMi 是我們的 ESM Bundless 方案,面向未來的方案,不僅適用于本地命令,還適用于搭建系統。他包含 Server 和 Client 兩部分功能。Client 會把 depinfo 傳給 Server 并要求 ImportMaps,Server 需要分析依賴并做云端構建,繼而返回 ImportMaps,Client 拿到 ImportMaps 后就可以在瀏覽器里渲染了。
約束與開放
社區同學經常一方面抱怨 Umi 太黑盒,一方面又抱怨這么多選擇,我應該選哪個;螞蟻內部同學經常抱怨內置方案我不喜歡,能否換一個?
之所以有這個問題,歸根結底還是場景不同。是個人還是團隊,是同一個團隊還是不同團隊。團隊需要一致性。到達終點的路很多,但這些路在一個團隊內卻不應該放開讓大家選。
所以「社區要開放,團隊要約束」。然后約束要有度。約束越多,越一致。但又不能把路堵死,堵久了容易固步自封。
螞蟻內部有 50 條「強約束」規則集,目的除了方案和編碼一致性,還可以提升安全性、規避常見錯誤、提升可維護性等。為了讓這些規則不像 eslint 可以在本地輕易跳過,采取了服務器下發的方式。
舉一些規則的例子。比如不能使用除 dva、use model 之外的數據流方案,不能無理由使用 eval、new Function、不能混用 cjs 和 esm 模塊規范,組件代碼不能超過 600 行,不能使用 resolution 鎖定一方庫和二方庫版本。
還有個特殊規則是同一 major version 下「只能使用框架最新版」,這使得我們全局只有一個框架版本,對于框架升級和應用治理都有很大幫助,并且副作用很小,RIO 很高。
再來看社區的方案。面向社區的 Umi 框架會更傾向「原子功能 + 組裝」的使用方式,盡量白盒,相比「集中式」用戶會有更多控制權。
舉個例子,比如提供功能 A,集中式是配置 A: {} 開啟,組裝式是分別提供 base 和 A,用戶通過 A + base 的方式開啟。再具體點比如 jest 配置,Umi 4 的組裝式是在 jest.config.ts 里配置 export default configUmiAlias(createConfig(opts)),把 createConfig 和 configUmiAlias 做個組裝。
One More Thing,螞蟻內部強約束的規則集會在 SEE Conf 分享中公開,可能會大家會有些借鑒意義。
Umi 4
最后,Umi 4 將于近期發布,在此和大家分享下 Umi 4 的特點。
1、體系化有體感的默認快
2、依賴預打包讓你的項目安全又穩定
3、雙構建引擎給用戶更多選擇
4、技術棧最新把底層依賴升到最新,尤其是 react router 6,我太喜歡這個版本了
5、最佳實踐 V2,包含請求、數據流和國際化的相關更新
6、Umi Pro 是內部 Bigfish 框架的對外版本,解我們自己的問題,同時也給社區另一個集中化框架的選擇。
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。分享、收藏、點贊、在看我的文章就是對我最大的支持~