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

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


框架發展歷史


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


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


在 Umi 和 Bigfish 時代,咱們從刀耕火種的時代跨入了工業化時代。由於在此以前,用戶須要接觸不少技術棧和細節,在 Umi 和 Bigfish 中,用戶只要知道一個框架,剩下的所有不用瞭解。框架像一個魔法球,把各類技術棧吸到一塊兒,加工後吐給用戶,以此來支撐業務。html



在兩個框架合併以後,咱們的現狀是這樣:前端

  • umi 對外開源,bigfish 對內服務內部同窗。
  • bigfish 扔掉原有實現,改形成 umi + umi 插件集的一個架構。
  • 咱們不是第一個這麼作的,相似的還有 eggjs 和 chair。這是一種很好的方式,開源和業務兩不誤。
  • 那麼,這是咱們的框架終局嗎?以及是否還有更好的方式?你們也能夠思考下,後面在將來規劃區域會有探討。



這是一些螞蟻的內部數據:vue

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

目前來看,這個框架基本統一了內部的框架使用狀況,不只有不熟前端的 Java 開發,略熟前端的外包,還有資深的前端同窗。要得到那麼多同窗的承認,並非件容易的事。java


爲何咱們能成

那麼,爲何咱們能成?我的理解,我以爲有幾個關鍵詞:
node

  • 業務
  • 流程
  • 開源


人是很是重要的一環,甚至比技術自己更重要一些。react


那麼別人爲啥要用你的框架?首先,框架要好用,這是最基本的;而後,使用者尤爲是資深的前端同窗,還得在這上面找到本身的成就感和 ownership,另外若是績效漂亮就更好了。總不能別人用你的框架,而後只有你本身一我的的績效好,那是不會長久的。webpack


咱們的解法是插件體系。git



框架不是憑空而來的,需求來自於業務,因此用框架寫業務的同窗每每能發現框架不足的點,他們能夠開發適用於本身業務的框架插件,反哺框架。若是這是通用需求,那就亮了。框架的內部開發羣有 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 服務內部同窗。


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


相關文章
相關標籤/搜索