乾貨分享:螞蟻金服前端框架和工程化實踐

螞蟻金服科技發表於2019-07-17
在InfoQ 2019年舉辦的 GMTC 全球大前端技術大會上,螞蟻金服高階技術專家陳成發表了《螞蟻金服前端框架和工程化實踐》的演講,以下是本次演講摘要。

乾貨分享:螞蟻金服前端框架和工程化實踐

框架發展歷史

乾貨分享:螞蟻金服前端框架和工程化實踐


這是我們的框架發展時間線。


  • 2015 年之前我們有 Sea.JS、Arale、SPM 開源技術方案,大家可以有所耳聞。
  • 2015 年我們接入 React,從自研的 Roof 到 Redux 再到開源的 Dva,一步步驗證我們的最佳實踐,並把這些實踐交給開源社群檢驗。
  • 2017 年開始嘗試了新一代的企業級前端框架,Umi 和 Bigfish,前者是從無線業務中長出來的,後者是從中臺業務中長出來的。
  • 一個團隊出兩個框架畢竟不是長久之計,後來老大直接把兩撥人調到一個組,於是就愉快地合併在了一起。

乾貨分享:螞蟻金服前端框架和工程化實踐


在 Umi 和 Bigfish 時代,我們從刀耕火種的時代跨入了工業化時代。因為在此之前,使用者需要接觸很多技術棧和細節,在 Umi 和 Bigfish 中,使用者只要知道一個框架,剩下的全部不用瞭解。框架像一個魔法球,把各種技術棧吸到一起,加工後吐給使用者,以此來支撐業務。


乾貨分享:螞蟻金服前端框架和工程化實踐


在兩個框架合併之後,我們的現狀是這樣:

  • umi 對外開源,bigfish 對內服務內部同學。
  • bigfish 扔掉原有實現,改造成 umi + umi 外掛集的一個架構。
  • 我們不是第一個這麼做的,類似的還有 eggjs 和 chair。這是一種很好的方式,開源和業務兩不誤。
  • 那麼,這是我們的框架終局嗎?以及是否還有更好的方式?大家也可以思考下,後面在未來規劃區域會有探討。


乾貨分享:螞蟻金服前端框架和工程化實踐


這是一些螞蟻的內部資料:

  • 1100+ 內部應用數
  • 新增產品 80% 都用此框架
  • 包含 100+ 外掛數量,社群夠活躍,尤其是內部的
  • 1500+ 內部使用者

目前來看,這個框架基本統一了內部的框架使用情況,不僅有不熟前端的 Java 開發,略熟前端的外包,還有資深的前端同學。要獲得那麼多同學的認可,並不是件容易的事。


為什麼我們能成

那麼,為什麼我們能成?個人理解,我覺得有幾個關鍵詞:

  • 業務
  • 流程
  • 開源

乾貨分享:螞蟻金服前端框架和工程化實踐


人是非常重要的一環,甚至比技術本身更重要一些。


那麼別人為啥要用你的框架?首先,框架要好用,這是最基本的;然後,使用者尤其是資深的前端同學,還得在這上面找到自己的成就感和 ownership,另外如果績效漂亮就更好了。總不能別人用你的框架,然後只有你自己一個人的績效好,那是不會長久的。


我們的解法是外掛體系。


乾貨分享:螞蟻金服前端框架和工程化實踐


框架不是憑空而來的,需求來自於業務,所以用框架寫業務的同學往往能發現框架不足的點,他們可以開發適用於自己業務的框架外掛,反哺框架。如果這是通用需求,那就亮了。框架的內部開發群有 100+ 人,包含大量來自業務線的同學,這就是外掛體系的好處,人人都能貢獻。為了讓寫外掛變得簡單,我們給框架分了五層架構。


乾貨分享:螞蟻金服前端框架和工程化實踐


包含依賴層、外掛層、外掛集層、應用型別層和部署模式層,大家可在任何一層都可貢獻程式碼,

  • 可以寫一個獨立的功能外掛,比如和某個服務的對接,比如擴充套件路由的某個功能,比如實現一套特殊的補丁方案;
  • 可以做歸類,把一系列外掛整理到一個外掛集裡,適用於某一類的業務開發;
  • 可以擴充套件應用型別,比如 SPA、MPA、微前端等等;
  • 可以擴充套件部署模式,比如和不同的框架或平臺做結合;


