我們假設(shè)有這么一個單體代碼庫,它使用了某種后端模板引擎或者系統(tǒng)(例如 EJS 或者 ERB),但沒有認(rèn)真考慮前端的設(shè)計需求?;蛘吒愀獾氖?,前端的開發(fā)早于 SPA 的出現(xiàn),或者還可能使用了類似 Ruby on Rails 那樣的框架。因此,JavaScript 文件(例如.js.erb 文件或 AEM 片段等等)中可能包含了后端變量。這種粗制濫造且各組件間緊密耦合的代碼庫幾乎無法進(jìn)行現(xiàn)代化升級。
我們當(dāng)然希望不要再在這個單體系統(tǒng)中開發(fā)前端代碼,我們希望轉(zhuǎn)向更加 JavaScript 化的生態(tài)系統(tǒng)——但具體該怎么做?
大多數(shù)企業(yè)都負(fù)擔(dān)不起(或者不愿負(fù)擔(dān))這種因工具淘汰帶來的重寫成本與停機(jī)時間。功能的演進(jìn)需要開發(fā)的支持,但要保持同樣的速度開發(fā)這些功能顯然越來越困難。
正因如此,我們應(yīng)該通過一種漸進(jìn)且平滑的方式將單體逐步拆分為更多較小的部分,同時保證不讓業(yè)務(wù)發(fā)生中斷。
說來簡單,但單體架構(gòu)的拆分過程相當(dāng)棘手。在進(jìn)行前端遷移時需要為支持 JavaScript 的應(yīng)用程序規(guī)劃和開發(fā)新 API,拆分就變得尤為困難。
在等待新 API 開發(fā)和發(fā)布的過程中,前端迭代開發(fā)、微前端(MFE)實(shí)現(xiàn)和團(tuán)隊(duì)的自主行動都將陷入僵局。但真的要這樣子嗎?錯!我們可以對前端和后端進(jìn)行解耦,讓它們齊頭并進(jìn)。
Zack Jackson — ScriptedAlchemy
下面將介紹一種方法,它能夠順利解耦前端,并將其移植成具有 SSR 的獨(dú)立 MFE。如此一來,團(tuán)隊(duì)不再需要等待后端 API 被拆分成微服務(wù)或者等待后端 API 可用。這個方法叫作“由內(nèi)而外替換單體”。
微前端通常包含以下兩大重要依賴項(xiàng):
1) 認(rèn)證;
2) 提供給應(yīng)用程序的數(shù)據(jù),在瀏覽器端和在服務(wù)器端渲染(SSR)期間。
根據(jù)我的個人經(jīng)驗(yàn),無論你的遺留系統(tǒng)屬于 Rails、Java 還是.Net,用戶身份認(rèn)證一直是最難與單體后端進(jìn)行剝離的部分。
MFE 有多種不同的架構(gòu)規(guī)范。本文將重點(diǎn)介紹其中一種,即在后端微服務(wù)中非常流行的一個版本——LOSA(Lots Of Small Applications,大量小應(yīng)用)。對于“由內(nèi)而外”遷移來說,這是最理想的選擇。
流經(jīng)單體的 LOSA 請求 / 響應(yīng)流
LOSA 應(yīng)用(通常為微前端)屬于獨(dú)立的 Node.js 服務(wù),能夠在服務(wù)器端渲染網(wǎng)頁的一部分或者某些片段。每個頁面可以由多個 LOSA 服務(wù)組成。這些應(yīng)用程序 / 微前端單獨(dú)進(jìn)行構(gòu)建、部署,并運(yùn)行在容器中。
上圖所示為同一頁面采用了三種不同的渲染方式,演示了一個增量遷移的過程。先是單體渲染頁面,再過渡到 LOSA 微前端,然后變成垂直的微前端。最后,單體被徹底替換掉。
當(dāng)然,單體仍然負(fù)責(zé)處理 HTTP 請求,并將最終響應(yīng)發(fā)送至客戶端。微前端可以放在集群的防火墻后面——僅提供給遺留系統(tǒng)使用,直到 API 網(wǎng)關(guān)和用戶身份認(rèn)證機(jī)制剝離完成(或者至少已經(jīng)轉(zhuǎn)化為 API 端點(diǎn))。在此期間,我們不需要做太多的改動。
下圖展示了遷移后的請求 / 響應(yīng)流程。
GET/POST 'https://MFEwebsite.com/parts/header?format=json
渲染頁面內(nèi)容需要各類數(shù)據(jù),那些無法從已解耦端點(diǎn)查詢到的“缺失”信息可以在請求期間以 props 的形式發(fā)送給 MFE。請求會經(jīng)過一系列中間件,這些中間件負(fù)責(zé)渲染 React 應(yīng)用程序,然后調(diào)用已解耦的 API,并將響應(yīng)結(jié)果以 props 的形式返回。這些 props 最終將組成 window.INITIAL_STATE。
關(guān)于模板功能或者過濾器的實(shí)現(xiàn)方法,我向大家推薦 Hypernova。不過我自己并沒用過,我已經(jīng)習(xí)慣了一切自己動手,并在 Rails、Node 以及 PHP 后端中實(shí)現(xiàn)了類似的機(jī)制。但考慮到各類后端平臺都有自己的特點(diǎn),所以這里我就用 Hypernova 作為示例向大家講解。
下面使用 express 實(shí)現(xiàn) MFE 渲染端點(diǎn):
GET/POST 'https://MFEwebsite.com/parts/header?format=json
{
html: '<div> ... </div>',
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));
});
}
發(fā)出這些初始 POST 請求的控制器也需要處理響應(yīng)結(jié)果,將 JS 與 CSS 放在正確的位置,最后在遺留模板的對應(yīng)位置渲染 React。之前通常由其他控制器負(fù)責(zé)處理的資產(chǎn)現(xiàn)在需要負(fù)責(zé)將腳本與樣式注入到遺留標(biāo)頭與 body 標(biāo)簽的底部。請注意,單體仍然被作為布局引擎。我們也在替換其他部分,并以 React SSR 方式添加新功能。最終,這些 LOSA 應(yīng)用將通過一個 MFE(或者借助 Webpack 黑魔法,我自己開發(fā)了 webpack-external-import)整合在一起。
在遷移中,解耦并上線新的 API 到底會帶來怎樣的影響?
之前,在單體把數(shù)據(jù)傳給 MFE 時,express 訪問 HTTP 的請求正文。而現(xiàn)在,express 向 API 異步獲取數(shù)據(jù)。雖然數(shù)據(jù)格式可能會發(fā)生變化,但 React 仍然能夠正確獲取到 props。
與舊單體相比,LOSA 架構(gòu)的性能還不夠好,通常需要 400 到 600 毫秒才能渲染出頁面的特定部分。我們采用了異步 Worker 結(jié)構(gòu),這樣就可以同時請求多項(xiàng)服務(wù)來渲染應(yīng)用程序的不同部分(而不是渲染單個應(yīng)用)。但這種作法提高了應(yīng)用下線的難度,因?yàn)椤吧a(chǎn)故障”會導(dǎo)致側(cè)邊欄或頁腳部分長時間缺失。因此,進(jìn)一步拆分才是最好的選擇。
我所說的 LOSA 異步 Worker 是這樣的:我們使用大量的 Node 服務(wù),每一個服務(wù)負(fù)責(zé)渲染頁面的一個或多個組件。
遺留控制器(圖中的灰色齒輪部分)可以將視圖數(shù)據(jù)轉(zhuǎn)給 POST 請求,而非后端模板引擎?;厥諗?shù)據(jù)機(jī)制則能夠幫助后端減少支持負(fù)擔(dān)。由于無需做出重大修改,后端開發(fā)人員能夠騰出時間,專注于解耦數(shù)據(jù)服務(wù),而前端也可以進(jìn)行獨(dú)立的開發(fā)。
視圖數(shù)據(jù)被發(fā)送給了外部的 React 服務(wù),而響應(yīng)消息(包含了 HTML、樣式表、初始狀態(tài)以及 CSS URL)則被發(fā)送給后端模板引擎?,F(xiàn)在,模板引擎只需要渲染 POST 請求所對應(yīng)的響應(yīng),從而將視圖或視圖的一部分與原有單體剝離開來。
React 真的很慢!SSR 也不怎么快——因此新的 LOSA 架構(gòu)解決方案無法帶來理想的性能表現(xiàn)。我們的解決方案是:在 React 內(nèi)部進(jìn)行片段緩存。
黃色:無 React 片段緩存——端到端(400 毫秒左右)
深紫:有 React 片段緩存——端到端(150 毫秒左右)
橙色:全優(yōu)化架構(gòu)(20 毫秒左右)
綠色(底部):來自后端的原生片段緩存
React 優(yōu)化工作相當(dāng)復(fù)雜,受篇幅所限,恐怕只能另起一篇文章詳加說明了??傊?,Graphana 數(shù)據(jù)顯示,我們至少將渲染性能提高了一倍,不過輪循時間仍然很長。盡管 React 已經(jīng)能夠在內(nèi)部快速完成渲染,但 150 毫秒的端到端時間還沒有達(dá)到我們的預(yù)期。在下一篇文章中,我們將具體聊聊片段后端與片段緩存。
渲染時間一直是個麻煩事,即使是在 React 中采用了片段緩存之后,性能仍然無法令人滿意。令我感到失望的是,雖然 Node.js 內(nèi)部的渲染速度很快(約 20 毫秒),但整個流程仍然需要 140 到 200 毫秒才能完成。
瓶頸所在
單體架構(gòu)與 MFE 之間的 HTTP 開銷——也就是 gRPC、CQRS、UDP 以及 Protobuf。二者之間的通信應(yīng)該通過內(nèi)部 Kubernetes 網(wǎng)絡(luò)進(jìn)行。POST 速度很慢,但也不是不能用。遇到問題時個別處理即可。
簡單來說,模板化、片段緩存與 gRPC/CQRS,移除 JSON 中臃腫的數(shù)據(jù)。React 在服務(wù)器端速度較慢,但請記住,一切拆分都只會讓速度變得稍慢,而不是更快。
開發(fā)人員在風(fēng)險較低的環(huán)境中可以快速行動,業(yè)務(wù)人員能夠?qū)⑿孪敕ㄍ葡蚴袌?,并對出現(xiàn)問題的部分及時回滾——快速行動的能力正是實(shí)現(xiàn)高成本效益的必要前提。
流量: 1000 萬次渲染 / 天
實(shí)例: 5
內(nèi)存: 100mi (100 MB 內(nèi)存)
CPU: 100 (單核)
最大 CPU 使用率閾值: 65%
響應(yīng)時間:20 至 25 毫秒
DOM 復(fù)雜度:高
響應(yīng)時間縮短了 95%
綠色:后端渲染時間
藍(lán)色:使用了片段緩存和 state 優(yōu)化的 React
我的單線程 JavaScript 應(yīng)用程序要比使用完整片段緩存的多線程后端系統(tǒng)更快。
原文鏈接:https://levelup.gitconnected.com/micro-frontend-architecture-replacing-a-monolith-from-the-inside-out-61f60d2e14c1
偶發(fā) bug 大大增加了排查的成本,復(fù)現(xiàn)也變成了所有研發(fā)心中的痛。在即將召開的 ArchSummit 全球架構(gòu)師峰會(北京站)上,貝殼找房基礎(chǔ)架構(gòu)中心前端架構(gòu)委員會專家陳辰將為大家?guī)碡悮ぷ匝斜O(jiān)控平臺燈塔之外的另一項(xiàng)目——時光機(jī),揭秘如何利用時光機(jī)讓偶現(xiàn) bug 無所遁形。