正文
如何利用微前端技術實現單體應用程序的現代化改造?在本篇教程中,我們將探討如何將前端從單體架構當中剝離出來,並快速完成微前端架構遷移。本文作者將結合個人項目實踐經驗為大家介紹心得。
問題所在
我們假設有這麼一個單體代碼庫,它使用了某種後端模板引擎或者系統(例如 EJS 或者 ERB),但沒有認真考慮前端的設計需求。或者更糟糕的是,前端的開發早於 SPA 的出現,或者還可能使用了類似 Ruby on Rails 那樣的框架。因此,JavaScript 文件(例如.js.erb 文件或 AEM 片段等等)中可能包含了後端變量。這種粗製濫造且各組件間緊密耦合的代碼庫幾乎無法進行現代化升級。
我們當然希望不要再在這個單體系統中開發前端代碼,我們希望轉向更加 JavaScript 化的生態系統——但具體該怎麼做?
大多數企業都負擔不起(或者不願負擔)這種因工具淘汰帶來的重寫成本與停機時間。功能的演進需要開發的支持,但要保持同樣的速度開發這些功能顯然越來越困難。
正因如此,我們應該通過一種漸進且平滑的方式將單體逐步拆分為更多較小的部分,同時保證不讓業務發生中斷。
說來簡單,但單體架構的拆分過程相當棘手。在進行前端遷移時需要為支持 JavaScript 的應用程序規劃和開發新 API,拆分就變得尤為困難。
在等待新 API 開發和發佈的過程中,前端迭代開發、微前端(MFE)實現和團隊的自主行動都將陷入僵局。但真的要這樣子嗎?錯!我們可以對前端和後端進行解耦,讓它們齊頭並進。
Zack Jackson — ScriptedAlchemy
下面將介紹一種方法,它能夠順利解耦前端,並將其移植成具有 SSR 的獨立 MFE。如此一來,團隊不再需要等待後端 API 被拆分成微服務或者等待後端 API 可用。這個方法叫作“由內而外替換單體”。
阻礙因素
微前端通常包含以下兩大重要依賴項:
1) 認證;
2) 提供給應用程序的數據,在瀏覽器端和在服務器端渲染(SSR)期間。
根據我的個人經驗,無論你的遺留系統屬於 Rails、Java 還是.Net,用戶身份認證一直是最難與單體後端進行剝離的部分。
將單體作為佈局引擎
MFE 有多種不同的架構規範。本文將重點介紹其中一種,即在後端微服務中非常流行的一個版本——LOSA(Lots Of Small Applications,大量小應用)。對於“由內而外”遷移來說,這是最理想的選擇。
流經單體的 LOSA 請求 / 響應流
LOSA 應用(通常為微前端)屬於獨立的 Node.js 服務,能夠在服務器端渲染網頁的一部分或者某些片段。每個頁面可以由多個 LOSA 服務組成。這些應用程序 / 微前端單獨進行構建、部署,並運行在容器中。
上圖所示為同一頁面採用了三種不同的渲染方式,演示了一個增量遷移的過程。先是單體渲染頁面,再過渡到 LOSA 微前端,然後變成垂直的微前端。最後,單體被徹底替換掉。
當然,單體仍然負責處理 HTTP 請求,並將最終響應發送至客戶端。微前端可以放在集群的防火牆後面——僅提供給遺留系統使用,直到 API 網關和用戶身份認證機制剝離完成(或者至少已經轉化為 API 端點)。在此期間,我們不需要做太多的改動。
渲染流程
下圖展示了遷移後的請求 / 響應流程。
首先,發出一個請求:
GET/POST 'http://MFEwebsite.com/parts/header?format=json
渲染頁面內容需要各類數據,那些無法從已解耦端點查詢到的“缺失”信息可以在請求期間以 props 的形式發送給 MFE。請求會經過一系列中間件,這些中間件負責渲染 React 應用程序,然後調用已解耦的 API,並將響應結果以 props 的形式返回。這些 props 最終將組成 window.INITIAL_STATE。
代 碼
關於模板功能或者過濾器的實現方法,我向大家推薦 Hypernova。不過我自己並沒用過,我已經習慣了一切自己動手,並在 Rails、Node 以及 PHP 後端中實現了類似的機制。但考慮到各類後端平臺都有自己的特點,所以這裡我就用 Hypernova 作為示例向大家講解。
下面使用 express 實現 MFE 渲染端點:
來自另一個系統的請求(在這裡就是那個單體):
GET/POST 'http://MFEwebsite.com/parts/header?format=json
{
html: '...',
css: '/static/header.3042u3298423.css',
js: '/static/header.idhf93hf23iu.js',
initial_state: {items:[...]}
}
用於處理響應的中間件:
export function exampleRenderAPIware(req, res) {
const renderedMarkup = renderHTMLpage(
req,
this.index,
intial_state,
);
asyncRender.then(() => {
const responseObject = {
html: renderedMarkup,
initial_state,
js: jsResource,
css: cssResource,
};
res.status(200).end(JSON.stringify(responseObject));
});
}
發出這些初始 POST 請求的控制器也需要處理響應結果,將 JS 與 CSS 放在正確的位置,最後在遺留模板的對應位置渲染 React。之前通常由其他控制器負責處理的資產現在需要負責將腳本與樣式注入到遺留標頭與 div 標籤的底部。請注意,單體仍然被作為佈局引擎。我們也在替換其他部分,並以 React SSR 方式添加新功能。最終,這些 LOSA 應用將通過一個 MFE(或者藉助 Webpack 黑魔法,我自己開發了 webpack-external-import)整合在一起。
如何從模板數據遷移至新 API?
在遷移中,解耦並上線新的 API 到底會帶來怎樣的影響?
之前,在單體把數據傳給 MFE 時,express 訪問 HTTP 的請求正文。而現在,express 向 API 異步獲取數據。雖然數據格式可能會發生變化,但 React 仍然能夠正確獲取到 props。
性能差異
與舊單體相比,LOSA 架構的性能還不夠好,通常需要 400 到 600 毫秒才能渲染出頁面的特定部分。我們採用了異步 Worker 結構,這樣就可以同時請求多項服務來渲染應用程序的不同部分(而不是渲染單個應用)。但這種作法提高了應用下線的難度,因為“生產故障”會導致側邊欄或頁腳部分長時間缺失。因此,進一步拆分才是最好的選擇。
我所說的 LOSA 異步 Worker 是這樣的:我們使用大量的 Node 服務,每一個服務負責渲染頁面的一個或多個組件。
遺留控制器(圖中的灰色齒輪部分)可以將視圖數據轉給 POST 請求,而非後端模板引擎。回收數據機制則能夠幫助後端減少支持負擔。由於無需做出重大修改,後端開發人員能夠騰出時間,專注於解耦數據服務,而前端也可以進行獨立的開發。
視圖數據被髮送給了外部的 React 服務,而響應消息(包含了 HTML、樣式表、初始狀態以及 CSS URL)則被髮送給後端模板引擎。現在,模板引擎只需要渲染 POST 請求所對應的響應,從而將視圖或視圖的一部分與原有單體剝離開來。
React 渲染時間
React 真的很慢!SSR 也不怎麼快——因此新的 LOSA 架構解決方案無法帶來理想的性能表現。我們的解決方案是:在 React 內部進行片段緩存。
- 黃色:無 React 片段緩存——端到端(400 毫秒左右)
- 深紫:有 React 片段緩存——端到端(150 毫秒左右)
- 橙色:全優化架構(20 毫秒左右)
- 綠色(底部):來自後端的原生片段緩存
React 優化工作相當複雜,受篇幅所限,恐怕只能另起一篇文章詳加說明了。總之,Graphana 數據顯示,我們至少將渲染性能提高了一倍,不過輪循時間仍然很長。儘管 React 已經能夠在內部快速完成渲染,但 150 毫秒的端到端時間還沒有達到我們的預期。在下一篇文章中,我們將具體聊聊片段後端與片段緩存。
渲染時間 VS 輪迴時間
渲染時間一直是個麻煩事,即使是在 React 中採用了片段緩存之後,性能仍然無法令人滿意。令我感到失望的是,雖然 Node.js 內部的渲染速度很快(約 20 毫秒),但整個流程仍然需要 140 到 200 毫秒才能完成。
瓶頸所在
- JSON 大小,特別是初始應用狀態——即渲染頁面所需要的最少 state。我們不再在初始渲染中放置太多字符串化的 state,只發送足夠讓 React 完成渲染並讓摺疊組件變得可交互的必要 state。
- 需要渲染的 DOM 節點數量——不再將代碼放在無用的 DIV 中,只需要給它們加個 class。利用 HTML 的語義特性以及 CSS 的級聯效果,我們可以少寫一些 HTML 代碼,這樣也就減少了 React.createComponent 函數的生成。
- 垃圾回收——我們將在下一篇文章中討論更多細節。
- 速度由數據服務決定——在中間層使用 Redis。很多朋友認為“緩存失效問題難以解決”,我建議各位認真考慮一下事件溯源,或者我們可以使用 CQRS 與異步 Worker 來處理讀寫操作。
- 單體架構與 MFE 之間的 HTTP 開銷——也就是 gRPC、CQRS、UDP 以及 Protobuf。二者之間的通信應該通過內部 Kubernetes 網絡進行。POST 速度很慢,但也不是不能用。遇到問題時個別處理即可。
如何提升後端渲染性能
簡單來說,模板化、片段緩存與 gRPC/CQRS,移除 JSON 中臃腫的數據。React 在服務器端速度較慢,但請記住,一切拆分都只會讓速度變得稍慢,而不是更快。
伸縮問題如何解決?
對於一切解決方案,如果不能在規模化場景下實現良好的成本效益,那麼都將只是空談。我們絕對不能容忍天文數字級的運營成本或者糟糕的性價比。大規模且廉價的解決方案才是好的解決方案。下面來看幾點容易被忽視的成本要素:
- 昂貴的第三方服務費用;
- 更多 / 更大的容器環境;
- 由於性能不佳而導致的收入損失;
- 由於兩個分支無法同時被合併到 master,因此單體架構會導致發佈週期或部署流程阻塞;
- 開發人員在風險較低的環境中可以快速行動,業務人員能夠將新想法推向市場,並對出現問題的部分及時回滾——快速行動的能力正是實現高成本效益的必要前提。
最終結果
流量: 1000 萬次渲染 / 天
資源分配:
- 實例: 5
- 內存: 100mi (100 MB 內存)
- CPU: 100 (單核)
- 最大 CPU 使用率閾值: 65%
- 響應時間:20 至 25 毫秒
- DOM 複雜度:高
- 響應時間縮短了 95%
- 綠色:後端渲染時間
- 藍色:使用了片段緩存和 state 優化的 React
我的單線程 JavaScript 應用程序要比使用完整片段緩存的多線程後端系統更快。
原文鏈接:levelup.gitconnected.com/micro-frontend-architecture-replacing-a-monolith-from-the-inside-out-61f60d2e14c1
閱讀更多 BigDataKer 的文章