乾貨分享:螞蟻金服前端框架和工程化實踐

這是外掛生命週期圖,包含:

  • node 環境執行的編譯時
  • 瀏覽器上執行的執行時
  • ui 輔助層的編輯時


大部分外掛體系只會考慮 node 編譯時,我們加上執行時和編輯時的支援,賦予了外掛更大的能力。具體做了什麼就不展開了,沒個框架都不同,但做的事情其實大體一致,往上說是 html、css、js,往下說還有各種工具的配置,比如 webpack、babel、postcss、dev 中介軟體 等等。

乾貨分享:螞蟻金服前端框架和工程化實踐

下面來看具體如何寫一個外掛,如果大家有寫過 vue-cli 的外掛,會發現很類似:

  • 匯出一個函式,第一個引數包含我們提供的能力。
  • 可以新增、修改、繫結事件等等。


乾貨分享:螞蟻金服前端框架和工程化實踐


我們目前的部分外掛,內部流程相關的就沒有列出來了,內外加起來應該有 100 多個了吧。


乾貨分享:螞蟻金服前端框架和工程化實踐

這是我們的分工表,基本上涉及到了框架和業務的方方面面,很多事情都是由不同的人來負責,大家的參與度也不錯。


當然,人總是不夠的,很多子項都還處於招人狀態。


乾貨分享:螞蟻金服前端框架和工程化實踐

我們的框架能成,我覺得另一個重要的原因是我們不僅做功能,還做業務和流程。我不清楚大家是如何走流程的,包括如何切換應用型別,如何和各種後端服務和平臺對接,反正我們的還是挺繁瑣的。程式設計師的時間浪費在這裡我覺得很不值,所以如果框架能解決這部分,應該會受到歡迎。


我們通過 appType 和 deployMode 兩個維護來對接各種場景,使用者只要配 deployMode: node 就能對接 node 框架,改成 java 就能對接 java 框架,背後的髒活累活交給框架做。


乾貨分享:螞蟻金服前端框架和工程化實踐


最後還有一個原因是我們做開源,我個人是比較熱衷開源的,把自己的實現完全透明地展示給社群,包括之前寫的工具和資料流方案,也都是從開源做起,因為我覺得開源相比在內網閉門造車,能帶來很多好處。

  • 程式碼質量,不寫用例的程式碼不會有人願意用。
  • Bugfix 和額外的程式碼貢獻,社群很多人都是願意參與的,在吸引到足夠的人使用之後,框架內部的問題會更快暴露出來,還會有很多人願意貢獻程式碼和修復 Bug。
  • umi core developer group,我們還組織了社群的 umi developer 群,比如 vscode 外掛、create-umi 等等的包,就是由社群同學主導維護的。


另外,開源做地好,也更容易獲得內部同學的認可。包括之前做的 dva、現在的 umi,都不是一開始的內部首選,而是後來慢慢逆襲的。


框架大圖

乾貨分享:螞蟻金服前端框架和工程化實踐


這是我們現在的框架大圖:

中間從下往上是社群開源、螞蟻開源、Bigfish 框架、應用釋出流程。

框架層主要就是我們前面介紹的五層架構。

左上主要是資產市場,我們提效的主要手段之前,這在後面會展開介紹。

左下是工程方面的配套設施,編輯器外掛、測試、lint 工具等等。

右邊是對接的服務,通過框架外掛,可實現配置式地對接外部服務,減少接入成本。


拳頭功能

下面是我們的一些拳頭功能。

資產市場

乾貨分享:螞蟻金服前端框架和工程化實踐


今年由於大形勢的原因,我們比較重研發提效,最好是一個人能幹 10 個人的活。關於提效,其中比較重要的是相同的程式碼不要重複寫,要做提取和元件化。而資產市場就是做的這件事。為了更有效地複用,我們對資產市場分了四級:

  • 元件,指通用元件,就是 antd,在下半年將要釋出的 antd@4 裡,我們會陸續提取更多通用元件到 antd 中。
  • 業務元件,不能提取通用元件的,我們會提到內部統一的業務元件倉庫中。
  • 區塊,由元件組成,可以想象成程式碼片段。
  • 頁面模板,由區塊組成


