基于模塊聯(lián)邦與大倉(cāng)模式的商家巨石應(yīng)用拆分實(shí)踐
一、背景
商家后臺(tái)前端代碼目前代碼量達(dá)到十萬(wàn)級(jí),每個(gè)迭代團(tuán)隊(duì)需要在同一倉(cāng)庫(kù)中迭代幾十個(gè)需求,在日漸龐大的巨石應(yīng)用下如此活躍的迭代,開發(fā)效率與構(gòu)建效率上給我們帶來(lái)了一些挑戰(zhàn),我們需要優(yōu)化以下幾點(diǎn):
- 代碼構(gòu)建體量大,隨著時(shí)間推移,構(gòu)建速度的優(yōu)化空間較少。
- 巨石應(yīng)用下各個(gè)業(yè)務(wù)模塊沒(méi)有做物理拆分,管理與維護(hù)難度提升。
- 應(yīng)用粒度較粗,在發(fā)布節(jié)點(diǎn)上需要對(duì)應(yīng)用做進(jìn)一步拆分以優(yōu)化發(fā)布粒度。
- 巨石應(yīng)用下,組件與業(yè)務(wù)的關(guān)系需要梳理,避免出現(xiàn)重復(fù)開發(fā)的情況。
微前端是目前解決應(yīng)用拆分的主要解決方案,但是由于其隔離性的機(jī)制使得各個(gè)子應(yīng)用間完全隔離,使得用戶在開發(fā)子應(yīng)用時(shí)無(wú)法訪問(wèn)其他子應(yīng)用頁(yè)面,這對(duì)于各子應(yīng)用存在關(guān)聯(lián)關(guān)系需要同時(shí)訪問(wèn)開發(fā)的場(chǎng)景開發(fā)效率較低,并且目前市面上已經(jīng)完全封裝的主流微前端框架對(duì)我們來(lái)說(shuō)是黑盒,無(wú)法做到高度自定義,無(wú)法滿足特定拆分需求,因此我們決定采用模塊聯(lián)邦與大倉(cāng)模式結(jié)合的方式解決以上問(wèn)題。
二、目標(biāo)
提升構(gòu)建與維護(hù)效率
將巨石應(yīng)用進(jìn)行拆分,獨(dú)立開發(fā)與部署、提升構(gòu)建部署效率與代碼維護(hù)效率。
雖是單個(gè)子應(yīng)用開發(fā),但可全量訪問(wèn)巨石應(yīng)用模塊
由于商家內(nèi)部各應(yīng)用間在業(yè)務(wù)上存在上下游的關(guān)系,我們?cè)陂_發(fā)D子應(yīng)用時(shí),需要在其上游的C、B子應(yīng)用中進(jìn)行相關(guān)配置,它們之間是強(qiáng)綁定的關(guān)系,結(jié)論就是:需要在本地開發(fā)時(shí),能夠同時(shí)使用其他子應(yīng)用的頁(yè)面模塊的功能。
三、技術(shù)方案
以上提到的提升構(gòu)建與維護(hù)效率,目前微前端與MF方案都是支持的,關(guān)鍵點(diǎn)在于如何在本地開發(fā)能夠訪問(wèn)到平臺(tái)的全量功能。本地代碼已經(jīng)有了,啟動(dòng)本地進(jìn)程即可,那么,其他子應(yīng)用如何訪問(wèn)?
微應(yīng)用代理
如果是從本地再啟動(dòng)其他應(yīng)用,這其實(shí)就背離了應(yīng)用拆分的初衷,同時(shí)也降低了本地開發(fā)的效率,同時(shí)隨著子應(yīng)用越來(lái)越多,本地進(jìn)程也會(huì)越來(lái)越多,這可能會(huì)大大影響本地開發(fā)效率。看來(lái)本地啟動(dòng)行不通,那么從哪里訪問(wèn)呢?對(duì)了,線上不是可以訪問(wèn)到其他模塊嗎?但是怎么將本地開發(fā)與線上的模塊結(jié)合呢?由于目前商家后臺(tái)使用了MF方式在路由層或vue文件內(nèi)遠(yuǎn)程引入MF模塊,那么是否可以通過(guò)MF模塊轉(zhuǎn)發(fā)的方式將遠(yuǎn)程組件代理至本地組件呢?
以上的組件代理插件實(shí)質(zhì)上是用于做MF組件加載管理的chrome插件,可以控制基座應(yīng)用加載的模塊是線上的還是本地。
代理方案如下
- 用戶首次進(jìn)入線上測(cè)試環(huán)境,會(huì)加載遠(yuǎn)程應(yīng)用入口文件,代理插件會(huì)將入口文件根據(jù)用戶配置轉(zhuǎn)發(fā)至對(duì)應(yīng)的本地地址。
- 進(jìn)入路由訪問(wèn),由于上一步對(duì)遠(yuǎn)程應(yīng)用入口做了代理,加載的模塊被代理至本地,這樣用戶就可以在線上訪問(wèn)到本地頁(yè)面,實(shí)現(xiàn)本地開發(fā)。
- 在線上基座應(yīng)用與本地應(yīng)用間建立websocket鏈接,本地應(yīng)用代碼更改后通知線上基座應(yīng)用刷新頁(yè)面。
下面是具體實(shí)現(xiàn):
generateRedirectUrl = (details: UrlDetailsType) => {
if (details.url.includes(MICRO_ONLINE_LOAD_PATH)) {
const redirectUrl = this.generateDefaultProxyUrl({ originUrl: details.url })
if (redirectUrl) {
console.log('觸發(fā)代理', `${details.url}代理至${redirectUrl}`)
this.checkMicroAppStatus({ originUrl: details.url, redirectUrl })
return {
redirectUrl,
}
}
}
if (details.url.includes('t1-dev.dewu.net:98')) {
console.log("details.url.includes('t1-dev.dewu.net:98')", details.url);
const redirectUrl = this.generateOnlineUrlByLocal({ originUrl: details.url })
if (redirectUrl) {
console.log('觸發(fā)代理', `${details.url}代理至${redirectUrl}`)
this.checkMicroAppStatus({ originUrl: details.url, redirectUrl })
return {
redirectUrl,
}
}
}
}
- 文件內(nèi)容代理為本地文件后,此時(shí)對(duì)應(yīng)的模塊加載path還是會(huì)加載線上路由,這里同樣需要做內(nèi)容代理。
- 由于是基于線上測(cè)試環(huán)境開發(fā),本地開發(fā)的頁(yè)面不僅需要在線上展示,并且本地代碼更新后需要觸發(fā)線上頁(yè)面更新,這是必不可少的步驟,我們基于websocket將本地與線上進(jìn)行連接。
- 不同子應(yīng)用動(dòng)態(tài)設(shè)置socketUrl與PingUrl。
function getHost() {
if (process.env.SOCKET_SERVER) {
return new URL(process.env.SOCKET_SERVER);
}
return location;
}
function getSocketUrl() {
let h = getHost();
let host = h.host;
host = `localhost:${PORT}`;
const isHttps = h.protocol === 'https:';
return `ws://${host}`;
}
function getPingUrl() {
const h = getHost();
return `${h.protocol}//${h.host}/__umi_ping`;
}
- 建立websocket連接,并定時(shí)觸發(fā)連接檢測(cè)。
let pingTimer = null;
let isFirstCompilation = true;
let mostRecentCompilationHash = null;
let hasCompileErrors = false;
let hadRuntimeError = false;
const pingUrl = getPingUrl();
if (!window[`${APP_NAME}UmiEntry`]) {
const socket = new WebSocket(getSocketUrl(), 'webpack-hmr');
socket.addEventListener('message', ({ data }) =>
__awaiter(void 0, void 0, void 0, function* () {
data = JSON.parse(data);
if (data.type === 'connected') {
console.log(`[webpack] connected.`);
// proxy(nginx, docker) hmr ws maybe caused timeout,
// so send ping package let ws keep alive.
pingTimer = setInterval(() => socket.send('ping'), 30000);
} else {
handleMessage(data).catch(console.error);
}
}),
);
}
- 本地需求開發(fā)共享部署態(tài)代碼的store與路由跳轉(zhuǎn)。
- 需求開發(fā)完成后進(jìn)行單個(gè)應(yīng)用部署,由于是本地代理,不影響測(cè)試訪問(wèn)。
基于模塊聯(lián)邦
前面的代理機(jī)制依賴于MF的遠(yuǎn)程加載,模塊聯(lián)邦加載機(jī)制可參考「掘金」平臺(tái)中題目為“最詳細(xì)的Module Federation的實(shí)現(xiàn)原理講解” 這篇文章?;谀K聯(lián)邦的微前端落地方案可以參考之前的一篇文章 基于Module Federation的模塊化跨棧方案探索。
本地與部署態(tài)基座應(yīng)用通過(guò)MF方案加載子應(yīng)用,同時(shí)部署態(tài)新增動(dòng)態(tài)加載保證遠(yuǎn)程組件的實(shí)時(shí)性,在加載入口文件處進(jìn)行監(jiān)控告警。
基座應(yīng)用為部署態(tài),在進(jìn)行MF加載時(shí),通過(guò)chrome插件動(dòng)態(tài)控制加載子文件路徑,開發(fā)態(tài)子應(yīng)用共享部署態(tài)代碼的store,路由注冊(cè)等基礎(chǔ)配置。
加載態(tài)依賴chrome插件做動(dòng)態(tài)代理,實(shí)現(xiàn)本地與其他測(cè)試環(huán)境構(gòu)建代碼的動(dòng)態(tài)切換,同時(shí)子應(yīng)用與部署態(tài)代碼建立websocket代碼更新鏈接,在子應(yīng)用更新代碼時(shí),實(shí)時(shí)刷新線上頁(yè)面。同時(shí)支持端口的動(dòng)態(tài)配置,一鍵關(guān)閉。
效果
以上介紹加載鏈路保證了構(gòu)建部署提速與功能的完整,較好的解決了應(yīng)用拆分功能不完備問(wèn)題。本次架構(gòu)優(yōu)化將構(gòu)建由15s減少至2.0s。業(yè)務(wù)需求部署速度由8min減少至2min。
四、應(yīng)用拆分
大倉(cāng)模式
應(yīng)用拆分只是目的,要實(shí)現(xiàn)這個(gè)目標(biāo)不僅僅要做拆分,對(duì)于商家后臺(tái)來(lái)說(shuō)各個(gè)應(yīng)用間的復(fù)用同等重要,由于是業(yè)務(wù)解耦,這意味著各應(yīng)用間存在更多可復(fù)用的功能與模塊。
同時(shí)不僅是商家后臺(tái)的部分模塊也會(huì)在交易后臺(tái)使用,既要保證應(yīng)用業(yè)務(wù)的解耦,同時(shí)要保證組件充分復(fù)用,大倉(cāng)模式是目前最合適的方案。
大倉(cāng)模塊化共享
由于商家后臺(tái)各個(gè)子應(yīng)用由于同屬商家整條業(yè)務(wù)鏈路,存在眾多可共用的組件和模塊,而npm發(fā)布模式本身給業(yè)務(wù)組件與業(yè)務(wù)項(xiàng)目帶來(lái)了一定隔離性,同時(shí)因?yàn)楦髯討?yīng)用業(yè)務(wù)上存在關(guān)聯(lián),很多大型模塊需要被多個(gè)子應(yīng)用引入,而這些大型模塊的迭代通常比較頻繁,同時(shí)需要對(duì)業(yè)務(wù)請(qǐng)求進(jìn)行封裝。這里我們使用了基于大倉(cāng)模式的源碼引入以達(dá)到代碼共用的目的。組件開發(fā)鏈路如下:
這里體現(xiàn)的是源碼引入的方式,在構(gòu)建態(tài)進(jìn)行通用模塊的打包構(gòu)建,這一點(diǎn)目前能跑通的背景是商家后臺(tái)本身是一個(gè)完整的應(yīng)用,現(xiàn)有的模式同樣是一個(gè)組件被多個(gè)模塊所使用,同時(shí)測(cè)試階段也是全量回歸。以下是大倉(cāng)組件基礎(chǔ)鏈路:
- 組件構(gòu)建發(fā)布使用標(biāo)準(zhǔn)的cli規(guī)范。
- 在提交MR節(jié)點(diǎn)與發(fā)布節(jié)點(diǎn)新增自動(dòng)化卡口。
- 通過(guò)依賴分析自動(dòng)化檢測(cè)單測(cè)運(yùn)行范圍。
- 組件發(fā)布時(shí)發(fā)布通知,提醒組件使用者,并運(yùn)行業(yè)務(wù)單測(cè)。
之后會(huì)對(duì)該部分做詳細(xì)介紹。
五、總結(jié)與思考
單應(yīng)用構(gòu)建->單頁(yè)面構(gòu)建?
以上主要講述了MF方案如何將本地結(jié)合線上開發(fā),這里僅對(duì)微應(yīng)用級(jí)別做了解耦,基于MF的模塊化實(shí)現(xiàn),由于remoteMicro實(shí)質(zhì)上是創(chuàng)建了一個(gè)引用路徑到require函數(shù)的映射然后代理至本地,那么對(duì)于不同模塊,在能力上是具備模塊化代理的能力的,基于目前MF按需構(gòu)建(僅構(gòu)建暴露出去的組件模塊)的規(guī)則,我們可以對(duì)某個(gè)模塊的映射對(duì)象里的xxx.async.js做代理。這樣就可以實(shí)現(xiàn)頁(yè)面粒度的按需構(gòu)建,在部署構(gòu)建提速上有很大潛力。
本篇文章主要介紹了如何對(duì)商家巨石應(yīng)用做拆分,包括拆分方案的介紹,如何同時(shí)保證單個(gè)構(gòu)建與功能完整性,并且針對(duì)微應(yīng)用代理加載進(jìn)行了進(jìn)一步探索,接著介紹了大倉(cāng)模式下需要遵循的規(guī)范以及未來(lái)的規(guī)劃。大倉(cāng)模式目前在前端平臺(tái)已經(jīng)持續(xù)不斷地完善,將來(lái)應(yīng)該會(huì)針對(duì)此模式做更詳細(xì)的介紹,在拆分這件事情上,對(duì)于構(gòu)建本身或許能被更加細(xì)粒度化,構(gòu)建文件的代理本質(zhì)上減少了代碼的構(gòu)建量,目前是通過(guò)人為控制的方式,此次驗(yàn)證了模塊聯(lián)邦支持可代理與動(dòng)態(tài)更改expose。基于這兩個(gè)特性,是否能將構(gòu)建做到更加局部化,這可能會(huì)成為構(gòu)建優(yōu)化的方向。應(yīng)用拆分一方面提升了開發(fā)人員的開發(fā)與部署效率,同時(shí)也對(duì)業(yè)務(wù)迭代流程做了業(yè)務(wù)解耦,明確了責(zé)任邊界,更有利于后臺(tái)應(yīng)用的開發(fā)需求管理,降低需求代碼維護(hù)成本。