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

在極客邦科技前兩天召開的 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、微前端等等;

  • 可以擴展部署模式,比如和不同的框架或平臺做結合;

這是插件生命周期圖,包含:

  1. node 環境執行的編譯時

  2. 瀏覽器上執行的運行時

  3. 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 個人的活。關于提效,其中比較重要的是相同的代碼不要重復寫,要做提取和組件化。而資產市場就是做的這件事。

為了更有效地復用,我們對資產市場分了四級:

  1. 組件,指通用組件,就是 antd,在下半年將要發布的 [email protected] 里,我們會陸續提取更多通用組件到 antd 中。

  2. 業務組件,不能提取通用組件的,我們會提到內部統一的業務組件倉庫中。

  3. 區塊,由組件組成,可以想象成代碼片段。

  4. 頁面模板,由區塊組成

我們可以借助工具把區塊和頁面模板添加到頁面中。

通過 Umi UI(可視化方式)添加區塊的樣式。

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

  • 最初的思路來源是 angular 的一個 theme market,以及飛冰。

  • 1.0 的版本時我們設計區塊是頁面級的,用戶可以在一個頁面里寫組件、數據流方案、mock 等等,這樣我們要做一個基于 antd 的 CRUD 頁面就很簡單,一個命令把區塊拿進來,然后修修改改就完事了。

  • 然后今年我們重新整理了區塊方案,因為我們希望區塊能更通用一些,比如可重復添加,可無限嵌套,支持區塊集,可結合布局,支持可視化添加等等。

這是區塊方案的迭代情況,一路踩著坑過來的。

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

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

  • 設計師,在設計師工具層同步資產市場,讓大家在設計時就按照約定的方式走。

  • 資產開發者,提供組件開發工具,包括組件的打包、文檔、本地調試、測試、發布、自動生成 CHANGELOG 等等。

  • 資產使用者,同時提供命令行和可視化工具,命令行是兜底方案,可視化的方式添加資產則更友好。

微前端

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

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

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

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

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

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

  • MPA(多頁應用)

  • 微前端

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

微前端的概念其實已經出來 3 年多了 ( https://www.thoughtworks.com/radar/techniques/micro-frontends

),但社區喊地比較多,給方案比較少,在生產環境應用地就更少了。

我們首先是基于 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 應用,配置一個為主應用,其他的為子應用,然后串起來就能跑了。

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

場景完備性

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

紅色的應用類型方面:

  • 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 實現。

  • 我們的,通過臨時文件實現。

臨時文件的實現是這樣的,

  1. 先用 Loading 組件占位。

  2. 當用戶訪問指定 url 時,才把相應路由的組件替換進去。

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

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

正常的方式有:

  • dll,把不會修改到的部分打到 dll 里,避免重復打包。

  • hard-source,利用物理文件緩存,但由于作者不維護,此方案已廢棄。

  • cache-loader、happypack。

  • external,比 dll 更有效的提速方案。

  • 硬件升級,簡單粗暴有效,有個案例是我們其中一個項目的 ci 需要 12 分鐘,換了臺機器后,只要 5 分鐘,所以有時做很多努力,不如換臺機器,:cry:。

  • 簡化配置,只給當前項目需要的配置,比如多一個模塊 resolve 規則,或者多載入不需要的 loader,都會降低編譯速度。

  • 按需編譯,在前面介紹過了。

  • [email protected],有時做很多努力,不如升個大版本提升大,參考 node 升級帶來的性能提升,:cry:。以 ant-design-pro 為例試驗了下 [email protected] 的物理緩存能力,首次編譯需要 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 服務內部同學。

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

我來評幾句
登錄后評論

已發表評論數()

相關站點

+訂閱
熱門文章
贵州11选5走势图软件