我們可以藉助工具把區塊和頁面模板新增到頁面中:


乾貨分享:螞蟻金服前端框架和工程化實踐


通過 Umi UI(視覺化方式)新增區塊的樣式。


乾貨分享:螞蟻金服前端框架和工程化實踐


區塊方案其實不是一開始就這樣,中間經歷了幾次迭代。

  • 最初的思路來源是 angular 的一個 theme market,以及飛冰。
  • 1.0 的版本時我們設計區塊是頁面級的,使用者可以在一個頁面裡寫元件、資料流方案、mock 等等,這樣我們要做一個基於 antd 的 CRUD 頁面就很簡單,一個命令把區塊拿進來,然後修修改改就完事了。
  • 然後今年我們重新整理了區塊方案,因為我們希望區塊能更通用一些,比如可重複新增,可無限巢狀,支援區塊集,可結合佈局,支援視覺化新增等等。
  • 這是區塊方案的迭代情況,一路踩著坑過來的。


乾貨分享:螞蟻金服前端框架和工程化實踐


資產市場不會憑空運轉起來,或者說我們做了資產市場,大家就會按照這套方案用起來。比如一個產品,設計師不按照約定的規範來設計,那資產市場就成了擺設。所以,這是一件自上而下的事情,並且得拉上設計師同學一起做,才有可能做好。

在工具層面,我們需要打通上下游,同時兼顧三類角色的同學:

  • 設計師,在設計師工具層同步資產市場,讓大家在設計時就按照約定的方式走。
  • 資產開發者,提供元件開發工具,包括元件的打包、文件、本地除錯、測試、釋出、自動生成 CHANGELOG 等等。
  • 資產使用者,同時提供命令列和視覺化工具,命令列是兜底方案,視覺化的方式新增資產則更友好。

微前端

乾貨分享:螞蟻金服前端框架和工程化實踐


我們在微前端方面也有一些沉澱,並在生產環境有大量應用。


關於微前端是啥?首先大家想到的可能是一個解決多套技術棧共存的方案,比如首頁用 jQuery,訂單頁用 React,客戶系統用 Vue。這沒錯,但是一個相對狹義的理解。


一個問題是,如果我們的技術棧一致,那是否就不需要微前端方案了?不是!


乾貨分享:螞蟻金服前端框架和工程化實踐


我對微前端的理解是,他不僅是個技術方案,更是個解決流程、組織架構等問題的方案。

比如淘寶網,可以簡單理解成有淘寶首頁、交易系統和幫助系統,這些系統是優先順序的,並且在我們人力有限的情況下,我們會把資深的同學投入到重要的系統裡,不重要的系統我們可能會通過外包或者購買的方式解決,但是一個底線是,不重要的系統不能影響重要的系統的運轉。

要實現這一點,目前流行的有兩種方式:

  • MPA(多頁應用)
  • 微前端
  • MPA 沒啥好說,成本低,大家都愛用。但如果想要更好地體驗,則不妨試試微前端。


乾貨分享:螞蟻金服前端框架和工程化實踐


微前端的概念其實已經出來 3 年多了,但社群喊地比較多,給方案比較少,在生產環境應用地就更少了。

我們首先是基於 SingleSPA。

  • 子應用提供 bootstrap、mount 和 unmount 三個生命週期方法。
  • 主應用註冊子應用並決定渲染哪個子應用。
  • 這樣能 Run 起來,但還只是玩具,要上到生產環境還遠遠不夠,還需要解決很多關鍵的技術問題。


乾貨分享:螞蟻金服前端框架和工程化實踐


圖中是我們結合實踐總結出的關鍵技術問題。

  • JS 沙箱和 CSS 隔離,是為了讓子應用之間互不影響。
  • Html Entry 和 Config Entry,是關於如何註冊子應用資訊。
  • 按需載入、公共依賴載入和預載入,是關於效能的,這些很重要,否則雖然上了微前端,但效能嚴重下降,或者由於升級引起線上故障,就得不償失了。
  • 父子應用通訊,顧名思義,無需解釋。
  • 子應用巢狀 和 子應用並行 是微前端的進階應用,在某些場景下會用到。


