微前端在美團(tuán)外賣的實(shí)踐(美團(tuán)微前端架構(gòu))
微前端是微服務(wù)理念在前端的應(yīng)用。之前給大家介紹過(guò)微前端在美團(tuán)HR系統(tǒng)和美團(tuán)閃購(gòu)的實(shí)踐文章。今天的文章來(lái)自美團(tuán)外賣廣告團(tuán)隊(duì),他們參考業(yè)界優(yōu)秀方案,同時(shí)也深度結(jié)合了廣告端實(shí)際業(yè)務(wù)的情況,提出了基于React的中心路由基座式微前端方案。
背景
微前端是一種利用微件拆分來(lái)達(dá)到工程拆分治理的方案,可以解決工程膨脹、開(kāi)發(fā)維護(hù)困難等問(wèn)題。隨著前端業(yè)務(wù)場(chǎng)景越來(lái)越復(fù)雜,微前端這個(gè)概念最近被提起得越來(lái)越多,業(yè)界也有很多團(tuán)隊(duì)開(kāi)始探索實(shí)踐并在業(yè)務(wù)中進(jìn)行了落地。可以看到,很多團(tuán)隊(duì)也遇到了各種各樣的問(wèn)題,但各自也都有著不同的處理方案。誠(chéng)然,任何技術(shù)的實(shí)現(xiàn)都要依托業(yè)務(wù)場(chǎng)景才會(huì)變得有意義,所以在闡述美團(tuán)外賣廣告團(tuán)隊(duì)的微前端實(shí)踐之前,我們先來(lái)簡(jiǎn)單介紹一下外賣商家廣告端的業(yè)務(wù)形態(tài)。目前,我們開(kāi)發(fā)和維護(hù)的系統(tǒng)主要包括三端:
- PC系統(tǒng):?jiǎn)伍T店投放系統(tǒng)PC端
- H5系統(tǒng):?jiǎn)伍T店投放系統(tǒng)H5端
- KA系統(tǒng):多門店投放系統(tǒng)PC端
如上圖所示,原始解決方案的三端由各自獨(dú)立開(kāi)發(fā)和維護(hù),各自包含所有的業(yè)務(wù)線,而我們的業(yè)務(wù)開(kāi)發(fā)情況是:
- PC端和H5端相同業(yè)務(wù)線的基本業(yè)務(wù)邏輯一致,UI差異大。
- PC端和KA端相同業(yè)務(wù)線的部分業(yè)務(wù)邏輯一致,UI差異小。
在這種特殊的業(yè)務(wù)場(chǎng)景下,就會(huì)出現(xiàn)一個(gè)有關(guān)開(kāi)發(fā)效率的抉擇問(wèn)題。即我們希望能復(fù)用的部分只開(kāi)發(fā)一次,而不是三次。那么接下來(lái),就有兩個(gè)問(wèn)題擺在我們面前:
- 如何進(jìn)行物理層面的復(fù)用(不同端的代碼在不同地址的Git倉(cāng)庫(kù))。
- 如何進(jìn)行邏輯層面的復(fù)用(不同端的相同邏輯如何使用一份代碼進(jìn)行抽象)。
我們這里重點(diǎn)看一下物理層面的復(fù)用,即:如何在物理空間上使得各自獨(dú)立的三端系統(tǒng)(不同倉(cāng)庫(kù))引入我們的復(fù)用層?我們嘗試了NPM包、Git subtree等類“共享文件”的方式后發(fā)現(xiàn),最有效率的復(fù)用方式是把三個(gè)系統(tǒng)放在一個(gè)倉(cāng)庫(kù)里,去消除物理空間上的隔離,而不是去連接不同的物理空間。當(dāng)然,我們?nèi)讼到y(tǒng)的技術(shù)棧是一致的,所以就進(jìn)行了如下圖的改造:
可以看到,當(dāng)我們把三端系統(tǒng)放在一個(gè)倉(cāng)庫(kù)中時(shí),通過(guò)common文件夾提供了物理層面可復(fù)用的土壤,不再需要“共享文件”式地進(jìn)行頻繁地拉取操作,直接引用復(fù)用即可。不過(guò),在帶來(lái)物理層面復(fù)用效率提升的同時(shí),也加速了整個(gè)工程出現(xiàn)了爆炸式發(fā)展的問(wèn)題,隨著產(chǎn)品線從最初的幾個(gè)發(fā)展到現(xiàn)在的幾十個(gè)之多,工程管理成本也在迅速增長(zhǎng)。具體來(lái)說(shuō),包括如下四個(gè)方面:
- 新業(yè)務(wù)線產(chǎn)品急速增加,同時(shí)為了保證三端系統(tǒng)復(fù)用效率的最大化,把文件放入同一倉(cāng)庫(kù)管理,導(dǎo)致文件數(shù)量增長(zhǎng)極快,管理及協(xié)同開(kāi)發(fā)難度也在不斷加大。
- 文件越來(lái)越多,文件結(jié)構(gòu)越不受控制,業(yè)務(wù)開(kāi)發(fā)尋址變得越來(lái)越困難。
- 文件越來(lái)越多,開(kāi)發(fā)、構(gòu)建、部署速度變得越來(lái)越慢,開(kāi)發(fā)體驗(yàn)在持續(xù)下降。
- 不同業(yè)務(wù)線間沒(méi)有物理隔離,出現(xiàn)了跨業(yè)務(wù)線互相引用混亂,例如A業(yè)務(wù)線出現(xiàn)了B業(yè)務(wù)線名字的組件。
如下圖所示,具體地說(shuō)明了原有架構(gòu)存在的問(wèn)題。為了要解決這些問(wèn)題,我們意識(shí)到需要拆分這些應(yīng)用,即進(jìn)行工程優(yōu)化的常規(guī)手段進(jìn)行“分治”。那么要怎么拆呢?自然而然地我們就想到了微前端的概念。也從這個(gè)概念出發(fā),我們參考業(yè)界優(yōu)秀方案,同時(shí)也深度結(jié)合了廣告端實(shí)際業(yè)務(wù)的開(kāi)發(fā)情況,對(duì)現(xiàn)有工程進(jìn)行了微前端的實(shí)踐與落地。
需求分析
結(jié)合現(xiàn)有工程的狀況,我們進(jìn)行了深度的分析。不過(guò),在進(jìn)行微前端方案確定前,我們先確定了需求點(diǎn)及期望收益,如下表所示:
方案選擇
經(jīng)過(guò)以上的需求分析,我們調(diào)研了業(yè)界及公司周邊的微前端方案,并總結(jié)了以下幾種方案以及它們各自主要的特點(diǎn):
- NPM式:子工程以NPM包的形式發(fā)布源碼;打包構(gòu)建發(fā)布還是由基座工程管理,打包時(shí)集成。
- iframe式:子工程可以使用不同技術(shù)棧;子工程之間完全獨(dú)立,無(wú)任何依賴;基座工程和子工程需要建立通信機(jī)制;無(wú)單頁(yè)應(yīng)用體驗(yàn);路由地址管理困難。
- 通用中心路由基座式:子工程可以使用不同技術(shù)棧;子工程之間完全獨(dú)立,無(wú)任何依賴;統(tǒng)一由基座工程進(jìn)行管理,按照DOM節(jié)點(diǎn)的注冊(cè)、掛載、卸載來(lái)完成。
- 特定中心路由基座式:子業(yè)務(wù)線之間使用相同技術(shù)棧;基座工程和子工程可以單獨(dú)開(kāi)發(fā)單獨(dú)部署;子工程有能力復(fù)用基座工程的公共基建。
通過(guò)對(duì)各個(gè)方案特點(diǎn)進(jìn)行分析,我們將重點(diǎn)關(guān)注項(xiàng)進(jìn)行了對(duì)比,如下表所示:
經(jīng)過(guò)上面的調(diào)研對(duì)比之后,我們確定采用了特定中心路由基座式的開(kāi)發(fā)方案,并命名為:基于React的中心路由基座式微前端。這種方案的優(yōu)點(diǎn)包括以下幾個(gè)方面:
- 保證技術(shù)棧統(tǒng)一在React。
- 子工程之間開(kāi)發(fā)互相獨(dú)立,互不影響。
- 子工程可單獨(dú)打包、單獨(dú)部署上線。
- 子工程有能力復(fù)用基座工程的公共基建。
- 保持單頁(yè)應(yīng)用的體驗(yàn),子工程之間切換不刷新。
- 改造成本低,對(duì)現(xiàn)有工程侵入度較低,業(yè)務(wù)線遷移成本也較低。
- 開(kāi)發(fā)子工程和原有開(kāi)發(fā)模式基本沒(méi)有不同,開(kāi)發(fā)人員學(xué)習(xí)成本較低。
微前端實(shí)踐概覽
通過(guò)對(duì)方案的分析及技術(shù)方向上的梳理,我們確定了微前端的整體方案,如下圖所示:
可以看到,整個(gè)方案非常簡(jiǎn)單明確,即按照業(yè)務(wù)線進(jìn)行了路由級(jí)別的拆分。整個(gè)系統(tǒng)可分為兩個(gè)部分:
- 基座工程:用于管理子工程的路由切換、注冊(cè)子工程的路由和全局Store層、提供全局庫(kù)和復(fù)用層。
- 子工程:用于開(kāi)發(fā)子業(yè)務(wù)線業(yè)務(wù)代碼,一個(gè)子工程對(duì)應(yīng)一個(gè)子業(yè)務(wù)線,并且包含三端代碼和復(fù)用層代碼。
基座工程和子工程聯(lián)系起來(lái)的橋梁則是子工程的入口文件地址和路由地址的映射信息。這些映射信息可以讓基座工程準(zhǔn)確地發(fā)現(xiàn)子工程資源的路徑從而進(jìn)行加載。
微前端架構(gòu)下的業(yè)務(wù)變化
經(jīng)過(guò)微前端實(shí)踐的改造,我們的業(yè)務(wù)在結(jié)構(gòu)上發(fā)生了如下的變化:
如上圖所示,我們進(jìn)行了微前端式的業(yè)務(wù)線拆分:
- 原有的PC系統(tǒng)、H5系統(tǒng)、KA系統(tǒng)分別改造成了PC基座系統(tǒng)、H5基座系統(tǒng)和KA基座系統(tǒng)。
- 原有的子業(yè)務(wù)線被拆分成了單獨(dú)的子倉(cāng)庫(kù),成為了業(yè)務(wù)線子工程(上圖中6個(gè)黑框豎列)。
- 業(yè)務(wù)線子工程分別包含PC端、H5端、KA端以及該業(yè)務(wù)線復(fù)用層的代碼(上圖中3個(gè)純色背景橫列)。
新的拆分使得子工程能夠按照業(yè)務(wù)線進(jìn)行劃分,獨(dú)立維護(hù)。在解決復(fù)用層的同時(shí)保證了子工程大小可控,即子工程只有單個(gè)業(yè)務(wù)線的代碼。而單個(gè)業(yè)務(wù)線的復(fù)雜度并不高,也降低了工程維護(hù)的復(fù)雜度。
采用微前端拆分的方案,使得我們的業(yè)務(wù)不僅在縱向上保有了復(fù)用的能力,更重要的是擁有了橫向擴(kuò)展的能力,無(wú)論產(chǎn)品業(yè)務(wù)線如何膨脹,我們都可以更輕松地應(yīng)對(duì)。那么為了實(shí)現(xiàn)以上的能力,我們做了哪些工作呢?下文我們會(huì)詳細(xì)進(jìn)行說(shuō)明。
基于React技術(shù)棧的中心路由基座式微前端
微前端拆分的方案,我們命名為:基于React技術(shù)棧的中心路由基座式微前端。在具體實(shí)現(xiàn)上,我們會(huì)分為動(dòng)態(tài)化方案、路由配置信息設(shè)計(jì)、子工程接口設(shè)計(jì)、復(fù)用方案設(shè)計(jì)和流程方案設(shè)計(jì)等幾個(gè)模塊來(lái)逐一進(jìn)行說(shuō)明。
動(dòng)態(tài)化方案
首先,我們需要路由的管理方案,使得子工程之間有能力互通切換。其次,我們需要Store層的方案,讓子工程有能力使用全局Store。并且,我們還需要CSS的加載方案,來(lái)加載子工程的樣式布局。下面來(lái)詳細(xì)說(shuō)明這三個(gè)方案。
動(dòng)態(tài)路由
動(dòng)態(tài)路由方案是想要進(jìn)行路由級(jí)別的拆分,首先我們要確定用什么來(lái)管理路由?很多實(shí)現(xiàn)方案傾向于使用特制路由來(lái)管理模塊。例如開(kāi)源框架Single-Spa,實(shí)現(xiàn)了自己的一套路由監(jiān)聽(tīng)來(lái)切換子工程,并且需要子工程實(shí)現(xiàn)特定的注冊(cè)、掛載、卸載等接口來(lái)完成子工程和基座工程的動(dòng)態(tài)對(duì)接,還需要特定的模塊管理系統(tǒng),例如systemjs來(lái)輔助完成這一過(guò)程。毋庸置疑,這對(duì)我們?cè)泄こ痰母脑斐杀竞艽?,還需要添加額外庫(kù),進(jìn)而造成包體積大小上的開(kāi)銷。并且子工程的開(kāi)發(fā)者需要熟悉這些特定的接口,學(xué)習(xí)成本也比較高。顯然,這對(duì)于我們的業(yè)務(wù)場(chǎng)景和需求來(lái)說(shuō)很不劃算。
那么,我們選擇什么來(lái)做路由管理呢?最終我們使用了React-Router,這樣能夠保持我們?cè)瓉?lái)的技術(shù)棧不變,同時(shí)對(duì)于工程的侵入也是最低,幾乎可以忽略不計(jì)。此外,React-Router完全可以滿足我們的需求,而且自動(dòng)會(huì)幫助我們管理頁(yè)面的加載與卸載,而不是每次切換路由都重新初始化整個(gè)子應(yīng)用,所以在加載速度體驗(yàn)上也是最優(yōu)的,跟單頁(yè)應(yīng)用的體驗(yàn)一致。
在實(shí)現(xiàn)上也很簡(jiǎn)單,如下圖所示:
上面這個(gè)流程圖,展示了我們?cè)诨こ讨星袚Q到子工程路由時(shí),加載子工程并進(jìn)行展示的過(guò)程。這里的重點(diǎn)步驟是加載子工程入口文件,并動(dòng)態(tài)注冊(cè)子工程路由的過(guò)程。由于我們使用的是React-Router,顯然要使用其提供的動(dòng)態(tài)能力來(lái)完成。這一過(guò)程也非常輕量,由于React-Router從版本4開(kāi)始有了“破壞級(jí)”的升級(jí),于是我們就調(diào)研了兩種方式進(jìn)行動(dòng)態(tài)加載路由(目前我們使用的是React-Router版本5),如下表所示:
React-Router版本3中,實(shí)現(xiàn)的基本代碼思路如下:
// react-router V3 用于接收子工程的路由export default () => ( <Route path="/subapp" getChildRoutes={(location: any, cb: any) => { const { pathname } = location.location; // 取路徑中標(biāo)識(shí)子工程前綴的部分, 例如 '/subapp/xxx/index' 其中xxx即路由唯一前綴 const id = pathname.split('/')[2]; const subappModule = (subAppMapInfo as any)[id]; if (subappModule) { if (subappRoutes[id]) { // 如果已經(jīng)加載過(guò)該子工程的模塊,則不再加載,直接取緩存的routes cb(null, [subappRoutes[id]]); return; } // 如果能匹配上前綴則加載相應(yīng)子工程模塊 currentPrefix = id; loadAsyncSubapp(subappModule.js) .then(() => { // 加載子工程完成 cb(null, [subappRoutes[id]]); }) .catch(() => { // 如果加載失敗 console.log('loading failed'); }); } else { // 可以重定向到首頁(yè)去 goBackToIndex(); } }} />);
而在React-Router版本4中,實(shí)現(xiàn)的基本代碼思路如下:
export const AyncComponent: React.FC<{ hotReload?: number; } & RouteComponentProps> = ({ location, hotReload }) => { // 子工程資源是否加載完成 const [ayncLoaded, setAyncLoaded] = useState(false); // 子工程url配置信息是否加載完成 const [subAppMapInfoLoaded, setSubAppMapInfoLoaded] = useState(false); const [ayncComponent, setAyncComponent] = useState(null); const { pathname } = location; // 取路徑中標(biāo)識(shí)子工程前綴的部分, 例如 '/subapp/xxx/index' 其中xxx即路由唯一前綴 const id = pathname.split('/')[2]; useEffect(() => { // 如果沒(méi)有子工程配置信息, 則請(qǐng)求 if (!subAppMapInfoLoaded) { fetchSubappUrlPath(id).then((data) => { subAppMapInfo = data; setSubAppMapInfoLoaded(true); }).catch((url: any) => { // 失敗處理 goBackToIndex(); }); return; } const subappModule = (subAppMapInfo as any)[id]; if (subappModule) { if (subappRoutes[id]) { // 如果已經(jīng)加載過(guò)該子工程的模塊,則不再加載,直接取緩存的routes setAyncLoaded(true); setAyncComponent(subappRoutes[id]); return; } // 如果能匹配上前綴則加載相應(yīng)子工程模塊 // 如果請(qǐng)求成功,則觸發(fā)JSONP鉤子window.wmadSubapp currentPrefix = id; setAyncLoaded(false); const jsUrl = subappModule.js; loadAsyncSubapp(jsUrl) .then(() => { // 加載子工程完成 setAyncComponent(subappRoutes[id]); setAyncLoaded(true); }) .catch((urlList) => { // 如果加載失敗 setAyncLoaded(false); console.log('loading failed...'); }); } else { // 可以重定向到首頁(yè)去 goBackToIndex(); } }, [id, subAppMapInfoLoaded, hotReload]); return ayncLoaded ? ayncComponent : null;};
可以看到,這種方式實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單,不需要額外依賴,同時(shí)滿足了我們“拆分”的訴求。
動(dòng)態(tài)Store
對(duì)于Store層,我們?cè)こ淌褂玫氖荝edux,子工程通過(guò)路由動(dòng)態(tài)注冊(cè)進(jìn)來(lái)天然就可以訪問(wèn)到全局Store,所以對(duì)于Store的訪問(wèn)能夠自動(dòng)支持。那么,如果子工程想要注冊(cè)自己的全局Store該怎么辦呢?而且我們還用了redux-saga來(lái)作為異步處理方案。redux-saga如何動(dòng)態(tài)注冊(cè)呢?還是利用它們各自的API就可以達(dá)到我們的目的?從下圖中可以看到,支持動(dòng)態(tài)Store也是花費(fèi)很小的改造成本就可以完成。
動(dòng)態(tài)CSS
同樣的對(duì)應(yīng)子工程的樣式布局,我們也需要通過(guò)某種途徑加載到基座工程中來(lái)。這個(gè)很自然地用異步加載CSS文件通過(guò)style標(biāo)簽注入來(lái)完成,不過(guò)這里需要注意兩個(gè)問(wèn)題:
一個(gè)問(wèn)題是,加載子工程的JS入口文件和CSS文件可以同時(shí)發(fā)起請(qǐng)求,但是需要保證CSS文件加載完成后再進(jìn)行JS入口文件的路由注冊(cè)。因?yàn)槿绻酚上茸?cè)了頁(yè)面就會(huì)顯示出來(lái),如果這時(shí)CSS文件還沒(méi)有加載完畢,就會(huì)出現(xiàn)頁(yè)面樣式閃動(dòng)的問(wèn)題。我們通過(guò)先加載CSS再加載JS的策略來(lái)避免這個(gè)問(wèn)題的發(fā)生。
另一個(gè)問(wèn)題是,怎么保證子工程的CSS不會(huì)和其他子工程沖突。我們利用PostCSS插件在編譯子工程時(shí),按照分配給子工程的唯一業(yè)務(wù)線標(biāo)識(shí),為每一組CSS規(guī)則生成了命名空間來(lái)解決這個(gè)問(wèn)題。而子業(yè)務(wù)線開(kāi)發(fā)者是沒(méi)有感知的,可以沒(méi)有“心智負(fù)擔(dān)”地書(shū)寫子工程的樣式。
路由配置信息方案
在動(dòng)態(tài)加載方案確定之后,基座工程怎么才能知道子工程的資源路徑,進(jìn)而加載對(duì)應(yīng)的JS和CSS資源呢?我們需要一組映射信息。如下圖所示,業(yè)務(wù)線唯一標(biāo)識(shí)為Key,相應(yīng)的靜態(tài)資源地址為Value。這樣的話,當(dāng)基座工程切換到子工程時(shí)就可以拉取這個(gè)配置信息,在路由切換時(shí)準(zhǔn)確地找到對(duì)應(yīng)的子工程,進(jìn)而進(jìn)行后續(xù)的資源加載過(guò)程。這里可能會(huì)遇到的一個(gè)問(wèn)題,即如果JS和CSS過(guò)大,是否能進(jìn)行拆分?
根據(jù)我們業(yè)務(wù)的實(shí)際情況,目前靜態(tài)資源的大小是可控的,無(wú)需注冊(cè)多個(gè),單一入口地址完全能夠滿足我們的業(yè)務(wù)需求,并且由于我們的改造完全基于現(xiàn)有技術(shù)棧。如果業(yè)務(wù)很復(fù)雜,完全可以在子工程中通過(guò)webpack的動(dòng)態(tài)import進(jìn)行路由懶加載,也就是說(shuō),子工程完全可以按照路由再次切分成chunks來(lái)減少JS的包體積。至于CSS本身就很小,長(zhǎng)期也不會(huì)有進(jìn)行切分的需要。
子工程接口方案
子工程需要暴露它要注冊(cè)給基座工程的對(duì)象,來(lái)進(jìn)行基座工程加載子工程的過(guò)程。在子工程入口文件中定義registerApp來(lái)傳遞注冊(cè)的對(duì)象,主要代碼如下:
import reducers from 'common/store/labor/reducer';import sagas from 'common/store/labor/saga';import routes from './routes/index';function registerApp(dep: any = {}): any { return { routes, // 子工程路由組件 reducers, // 子工程Redux的reducer sagas, // 子工程的Redux副作用處理saga };}export default registerApp
我們這里暴露了子工程的三個(gè)對(duì)象:這里最重要的就是routes路由組件,就是在寫React-Router(版本4及以上)的路由。子工程開(kāi)發(fā)者只需要配置routes對(duì)象即可,沒(méi)有任何學(xué)習(xí)成本,其代碼如下:
/** * 子工程路由注冊(cè)說(shuō)明 * 如注冊(cè)的路由如下: * path: 'index' * 路由前綴會(huì)被追加上,路由前綴規(guī)則見(jiàn)變量urlPrefix * 在主工程的訪問(wèn)路勁為:/subapp/${工程注冊(cè)名稱}/index */const urlPrefix = `/subapp/${microConfig.name}/`;const routes = [ { path: 'index', component: IndexPage, },];const AppRoutes = () => ( <Switch> { routes.map(item => ( <Route key={item.path} exact path={`${urlPrefix}${item.path}`} component={item.component} /> )) } <Redirect to="/" /> </Switch>);export default AppRoutes;
除了上方的routes對(duì)象,還剩下兩個(gè)接口對(duì)象是:reducers和sagas,用于動(dòng)態(tài)注冊(cè)全局Store相關(guān)的數(shù)據(jù)和副作用處理。這兩個(gè)接口我們?cè)谧庸こ讨袝簳r(shí)沒(méi)有開(kāi)放,因?yàn)榘凑諛I(yè)務(wù)線拆分過(guò)后,由于業(yè)務(wù)線間獨(dú)立性很強(qiáng),全局Store的意義就不大了。我們希望子工程可以自行處理自己的Store,即每個(gè)業(yè)務(wù)線維護(hù)自己的Store,這里就不再展開(kāi)進(jìn)行說(shuō)明了。
復(fù)用方案
基座工程除了路由管理之外,還作為共享層共享全局的基建,例如框架基本庫(kù)、業(yè)務(wù)組件等。這樣做的目的是,子業(yè)務(wù)線間如果有相同的依賴,切換的時(shí)候就不會(huì)出現(xiàn)重復(fù)加載的問(wèn)題。例如下面的代碼,我們把React相關(guān)庫(kù)都以全局的方式導(dǎo)出,而子工程加載的時(shí)候就會(huì)以external的形式加載這些庫(kù),這樣子工程的開(kāi)發(fā)者不需要額外的第三方模塊加載器,直接引用即可,和平時(shí)開(kāi)發(fā)React應(yīng)用一致,沒(méi)有任何學(xué)習(xí)成本。而和各個(gè)業(yè)務(wù)都相關(guān)的公用組件等,我們會(huì)放到wmadMicro的全局命名空間下進(jìn)行管理。主要代碼如下:
import * as React from 'react';import * as ReactDOM from 'react-dom';import * as ReactRouterDOM from 'react-router-dom';import * as Axios from 'axios';import * as History from 'history';import * as ReactRedux from 'react-redux';import * as Immutable from 'immutable';import * as ReduxSagaEffects from 'redux-saga/effects';import Echarts from 'echarts';import ReactSlick from 'react-slick';function registerGlobal(root: any, deps: any) { Object.keys(deps).forEach((key) => { root[key] = deps[key]; });}registerGlobal(window, { // 在這里注冊(cè)暴露給子工程的全局變量 React, ReactDOM, ReactRouterDOM, Axios, History, ReactRedux, Immutable, ReduxSagaEffects, Echarts, ReactSlick,});export default registerGlobal;
流程方案
在確定了程序拆分運(yùn)行的整體銜接之后,我們還要確定開(kāi)發(fā)方案、部署方案以及回滾方案。我們?nèi)绾伍_(kāi)始開(kāi)發(fā)一個(gè)子工程?以及我們?nèi)绾尾渴鹞覀兊淖庸こ蹋?/p>
開(kāi)發(fā)流程
有兩種開(kāi)發(fā)方案可以滿足獨(dú)立開(kāi)發(fā)的目的:第一種是提供一個(gè)基座工程的Dev環(huán)境,子工程在本地啟動(dòng)后在Dev環(huán)境進(jìn)行開(kāi)發(fā),這種開(kāi)發(fā)方式要求有一套基座工程的更新機(jī)制,例如基座工程更新后要同步部署到Dev環(huán)境。第二種是子工程開(kāi)發(fā)者拉取基座工程到本地并啟動(dòng)本地開(kāi)發(fā)環(huán)境,然后拉取子工程到本地,再啟動(dòng)子工程本地開(kāi)發(fā)環(huán)境進(jìn)行開(kāi)發(fā),這種開(kāi)發(fā)方式是目前我們使用的方式。如下圖所示,我們提供了子工程腳手架來(lái)快速創(chuàng)建子工程,開(kāi)發(fā)者無(wú)需做任何配置和額外學(xué)習(xí)成本,就可以像開(kāi)發(fā)React應(yīng)用一樣進(jìn)行開(kāi)發(fā)。
熱更新
在開(kāi)發(fā)過(guò)程中,我們希望我們的開(kāi)發(fā)體驗(yàn)和開(kāi)發(fā)單頁(yè)應(yīng)用的體驗(yàn)一致,也要支持熱更新。由于我們的拆分,實(shí)際上有兩個(gè)服務(wù),即基座和子工程,所以我們以上圖的方式完成了熱更新的支持:在子工程的module.hot中通過(guò)再次觸發(fā)基座工程中的JSONP鉤子來(lái)通知基座工程,來(lái)再次觸發(fā)renderApp達(dá)到子工程更新代碼則頁(yè)面熱刷新的目的。主要代碼如下:
// 在子工程入口文件import routes from './routes/index';function registerApp(dep: any = {}): any { return { routes, };}if ((module as any).hot) { (module as any).hot.accept('./routes/index', (): any => { window.wmadSubapp(registerApp, true); // 支持子工程熱加載的信息傳遞 });}export default registerApp
Mock數(shù)據(jù)
子工程目前Mock數(shù)據(jù)的方式有三種:一是在基座本地Mock,這種Mock方式天然支持,因?yàn)榛こ袒谕赓u工程化Nine腳手架進(jìn)行開(kāi)發(fā),本身支持本地Mock。二是支持子工程本地Mock。三是使用公共Mock服務(wù)YAPI。目前子工程開(kāi)發(fā)的Mock功能結(jié)合第一種方式和第三種方式進(jìn)行。
部署方案
最后是部署方案,我們達(dá)成了獨(dú)立部署上線的目的,即子工程發(fā)布不需要基座工程的參與。之前所有子業(yè)務(wù)線都在一個(gè)工程中,打包速度隨著業(yè)務(wù)線的膨脹變得越來(lái)越慢,而如下的方案使得子工程的開(kāi)發(fā)和部署完全獨(dú)立,單個(gè)業(yè)務(wù)線的打包速度會(huì)非常快,從之前的分鐘級(jí)別降到了秒級(jí)別。如下圖所示,子工程部署只需要把子工程打包,并在上傳CDN之后,把配置信息更新即可,因?yàn)榕渲眯畔⒅杏凶庸こ绦碌馁Y源地址,這樣就達(dá)到了發(fā)布上線的目的。
整個(gè)部署過(guò)程我們是托管到Talos(美團(tuán)內(nèi)部自研的部署工具)上的,配置信息我們是托管到Portm(美團(tuán)內(nèi)部自研的文件存儲(chǔ))上的(通過(guò)我們開(kāi)發(fā)的Talos的插件UpdatePubInfo-To-Portm來(lái)更新我們的配置信息)。在靜態(tài)資源上傳到CDN之后,就可以更新配置信息,供主工程調(diào)用,也就完成了子工程上線的過(guò)程。利用美團(tuán)現(xiàn)有服務(wù),我們很迅速地完成了子工程單獨(dú)部署上線的整個(gè)流程。
回滾方案
在部署方案中,我們通過(guò)Talos進(jìn)行部署,它本身就帶有回滾功能。得益于子工程的發(fā)布和普通工程的發(fā)布并沒(méi)什么本質(zhì)不同,都是將靜態(tài)資源放置到CDN上,通過(guò)靜態(tài)資源的的contenthash值來(lái)區(qū)分不同版本,所以回滾的時(shí)候,Talos取到上個(gè)版本(或者某個(gè)前版本)的靜態(tài)資源,再通過(guò)Portm更新我們的配置信息即可完成。整個(gè)過(guò)程和普通工程沒(méi)有區(qū)別,發(fā)版人員只需簡(jiǎn)單地點(diǎn)下回滾按鈕即可。
監(jiān)控方案
改變了原有的開(kāi)發(fā)模式后,我們還對(duì)幾個(gè)關(guān)鍵節(jié)點(diǎn)進(jìn)行了監(jiān)控報(bào)警的埋點(diǎn)。利用美團(tuán)CAT(已經(jīng)在GitHub上開(kāi)源)和天網(wǎng)(美團(tuán)內(nèi)部的監(jiān)控系統(tǒng)),我們分別在子工程的配置信息、靜態(tài)資源加載等節(jié)點(diǎn)上進(jìn)行了埋點(diǎn)上報(bào),統(tǒng)計(jì)子工程加載成功率,及時(shí)發(fā)現(xiàn)可能出現(xiàn)的子工程切換問(wèn)題。具體情況如下圖所示:
上方左圖是按照端維度進(jìn)行統(tǒng)計(jì)的示例,上方右圖是PC端按照產(chǎn)品線統(tǒng)計(jì)加載成功數(shù)的示例。默認(rèn)都是統(tǒng)計(jì)當(dāng)天的數(shù)據(jù),顯示‘-’的表明當(dāng)前沒(méi)有數(shù)據(jù)。對(duì)資源加載的監(jiān)控目前有三種類型:JSON、JS和CSS,資源加載失敗的統(tǒng)計(jì)也包含這三種類型。天網(wǎng)的監(jiān)控按照分鐘級(jí)進(jìn)行,每分鐘內(nèi)如果有加載失敗就會(huì)發(fā)出報(bào)警,偶爾的報(bào)警可能是用戶網(wǎng)絡(luò)的問(wèn)題,如果出現(xiàn)大批量的報(bào)警就要引起重視了。
總結(jié)
以上就是微前端在外賣商家廣告端的實(shí)踐過(guò)程??偟膩?lái)說(shuō),我們完成了以下的目標(biāo):
- 按照領(lǐng)域(業(yè)務(wù)線)拆分工程,工程的可維護(hù)性得到提高,相關(guān)領(lǐng)域進(jìn)行了內(nèi)聚,無(wú)關(guān)領(lǐng)域進(jìn)行了解耦。
- 子工程提供了PC、H5、KA三端的物理復(fù)用土壤,消除了工程膨脹問(wèn)題,工程大小也變得可控。
- 子工程打包速度從分鐘級(jí)降為秒級(jí),提高了開(kāi)發(fā)體驗(yàn),加快了上線的速度。
- 子工程開(kāi)發(fā)支持熱更新,開(kāi)發(fā)體驗(yàn)不降級(jí)。
- 子工程能夠單獨(dú)開(kāi)發(fā)、單獨(dú)部署、單獨(dú)上線,業(yè)務(wù)線間互不影響。
- 整體工程改造成本低,插拔式開(kāi)發(fā),無(wú)侵入式代碼,在正常業(yè)務(wù)開(kāi)發(fā)的同時(shí)短期內(nèi)就可以完成上線。
- 開(kāi)發(fā)者學(xué)習(xí)成本低,完整地保留了單頁(yè)應(yīng)用開(kāi)發(fā)的開(kāi)發(fā)體驗(yàn),開(kāi)發(fā)者可快速上手。
目前在美團(tuán)廣告端,以微前端模式上線的子業(yè)務(wù)線已經(jīng)有很多個(gè)。另外還有多個(gè)正在開(kāi)發(fā)的微前端子工程,剩余在主工程中的子業(yè)務(wù)線后續(xù)也可以無(wú)痛遷移出來(lái)成為子工程。我們內(nèi)部也在此過(guò)程中搜集了不少意見(jiàn)反饋,未來(lái)繼續(xù)在實(shí)踐中進(jìn)行思考和完善。在此過(guò)程中,我們深知還有很多做得不夠完善甚至存在問(wèn)題的地方,歡迎大家跟我們進(jìn)行交流,幫我們提出寶貴意見(jiàn)或者給予指導(dǎo)。當(dāng)然也歡迎大家加入我們團(tuán)隊(duì)(文末有招聘信息),一起共建。
作者簡(jiǎn)介
張嘯、魏瀟、天堯,均為美團(tuán)外賣前端團(tuán)隊(duì)研發(fā)工程師。
招聘信息
美團(tuán)外賣廣告前端團(tuán)隊(duì)誠(chéng)招高級(jí)前端開(kāi)發(fā)、前端開(kāi)發(fā)專家。我們?yōu)樯碳姨峁┳儸F(xiàn)服務(wù)平臺(tái),為用戶提供優(yōu)質(zhì)廣告體驗(yàn),是外賣商業(yè)變現(xiàn)中的重要環(huán)節(jié)。歡迎各位小伙伴的加入,共同打造極致廣告產(chǎn)品。感興趣的同學(xué)可投遞簡(jiǎn)歷至:tech@meituan.com(郵件標(biāo)題注明:美團(tuán)外賣廣告前端團(tuán)隊(duì))