以上問題,我們都有解決方案,但可能有些還不完美,需要進一步嘗試。


乾貨分享:螞蟻金服前端框架和工程化實踐


這是部分問題的實現原理。

  • 首先子應用提供樣式、指令碼等配置,有內聯也有外鏈。
  • 先通過 SEMVER MAP 解決公共依賴不重複載入的問題,比如 antd、react 都只載一份。
  • 然後通過 xhr 拉外鏈的樣式和指令碼,實現按需載入。
  • 樣式會合併成一份,通過 <style> 寫入到 DOM 結構,子應用 unmout 時刪除,以此做到 CSS 隔離。
  • 指令碼通過記錄和 diff window 變數上的屬性來取到子應用匯出的生命週期方法,然後通過 eval + 基於 Proxy 實現的 Sandbox 實現 JS 沙箱。


更多實現細節,可以關注文章,分享之後我們會公佈。


乾貨分享:螞蟻金服前端框架和工程化實踐

正如前面所言,我們熱衷於開源,所以這套微前端方案在業務上驗證過之後,我們就把他開源了:https://github.com/umijs/qiankun

  • 核心取名為乾坤,意義是統一。
  • 然後,搭配 umi 外掛使用,效果會更好,比如我們建幾個 umi 應用,配置一個為主應用,其他的為子應用,然後串起來就能跑了。


乾貨分享:螞蟻金服前端框架和工程化實踐

這句話不是我說的,大家如果有發現更好的方案,可以找 @有知 探討下。

場景完備性

乾貨分享:螞蟻金服前端框架和工程化實踐


作為一個框架,你得有亮點;而作為一個企業級框架,你得滿足需求。而要滿足需求,該有的功能就必須有,亮不亮不管,得有,不能讓框架成為業務需求的瓶頸。

紅色的應用型別方面:

  • SPA 應該是目前用地最多的一種應用型別,但有時也會不滿足需求。
  • 比如運營頁面,多個頁面之間沒有一點點關係,也不需要互相跳轉,用 SPA 就沒有意義,這時候 MPA 可能更適合。
  • 比如語雀,我們的文件平臺,他有前臺、有後臺、有 PC 端、有無線端,如果整體是一個 SPA,不僅尺寸大,公共依賴的提取是個問題,不同場景之間可能還會相互影響,這時候,多 SPA 的組合會更適合他。
  • 微前端前面已經提過。
  • SSR 和 Prerender 則是為了更好的瀏覽器效能,順便解決 SEO 的問題。


藍色的部署模式方面:

  • Node 框架和 Java 框架是框架層的,我們需要通過 HTML 層與這些後端框架做一層對接。
  • 離線包是指支付寶的手機應用錢包,讓我們的應用可以快速打包成一個壓縮包,上傳到手機裡。
  • 等等。

當前端框架成為內部的一致選擇之後,就會被推著去做很多業務方面的事情,適配各種場景的需求。不過好在我們有外掛機制,上面大部分的需求都是業務方同學通過外掛和我們一起實現的。


專題研究

除了拳頭功能,要做一個框架,還不得不在一些專題上有深入的研究,很多知識點是需要徹底搞透的,這樣才能知道如何設計更合適。

路由

乾貨分享:螞蟻金服前端框架和工程化實踐


首先,我們既支援配置式路由,也支援約定式路由。配置式是實際需要,約定式是理想:

  • 約定式路由即以物理檔案的路徑作為路由,可減少冗餘的配置層。
  • 但是,這明顯沒有用 JSON 配置靈活,所以我們在命名上做了一些處理,實現 動態路由 和 巢狀路由。
  • 還不夠,比如要給路由加個 title 屬性的配置,所以我們又允許通過 yaml 註釋為路由提供額外的屬性配置。


功能方面我們最先是參考 next.js 做的,但發現 next.js 只支援簡單的路由功能,於是自己做了很多擴充套件,

  • 許可權路由,是否允許進入。
  • 切換動效。
  • 麵包屑,根據路由生成麵包屑。
  • 滾動條狀態,清空或保持。
  • keep-alive,來自 vue router,讓路由切掉後不銷燬。


由於我們是集中式的路由組織方式,並且管控了路由的渲染邏輯,所以基於路由就可以做很多事,

  • 標題切換,基於路由的標題切換。
  • dva model 繫結,和按需載入。
  • 埋點,路由切換時埋點。
  • 編譯時按需編譯。
  • 執行時按需載入,還有各種按需載入策略。
  • 生成選單,根據路由配置結合 antd 元件自動生成側邊欄選單。


這個列表每次分享時都會增加,很有想象空間。


乾貨分享:螞蟻金服前端框架和工程化實踐


這裡介紹一個大家可能感興趣的點,基於路由的按需編譯。就是比如我們有 1000 個頁面,而除錯時只要調其中的 5 個頁面,那隻編譯這 5 個就是最理想的。

這有幾種實現方式:

  • next.js 的,通過動態 entry 實現。
  • 我們的,通過臨時檔案實現。


臨時檔案的實現是這樣的,

  • 先用 Loading 元件佔位。
  • 當使用者訪問指定 url 時,才把相應路由的元件替換進去。

雖然有些取巧,但簡單有效。


乾貨分享:螞蟻金服前端框架和工程化實踐


我們的編譯是基於 webpack 的,誠如大家所料,啟動速度還是比較慢的,尤其是專案大了之後。為了讓使用者體驗更好,我們在這邊也做了很多嘗試,有正常的方式,也有不正常的方式。

正常的方式有:

  • dll,把不會修改到的部分打到 dll 裡,避免重複打包。
  • hard-source,利用物理檔案快取,但由於作者不維護,此方案已廢棄。
  • cache-loader、happypack。
  • external,比 dll 更有效的提速方案。
  • 硬體升級,簡單粗暴有效,有個案例是我們其中一個專案的 ci 需要 12 分鐘,換了臺機器後,只要 5 分鐘,所以有時做很多努力,不如換臺機器。
  • 簡化配置,只給當前專案需要的配置,比如多一個模組 resolve 規則,或者多載入不需要的 loader,都會降低編譯速度。
  • 按需編譯,在前面介紹過了。
  • webpack@5,有時做很多努力,不如升個大版本提升大,參考 node 升級帶來的效能提升。以 ant-design-pro 為例試驗了下 webpack@5 的物理快取能力,首次編譯需要 37s,二次編譯只要 4s!!
  • Plug'n'Play,和編譯關係不大,但能提升依賴安裝速度。


進階優化的有:

  • auto-external,external 雖然效果好,但配置麻煩,所以我們封裝了一個外掛解決配置麻煩的問題。
  • uglifyjs hash cache,構建差不多 70% 時間是在做壓縮,如果能把不需要壓縮的不壓縮,壓縮過的不重複壓縮,那會快很多。


“變態”優化的有:

  • 我們現在都在用 webpack,大家也可以想想,我們是否一定要用 webpack?
  • 我們目前是,三年之後可能就不是了。在上雲的大環境下,雲端跑 webpack 不僅成本高,而且效率低,我們可能會考慮低成本的方案,比如 codesandbox 或 stackbliz 的雲編譯方案,也有可能會藉助 rust 提升編譯器執行效率,現在社群已經有一些嘗試了。
  • 並且,隨著瀏覽器的發展,已經可以在瀏覽器裡用 esm 這種格式,所以未來也可能不再需要編譯器或者只要做一層很薄的合併操作。


乾貨分享:螞蟻金服前端框架和工程化實踐


效能優化是每個框架和每個前端都逃不開的點,從我 10 年前做前端起就關注這個點了,到目前方法有些變化,但效能優化依然很重要。下面我們的一些嘗試,

  • 按需載入,通常是以路由為維度的,但這裡還有些細節的點,比如載入到哪一層的路由,子路由是否應該合併到一個檔案裡,和路由相關的資料流檔案和國際化檔案如何按需載入,等等。
  • 一鍵切框架,對於一些無線場景,切成小尺寸的 react 實現能大幅降低產物大小,但需格外小心相容問題。
  • 公共檔案提取策略。
  • SSR + Prerender。
  • Prefetch 和 Preload。
  • modern mode,如果大家有聽說過,對,就是 vue-cli 的那個 modern mode


乾貨分享:螞蟻金服前端框架和工程化實踐


目前為止,因為瀏覽器的差異,我們仍需處理瀏覽器的相容問題。不過比第一代前端需要處理 IE6 的相容問題已經好多了。


關於補丁方案:

  • 元件不打補丁,這點上很多人有認知誤區,元件會做語法轉換,但不會包含補丁,因為包含補丁會造成冗餘。
  • 目前最常用的常規方案,如右圖所示,通過 targets 配置配需要相容啥瀏覽器的啥版本,實現上要注意需同時給到 babel 和 postcss,處理 JS 和 CSS。
  • 某些場景會很在意效能,多一個位元組都捨不得,比如無線,他們會追求 極限方案,強制寫死就打某幾個補丁,然後通過 eslint 外掛限制不能使用需要補丁的那些 es 語法,用了就報錯。
  • 最後是我個人理解的終極方案,線上補丁服務 + 本地特性檢測,本地特性檢測可以保證特性的最小化,線上補丁服務可以區分瀏覽器差異,保證特性瀏覽器下載固定補丁列表時的最小化。


乾貨分享:螞蟻金服前端框架和工程化實踐


編輯器外掛是框架非常重要的配套設施,很多功能在框架層其實沒法做,尤其是用了大量的約定之後,編碼時會損失程式碼提示方面的支援,利用編輯器外掛就能彌補這一點。


乾貨分享:螞蟻金服前端框架和工程化實踐


舉兩個例子,

  • 比如,dva 的資料流方案基於 redux,而 redux 的 action 是基於字串,很難利用 TypeScript 特性做自動提示。藉助 vscode 外掛即可做到這一點。
  • 再比如,umi 的路由配置是指向路由元件的路徑字串,框架層做不到提示補全,藉助 vscode 外掛也可以做到。


乾貨分享:螞蟻金服前端框架和工程化實踐


測試方面基本上和大家都一樣,包含單測、UI 測試、e2e 測試和整合測試,基本方案是基於 Jest + test-react-library + Puppeteer。


但是,大家都知道業務同學很忙,沒有太多時間寫測試。所以我們如果能有個基於路由的自動化測試方案,讓業務不寫程式碼也能確保每個路由都能正常執行,也是個不錯的選擇。


乾貨分享:螞蟻金服前端框架和工程化實踐


這是我們的監控體系,有了資料,才能知己知彼,有的放矢。


分構建時和執行時。


構建時在雲構建容器層去生成構建報告,我們自研的工具比較好辦,但就算在螞蟻內部,也還是其他工具的存在,比如直接用 webpack 做構建的,或者基於 webpack 封裝的。對於這些非自研的構建,我們會用猜測的方式,來定位出他是有什麼工具進行構建。


資料層會跑大量的定時任務去做資料清理,提供夠展示層。展示層提供排名、大盤、版本分佈、競品分析、出錯預警等資訊。


執行時沒啥特別的,大家的方法都差不多。有一點值得一提的是我們會在雲構建平臺去自動申請埋點標識並在構建時自動注入,讓使用者免去埋點標識的申請,所有產品自動就會有資料支撐。


乾貨分享:螞蟻金服前端框架和工程化實踐


這是一些構建時的資料展現示例。

未來和規劃

乾貨分享:螞蟻金服前端框架和工程化實踐


Bigfish + Umi 的內外結合的方式目前看起來還不錯,但畢竟是兩個團隊妥協後的方案,在我們需要服務外部 ISV 時暴露了一些問題:

  • Bigfish 是內網框架,綁了很多內部服務,不能直接給 ISV 用。
  • umi 給 ISV 又會存在一些差異。


雖然底層都是 umi,但內外網同學的使用方式還是有很大差別的,導致我們的方案對外時會有額外的成本,以及我們自己在文件等方面的投入上都需要做兩次。差異主要是:

  • 配置不完全一致。
  • 文件不統一。


所以,我們要 讓內外網的框架方案保持一致

  • 內部同學也統一用 Umi。
  • 修改 Umi 的外掛配置方式,和內部保持一致。
  • Umi 增加 Preset 的概念,之前的 Bigfish 框架提供 umi-preset-bigfish 服務內部同學。


修改後的這一版是我能預見的框架終態。


乾貨分享:螞蟻金服前端框架和工程化實踐

乾貨分享:螞蟻金服前端框架和工程化實踐


相關文章