基于wujie的解決方案來簡(jiǎn)單聊聊微前端
共 15320字,需瀏覽 31分鐘
·
2024-07-15 09:15
前言
因?yàn)槟壳坝袝r(shí)間了,所以在整理一下自己這幾年寫過的一些東西的相關(guān)文檔,準(zhǔn)備把一些東西改一下發(fā)出來,有的內(nèi)容可能并不復(fù)雜,甚至有點(diǎn)淺顯,但是也是對(duì)自己這幾年的一些復(fù)盤和總結(jié)了
如果有需要,轉(zhuǎn)載前請(qǐng)向我確認(rèn)
另:剛?cè)肼毜墓景芽偙O(jiān)和經(jīng)理都干掉了,有可能因?yàn)榘l(fā)展路線原因不再需要前端,現(xiàn)在求一份廣州 、 深圳的前端開發(fā)工作
本科5年經(jīng)驗(yàn),19年畢業(yè),18年開始從事前端工作,基礎(chǔ)良好、有千萬級(jí)日活產(chǎn)品開發(fā)維護(hù)經(jīng)驗(yàn),有大型產(chǎn)品開發(fā)經(jīng)驗(yàn),有良好的代碼風(fēng)格與文檔習(xí)慣
什么是微前端,它解決了什么問題
什么是微前端
微前端這個(gè)概念相信對(duì)于前端來講,其實(shí)并不陌生,大家也或多或少看到過相關(guān)的文章、或者有過相關(guān)的實(shí)踐,如果我們?nèi)ニ阉鳎菏裁词俏⑶岸?/p>
那么我們其實(shí)很容易找到以下一些類似的定義:
-
是將前端應(yīng)?分解成?些更?、更簡(jiǎn)單的能夠獨(dú)?開發(fā)、測(cè)試、部署的?塊,?在?戶看來仍 然是內(nèi)聚的單個(gè)產(chǎn)品的技術(shù)或思想。 -
將某個(gè)單?的單體應(yīng)?,轉(zhuǎn)化為多個(gè)可以獨(dú)?運(yùn)?、獨(dú)?開發(fā)、獨(dú)?部署、獨(dú)?維護(hù)的服務(wù)或者應(yīng)? 的聚合,從?滿?業(yè)務(wù)快速變化及分布式多團(tuán)隊(duì)并?開發(fā)的需求 -
.......
等等,諸如此類的定義
「這時(shí)候可能會(huì)有同學(xué)會(huì)說:哎嘿?那我搞幾個(gè)項(xiàng)目里面套幾個(gè)iframe那不也是微前端?」
沒錯(cuò),如果按照上面的微前端的定義來說,iframe或許就是最初的微前端方案了,甚至連通過nginx路由轉(zhuǎn)發(fā)來組合不同項(xiàng)目的功能組成一個(gè)系統(tǒng)都能可以是微前端。
那這時(shí)候可能不太清楚的同學(xué)就會(huì)想:既然基于 iframe 我們就可以搭建一套微前端的系統(tǒng)了,那為什么現(xiàn)在業(yè)界的微前端方案還層出不窮,各自都給出了自己的答卷?
關(guān)于這個(gè)問題,莫急,關(guān)于這個(gè)我們稍后簡(jiǎn)單討論一下
「前面說了這么多,那微前端到底是什么呢?」
就像我們前面說過的一樣,我們將不同的功能模塊或業(yè)務(wù)通過例如
1、按照業(yè)務(wù)
2、按照權(quán)限
3、按照變更的頻率
4、按照組織結(jié)構(gòu)
5、跟隨后端微服務(wù)設(shè)計(jì)
6、從代碼出發(fā)的ddd實(shí)踐
(可能很多人了解 ddd 這個(gè)概念都是在微前端或者微服務(wù)入坑的,手動(dòng)滑稽.jpg)
等等的各種適用于不同實(shí)際業(yè)務(wù)場(chǎng)景的原則來劃分出不同的子應(yīng)用,通過「組合」的方式來組成一個(gè)完整應(yīng)用的思想和技術(shù)方案 (所以微前端的拆分通常沒辦法簡(jiǎn)單抄作業(yè))
所以,微前端本質(zhì)上是一種通過 「模塊化、拼圖式」的開發(fā),以「組合的思想」來降低前端集成的復(fù)雜度和成本的思想(以上概念僅限于個(gè)人理解,本人不為該觀點(diǎn)正確性負(fù)責(zé))
說到組合的思想,這時(shí)候可能很多同學(xué)都會(huì)不約而同地想到:
「嘿,終于到我熟悉的領(lǐng)域了」
在目前組合代替繼承的思想流行下,相信每位前端同學(xué)對(duì)組合的思想都有自己的經(jīng)驗(yàn)和見解,那么在這種思想下進(jìn)行的前端開發(fā)中碰到的一些組合的思想所帶來的問題,其實(shí)在微前端這種應(yīng)用層面的組合上也不能逃脫
具體的內(nèi)容我們會(huì)在第二部分中再進(jìn)行簡(jiǎn)單的討論
微前端適用于什么場(chǎng)景
在前面我們拋出了一大堆的概念,大家其實(shí)對(duì)微前端的思想也有了一定的了解,很多人可能這時(shí)候會(huì)在想:
前面巴巴巴說了一大堆,那我一定就要用到這玩意嗎?
它能給我們的業(yè)務(wù)帶來什么價(jià)值,能超過它所帶來的項(xiàng)目管理的問題嗎?
(雖然我們可以通過 Lerna+Monorepo之類的工程結(jié)構(gòu)來緩解管理的問題)
我們面臨的問題就非它不可,必須要從架構(gòu)的層面上去改變嗎?
其實(shí)在采用微前端架構(gòu)的時(shí)候,不管是用 qiankun,還是用 iframe,抑或是其他的什么解決方案,能用不同框架只是添頭,實(shí)際上并沒有觸及到問題的本質(zhì),微前端各個(gè)部分之間相互獨(dú)立,獨(dú)立部署的能力本質(zhì)上是在允許構(gòu)建孤立或「松散耦合」的服務(wù)。
而松散耦合的系統(tǒng),對(duì)于開發(fā)、維護(hù)、還是后期漸進(jìn)式重構(gòu)的好處都是毋庸置疑的。
什么是松散耦合
松散耦合是各種相互聯(lián)合事件的反映但是,每一個(gè)事件也都在保持自身的獨(dú)特性,也存在著某些物質(zhì)或邏輯上的分離,各種結(jié)構(gòu)性要素松散聯(lián)系,但是結(jié)構(gòu)對(duì)結(jié)果基本沒有什么影響的狀態(tài),有興趣的同學(xué)可以了解一下相關(guān)概念,挺有意思的
所以在下面我們可以簡(jiǎn)單討論一下,它更具體一點(diǎn)的應(yīng)用場(chǎng)景
1. 治理巨石應(yīng)用
這個(gè)無疑是提起微前端時(shí)最容易被聯(lián)想到的場(chǎng)景,在實(shí)際的開發(fā)生涯中其實(shí)我們經(jīng)常能碰到以下的經(jīng)典場(chǎng)景:
「(1) 在進(jìn)入一個(gè)新團(tuán)隊(duì)的時(shí)候,經(jīng)常有可能接手到一個(gè) 5 年陳的項(xiàng)?,或者我們需要重啟一個(gè)多年未維護(hù)的項(xiàng)目」
這個(gè)項(xiàng)目可能會(huì)有強(qiáng)?混?多種技術(shù)棧的情況,例如我們現(xiàn)在這個(gè)六年陳的pc端就會(huì)有react 和 jq 混合開發(fā)的情況,或者是使?的技術(shù)棧落后、較為?眾或?qū)W習(xí)成本過?。
例如Foundation、angular1.x、Easy Framework之類的玩意,又或者是重構(gòu)不徹底的代碼,經(jīng)歷了了重構(gòu)-爛尾-又重構(gòu)-又爛尾的項(xiàng)目(沒錯(cuò),說的上面那項(xiàng)目)
那這時(shí)候我們把原有功能抽離為單獨(dú)的子應(yīng)用,而新的功能模塊作為新的子應(yīng)用嵌入的辦法來保證在逐漸重構(gòu)的同時(shí)既要保證中間版本能夠平滑過渡,同時(shí)持續(xù)交付新的功能?(只是一個(gè)思路,不一定就是用這種方式)
「(2) 保證當(dāng)前技術(shù)?案在 3-5 年的業(yè)務(wù)迭代后還保有?命?,不會(huì)變成?個(gè)遺產(chǎn)項(xiàng)?」
「(3) 避免代碼庫不斷膨脹?帶來的各種問題」
「例如」
-
因?yàn)閱误w應(yīng)用的不斷膨大導(dǎo)致的理解和修改成本的不斷上升
-
從代碼的提交到實(shí)際部署的周期越來越?,并且很容易出問題,例如我們的H5項(xiàng)目有接近一百五十個(gè)打包入口,在每一個(gè)版本的迭代都需要重新全量打包部署,之前在有集團(tuán)內(nèi)項(xiàng)目打包時(shí)間統(tǒng)計(jì)的時(shí)候也名列打包最長(zhǎng)時(shí)間的前三
-
難以交付可靠的單體應(yīng)?, 系統(tǒng)龐?復(fù)雜 -> ?法進(jìn)?全??徹底的測(cè)試 -> 代碼中的錯(cuò)誤會(huì)進(jìn)??產(chǎn)環(huán)境 -> 因?yàn)槌绦蛑械拇a都在同?進(jìn)程中運(yùn)?,應(yīng)?缺乏故障隔離 -> 可能出現(xiàn)?些例如:內(nèi)存泄漏 之類的問題導(dǎo)致?戶運(yùn)?過久后崩潰之類的問題
例如node寫的中間件的實(shí)例則有可能崩潰導(dǎo)致?半夜因?yàn)?產(chǎn)環(huán)境的問題爬起來查bug
-
需要?期依賴某個(gè)可能已經(jīng)過時(shí)的技術(shù)棧,并且框架難以升級(jí)新的版
等等。。。。。。。。
2.快速驗(yàn)證
其實(shí)我們碰到
「(1)產(chǎn)品想要去上線一個(gè)試驗(yàn)性的活動(dòng)或者功能的場(chǎng)景非常多」
這類功能在用戶反饋不好的時(shí)候或許在很短的時(shí)間內(nèi)就會(huì)被下線,而這個(gè)活動(dòng)或功能的上線和下線每次都要經(jīng)歷一個(gè)成本較高的過程。
「(2)同時(shí)驗(yàn)證同一個(gè)功能的不同實(shí)現(xiàn)」
而在這方面其實(shí)就像酷狗曾經(jīng)有一個(gè)組件即服務(wù)的微前端實(shí)現(xiàn),每個(gè)直播間的組件靈活控制上下線,對(duì)于用戶反饋的時(shí)機(jī)把握賊靈活
3. 應(yīng)?功能?由組合拆分及定制化開發(fā)
例如我們的目前的業(yè)務(wù)、實(shí)際上經(jīng)常有可能會(huì)面臨對(duì)不同學(xué)校有不同的特定的定制化需求或者功能組合,那實(shí)際上我們可以對(duì)每個(gè)單獨(dú)的功能作為一個(gè)單獨(dú)的子系統(tǒng)開發(fā)、?系統(tǒng)間的耦合只需要規(guī)定好相應(yīng)的通訊?式和內(nèi)容,不需要關(guān)注對(duì)?的實(shí)現(xiàn),在需要的時(shí)候自由組合即可。
4. 可同時(shí)灰度多條產(chǎn)品功能等等
因?yàn)樗缮Ⅰ詈系南到y(tǒng)結(jié)構(gòu)可應(yīng)用的場(chǎng)景太多了,所以就不一一列舉了
如果我們需要微前端的能力我們需要關(guān)注什么
如果我們需要一個(gè)微前端方案,我們需要關(guān)注什么?
通過第一部分的簡(jiǎn)單描述,我們大概了解了什么是微前端,那么,在我們假設(shè)對(duì)目前流行的微前端的技術(shù)方案或者架構(gòu)都不清楚的情況下,倘若我們需要去實(shí)踐這么一個(gè)微前端的架構(gòu),我們需要關(guān)注哪些切實(shí)的問題?
組件之間的組合最重要的是什么
跟我們?cè)诘谝徊糠炙岬降囊粯樱⑶岸说膶?shí)現(xiàn)本質(zhì)上也是一種組合的思想,子應(yīng)用間的組合其實(shí)和功能中組件的組合有一定程度上的異曲同工之妙,那么我們組件之間的組合最重要的是什么?
沒錯(cuò)!就是 「通訊和狀態(tài)管理」
就好像組件一樣,子應(yīng)用總會(huì)有各樣的組合方式,那么父子應(yīng)用間的通訊、兄弟應(yīng)用之間的通訊、甚至爺孫應(yīng)用之間的通訊就成了一個(gè)需要關(guān)注的問題。
既然是由不同的子應(yīng)用之間組合而成,無法避免狀態(tài)管理的問題,無論是全局下的狀態(tài)管理、幾個(gè)子應(yīng)用間的局部狀態(tài)管理,還是單個(gè)子應(yīng)用間的狀態(tài)管理,以及狀態(tài)的上傳和下發(fā),都將是我們需要去考慮的問題
我們簡(jiǎn)單舉一個(gè)例子,對(duì)于數(shù)據(jù)和狀態(tài)的管理、在你想采用微前端架構(gòu)的時(shí)候,不管是用 qiankun,還是用 iframe,用不同框架只是添頭,沒什么作用(因?yàn)榫幊陶Z言或者目標(biāo)語言沒有改變),很多時(shí)候你遇到的問題可以轉(zhuǎn)化成這樣
就容易變成了redux 動(dòng)機(jī)文檔中的曼妥思糖 如果一個(gè) model 的變化會(huì)引起另一個(gè) model 變化,那么當(dāng) view 變化時(shí),就可能引起對(duì)應(yīng) model 以及另一個(gè) model 的變化,依次地,可能會(huì)引起另一個(gè) view 的變化。直至你搞不清楚到底發(fā)生了什么
單獨(dú)看這個(gè)問題,你會(huì)很想當(dāng)然地說出狀態(tài)提升的解法
這種解法沒有問題,但是,當(dāng)你這么解決問題,一旦應(yīng)用范圍內(nèi)廣泛存在數(shù)據(jù)耦合,你怎么辦?
這時(shí)候必然將所有數(shù)據(jù)放置于全局單例,雖然犧牲了多例和初始化控制以及析構(gòu)控制能力,但是確實(shí)能大大減少開發(fā)負(fù)擔(dān),這種解法就是 redux 或者說狀態(tài)管理的本質(zhì)
但是!
這種解法在微前端中是完全無法使用的
因?yàn)槲⑶岸思軜?gòu),在應(yīng)用這一層級(jí)之上,目標(biāo)是「分開開發(fā),分開構(gòu)建,分開部署」
「你能將單一數(shù)據(jù)原則應(yīng)用于此么?」
答案是不能的,每個(gè)應(yīng)用有自己的 store,最終還是會(huì)回到第一個(gè)例子的問題上去
所以,我們要正視怎么解決這樣的問題,這才是微前端之所以微,松散耦合之所以松散的本質(zhì)
「其實(shí)這個(gè)本質(zhì)上是兩個(gè)問題」
1、子會(huì)響應(yīng)父的狀態(tài)變化,父會(huì)在子初始化之后初始化,子會(huì)在父變更后變更,導(dǎo)致子狀態(tài)必須在子組件內(nèi)部
2、react 以及狀態(tài)管理,只有子向父 dispatch 事件的能力,父無法向子 dispatch 事件
即使是換到父子應(yīng)用層面上的理解也依舊如此
而且我們需要知道一件事,在第一部分中其實(shí)我們有提過微前端的應(yīng)用間的關(guān)系其實(shí)也是一種組合關(guān)系
在組合關(guān)系中,「被組合對(duì)象是絕對(duì)會(huì)耦合于源對(duì)象的」
所以我們?cè)跔顟B(tài)管理的層面上將組合的方式轉(zhuǎn)為更松散的邏輯關(guān)系,例如:聚合?
而這種轉(zhuǎn)換其實(shí)一般來說我們都是通過「依賴注入」的方式去實(shí)現(xiàn)的
但是這種情況下,子組件依然無法擁有自己的狀態(tài),因?yàn)檫@依然是單一數(shù)據(jù)處理方式,只不過沒有隔一層 props
并且這時(shí)候還是沒能解決 只有子向父 dispatch 事件的能力,父無法向子 dispatch 事件的問題
「那我們需要怎么解決這個(gè)問題?」
其實(shí)這個(gè)問題很好解決,實(shí)現(xiàn)一個(gè)「發(fā)布訂閱模型」就可以了,所以這也是為什么qiankun之類的微前端方案的應(yīng)用間通訊方式是使用發(fā)布訂閱的方式進(jìn)行
這樣處理,parent 和 Child 就可以彼此通過事件傳遞消息,且獨(dú)立變化,讓整個(gè)結(jié)構(gòu)松散耦合起來
工作空間的獨(dú)立
js沙箱相關(guān):https://zhuanlan.zhihu.com/p/527437146
「js 沙箱」
因?yàn)槲覀兠恳粋€(gè)子應(yīng)用都是一個(gè)單獨(dú)的項(xiàng)目和應(yīng)用,在同一個(gè)頁面中出現(xiàn)多個(gè)子應(yīng)用是很常見的場(chǎng)景
那么出于安全的考慮,例如全局變量污染、多版本庫、以及上面提到的故障隔離,還有各種復(fù)雜的場(chǎng)景下的執(zhí)行問題
我們理所當(dāng)然是需要對(duì)每個(gè)子應(yīng)用間的 js 的工作空間進(jìn)行隔離,使每個(gè)子應(yīng)用內(nèi)的執(zhí)行自洽,只關(guān)注輸出和輸入
那么js沙箱的實(shí)現(xiàn)也就無疑成為了微前端方案中急需關(guān)注的一點(diǎn)。
「css 沙箱」
既然關(guān)注了js的隔離,那css的隔離自然也逃不掉
雖然是微前端的結(jié)構(gòu),但是本質(zhì)上同一個(gè)頁面中每個(gè)子應(yīng)用的掛載依舊在同一個(gè)dom樹上
那么我們自然不希望子應(yīng)用間的樣式會(huì)出現(xiàn)互相干擾的情況,并且在子應(yīng)用切換時(shí)可以自行裝載和卸載。
說句實(shí)話,單純的iframe實(shí)現(xiàn)微前端的方案雖然有很多問題,但是在js和css的隔離的上無疑是成本最低且較好的實(shí)現(xiàn)
預(yù)加載
因?yàn)樵谖⑶岸说姆桨钢校總€(gè)子應(yīng)用都是獨(dú)立的,所以如果不做任何處理的話在一個(gè)頁面存在多個(gè)子應(yīng)用的情況下,每個(gè)子應(yīng)用的加載都是獨(dú)立的,在這種情況下我們就很容易讓用戶體驗(yàn)到不同功能之間陸續(xù)從白屏到加載完成的割裂過程。這個(gè)無疑是致命的缺陷。
并且在實(shí)際用戶使用中,瀏覽器大部分時(shí)間是處在空閑的,我們要怎么利用這樣的空閑時(shí)間,去加載其他子應(yīng)用的JS,用來優(yōu)化用戶體驗(yàn)?
公共依賴的處理問題
這個(gè)問題屬于我們軟件開發(fā)中項(xiàng)目管理的部分,子項(xiàng)目多了之后,公共依賴如果處理不好,不但造成我們開發(fā)工時(shí)的浪費(fèi),有各種重復(fù)工作,同時(shí)BUG的風(fēng)險(xiǎn)也會(huì)隨著復(fù)制的代碼過多,成指數(shù)增長(zhǎng),更不要說日后的長(zhǎng)期維護(hù)
-
可以企業(yè)內(nèi)部搭建npm服務(wù)器發(fā)布到,但問題也很明顯,例如npm包更新,需要手動(dòng)更新、任何改動(dòng)都需要子應(yīng)用重新部署上線,用的子應(yīng)用多了,這更新氣來就麻煩死了 -
webpack external 外部擴(kuò)展,可以將通用的一些包排除在bundle之外,然后使用直接訪問公共包JS的方式(一般采用CDN),直接在index.html中引入,但是這樣的話,公共依賴必須采用UMD格式,那用的時(shí)候也要遵循,就有點(diǎn)麻煩 -
webpack federation 模塊聯(lián)邦,可以使一個(gè)JS應(yīng)用,動(dòng)態(tài)加載其他JS應(yīng)用的代碼,并且我們可以把一些公共依賴,都抽離到主包。在子包中,只輸出業(yè)務(wù)代碼即可,模塊聯(lián)邦提供對(duì)應(yīng)的配置功能,并且由于是從網(wǎng)絡(luò)獲取,可以做做熱更新。但是老項(xiàng)目要升級(jí)webpack,子項(xiàng)目和主項(xiàng)目都需要進(jìn)行webpack federation的配置工作,才能用,這個(gè)也是要開發(fā)工作量,太麻煩 -
monorepo 多包管理,目前主流使用lerna框架進(jìn)行多包管理,把單獨(dú)的包抽離到獨(dú)立的子項(xiàng)目中維護(hù),后期如果項(xiàng)目穩(wěn)定,可以把依賴抽離到webpack federation 做熱更新,但是也好麻煩
路由的管理
這個(gè)為啥要關(guān)注不用我多說了吧,這位靚仔,你也不想你的路由不知道咋跳對(duì)吧。目前我知道的有兩個(gè)方案
路由劫持
方案原理大致是監(jiān)聽了popstats或者h(yuǎn)ashchange事件,并劫持了瀏覽器history下的pushState和replaceState后
主應(yīng)用控制路由
實(shí)現(xiàn)思路是主應(yīng)用使用現(xiàn)有的路由庫,例如vue router或者react router,子應(yīng)用使用webpack federation,將現(xiàn)有的頁面發(fā)布成獨(dú)立的服務(wù),在主應(yīng)用中重新配置路由,使用webpack提供的import()函數(shù),動(dòng)態(tài)加載子用的模塊
但是上面其實(shí)沒有考慮到子應(yīng)用自身的路由跳轉(zhuǎn)、子應(yīng)用a跳轉(zhuǎn)到子應(yīng)用b,或者子應(yīng)用a要打開子應(yīng)用b的指定路由的跳轉(zhuǎn)之類的場(chǎng)景子應(yīng)用的資源加載方式
是html entry還是js entry。別問,問就是快,一個(gè)加載的是按原來方式打包出來的一個(gè)html文件,一個(gè)是加載子應(yīng)用打出來的整個(gè)js文件。
而且將整個(gè)微應(yīng)用打包成一個(gè) JS 文件常見的打包優(yōu)化基本上都沒了,按需加載、首屏資源加載優(yōu)化、css 獨(dú)立打包等想都別想
支不支持應(yīng)用保活
事關(guān)keep-alive之類的需求在微前端的架構(gòu)中怎么做,這位靚仔你也不想切個(gè)子應(yīng)用,之前的狀態(tài)就全沒了吧
子應(yīng)用之間的互相嵌套
支不支持子應(yīng)用之間的互相嵌套,子應(yīng)用件互相嵌套情況下的通訊和狀態(tài)管理
是否有生命周期的設(shè)計(jì)
小結(jié)
當(dāng)然,以上這些實(shí)際上只是具體在落地微前端方案的時(shí)候需要關(guān)注的具體問題 但是從更高一點(diǎn)的層面上來看
其實(shí)我個(gè)人覺得主要其實(shí)要關(guān)注的有「三個(gè)緯度和兩個(gè)問題」
三個(gè)維度:代碼的管理、工程的獨(dú)立性、優(yōu)雅地集成
兩個(gè)問題:什么場(chǎng)景適用、怎么更好地拓展
其實(shí)以上的問題都是圍繞著解決 安全性和域完整性這兩塊問題
包括了請(qǐng)求安全、數(shù)據(jù)安全、配置安全、界面完整、交互完整、元素完整等,但是這里我們就不細(xì)細(xì)討論了
畢竟這只是一個(gè)四十分鐘不到的分享
「所以我們最后的小結(jié)是」:
在我們需要一個(gè)微前端方案時(shí),我們需要考慮的問題有如下幾點(diǎn):
-
應(yīng)用間的通訊 -
應(yīng)用間的狀態(tài)管理 -
js 沙箱 -
css 隔離 -
預(yù)加載 -
公共依賴的處理 -
路由狀態(tài)管理 -
是否支持html entry -
應(yīng)用支不支持保活 -
子應(yīng)用之間的互相嵌套 -
是否有生命周期的設(shè)計(jì)
目前都有什么流行的技術(shù)方案,它們解決了什么問題
通過第二部分的思考,我們大概了解了,假設(shè)我們?cè)趯?duì)現(xiàn)有的微前端解決方案都不清楚的情況下
我們需要去落地一個(gè)微前端的架構(gòu),我們將會(huì)需要關(guān)注哪方面,以及需要應(yīng)對(duì)哪些可能會(huì)出現(xiàn)的問題
那么在這一部分,我們將會(huì)去對(duì)比一下當(dāng)前除了 wujie 以外的較為流行的微前端方案
簡(jiǎn)單探討一下他們的實(shí)現(xiàn)方案的優(yōu)劣,以及未來可能會(huì)存在的發(fā)展方向
(因?yàn)闀r(shí)間有限,并且不是此次分享重點(diǎn),所以不會(huì)過于詳細(xì))
nginx轉(zhuǎn)發(fā)
根據(jù)我們第一部分對(duì)微前端的定義來說,nginx轉(zhuǎn)發(fā)來分割和組合應(yīng)用其實(shí)也能算是一種微前端的實(shí)現(xiàn)方案 但是根據(jù)我們第二部分的思考來說,顯然大部分問題的處理都是做不到的,所以這里就貼個(gè)圖出來湊個(gè)數(shù)算了
純iframe的實(shí)現(xiàn)方案
根據(jù)我們第二部分的思考,其實(shí)iframe的方案在一些地方有著天然的優(yōu)勢(shì),例如 iframe 的隔離完美,無論是 js、css、dom 都完全隔離開來,子應(yīng)用間的嵌套毫無壓力,隨便套,并且使用簡(jiǎn)單,沒有任何心智負(fù)擔(dān),天然支持html entry
但是「缺點(diǎn)」也非常明顯:
-
?論是使?postMessage還是通過 iframeEl.contentWindow 去獲取 iFrame 元素的 Window 對(duì)象,?或者是直接???調(diào)?????法:FrameName.window.childMethod();???調(diào)?????法:parent.window.parentMethod();來通訊都并不是太過友好的事情,需要設(shè)計(jì)?套規(guī)范的通訊標(biāo)準(zhǔn),實(shí)在過于麻煩,并且狀態(tài)管理、公共依賴的處理等都能通過其他的方式封裝實(shí)現(xiàn) -
路由狀態(tài)丟失,刷新一下,iframe 的 url 狀態(tài)就丟失了 -
dom 割裂嚴(yán)重,彈窗只能在 iframe 內(nèi)部展示,無法覆蓋全局,并且事件傳遞上存在者很大的問題,例如拖拽 -
白屏?xí)r間太長(zhǎng),對(duì)于SPA 應(yīng)用應(yīng)用來說無法接受 -
難以做預(yù)加載 -
應(yīng)用完全沒辦法保活,每次都是新的加載 -
iframe和主??共享連接池,?瀏覽器對(duì)相同域的連接有限制,所以會(huì)影響??的并?加載,出現(xiàn)iframe中的資源占?了可?連接?阻塞了主??的資源加載
所以這很難說得上是一個(gè)比較好的實(shí)現(xiàn)方案,但是后續(xù)我們會(huì)講到wujie是怎么解決iframe的缺點(diǎn),打造一個(gè)接近完美的iframe方案的。
基座模式的代表 qiankun.js (基于single-spa)
qiankun是一個(gè)很經(jīng)典的基座模式下的微服務(wù)方案,但是在使用成本上來說是有點(diǎn)大的,它對(duì)代碼的侵入型很強(qiáng),如果要改造的話,從 webpack、代碼、路由等等都要做一系列的適配
| 能力 | 現(xiàn)狀 |
|---|---|
| 通訊與狀態(tài)管理 | 1、初始化狀態(tài)通過props傳入子組件 2、qiankun通過initGlobalState, onGlobalStateChange, setGlobalState實(shí)現(xiàn)主應(yīng)?的全局狀態(tài)管理,然后默認(rèn)會(huì)通過props將通信?法傳遞給?應(yīng)?,但是本質(zhì)上還是通過發(fā)布 - 訂閱的方式來進(jìn)行通訊 3、主子應(yīng)用localStrage、cookie可共享 所以在通訊上并不是很完美 |
| js和css隔離 | 提供js和css隔離,但是在js的沙箱方面依然有不少坑有問題,近一年的很多changelog都是在修js沙箱的問題 |
| 預(yù)加載 | 做了靜態(tài)資源的預(yù)加載能力 |
| 公共依賴的處理 | 文檔內(nèi)寫明不推薦,但是硬要的話可以在微應(yīng)用中將公共依賴配置成 external,然后在主應(yīng)用中導(dǎo)入這些公共依賴的 |
| 路由管理 | 1、每個(gè)子應(yīng)用中注冊(cè),然后由主應(yīng)用進(jìn)行管理,主應(yīng)用的路由實(shí)例通過 props 傳給微應(yīng)用,微應(yīng)用這個(gè)路由實(shí)例跳轉(zhuǎn) 2、注冊(cè)微應(yīng)用的基礎(chǔ)配置信息。當(dāng)瀏覽器 url 發(fā)生變化時(shí),會(huì)自動(dòng)檢查每一個(gè)微應(yīng)用注冊(cè)的 activeRule 規(guī)則,符合規(guī)則的應(yīng)用將會(huì)被自動(dòng)激活 3、頁面上不能同時(shí)顯示多個(gè)依賴于路由的微應(yīng)用,因?yàn)闉g覽器只有一個(gè) url,如果有多個(gè)依賴路由的微應(yīng)用同時(shí)被激活,那么必定會(huì)導(dǎo)致其中一個(gè) 404。 「(因?yàn)榛诼酚善ヅ洌詿o法同時(shí)激活多個(gè)子應(yīng)用)」 |
| html entry | 支持 |
| 應(yīng)用保活 | 無法支持子應(yīng)用保活 |
| 應(yīng)用嵌套 | 支持 |
| 生命周期 | 支持 |
micro-app
| 能力 | 現(xiàn)狀 |
|---|---|
| 通訊與狀態(tài)管理 | 基于發(fā)布訂閱+CustomEvent |
| js和css隔離 | js: 使用Proxy攔截了用戶全局操作的行為。 css: 利用標(biāo)簽的name屬性為每個(gè)樣式添加前綴,將子應(yīng)用的樣式影響禁錮在當(dāng)前標(biāo)簽區(qū)域,但是依舊沒辦法必定隔絕 |
| 預(yù)加載 | 在瀏覽器空閑時(shí)間,依照開發(fā)者傳入的順序,依次加載每個(gè)應(yīng)用的靜態(tài)資源,以確保不會(huì)影響基座應(yīng)用的性能 |
| 公共依賴的處理 | 沒有解決基座應(yīng)用和子應(yīng)用共用依賴的特性,issues中回答了在計(jì)劃中,但是還沒想好怎么做 |
| 路由管理 | 每個(gè)應(yīng)用的路由實(shí)例都是不同的,應(yīng)用的路由實(shí)例只能控制自身,無法影響其它應(yīng)用,包括基座應(yīng)用無法通過控制自身路由影響到子應(yīng),路由跳轉(zhuǎn)只有三種方式:window.history,通過history.pushState或history.replaceState進(jìn)行跳轉(zhuǎn)、通過數(shù)據(jù)通信控制跳轉(zhuǎn)(基座控制子應(yīng)用跳轉(zhuǎn),子應(yīng)用監(jiān)聽基座數(shù)據(jù)變化而跳轉(zhuǎn))、傳遞路由實(shí)例方法,把實(shí)例傳到子應(yīng)用里去跳轉(zhuǎn)(子應(yīng)用控制基座跳轉(zhuǎn)) url屬性和子應(yīng)用路由沒有關(guān)系,只是用來加載html資源 |
| html entry | 支持, 實(shí)際上是以類WebComponent + HTML Entry實(shí)現(xiàn)微前端的組件化,但是webcomponents沒有做降級(jí)處理 |
| 應(yīng)用保活 | 支持,< micro-app name='xx' url='xx' keep-alive> |
| 應(yīng)用嵌套 | 支持 |
| 生命周期 | 支持 |
Module Federation
| 能力 | 現(xiàn)狀 |
|---|---|
| 通訊與狀態(tài)管理 | 別想了,互相之間都是獨(dú)立模塊,并且去中心了,你還想咋共享? |
| js和css隔離 | js: 沒有沙箱,全憑開發(fā)者自覺不瞎搞 css: 別想了,只能通過 postcss-selector-namespace 添加前綴或者別名之類的方式來處理 簡(jiǎn)單來說,沒有有用的 css 沙箱和 js 沙箱,一切需求靠用戶自覺 |
| 預(yù)加載 | 沒有利用瀏覽器空閑時(shí)間去做子應(yīng)用加載的處理 |
| 公共依賴的處理 | 天然支持 |
| 路由管理 | 微應(yīng)用的路由是有發(fā)生沖突的可能性的,為了實(shí)現(xiàn)獨(dú)立部署能切換頁面,各微應(yīng)用都有自己的路由,要解決就只能微應(yīng)用單獨(dú)部署時(shí),使用 web 路由,集成到 container 時(shí),使用內(nèi)存路由,web 路由由 container 接管,瀏覽器地址欄變化時(shí),告 訴集成進(jìn)來的微應(yīng)用,然后微應(yīng)用再跳轉(zhuǎn)到相應(yīng)的頁面。 |
| html entry | 沒有 |
| 應(yīng)用保活 | 沒有,別想 |
| 應(yīng)用嵌套 | 想怎么套就怎么套 |
| 生命周期 | 別想 |
wujie 是怎么解決以上問題的?
通過第二和第三部分的思考,我們大概了解了落地一個(gè)微前端的架構(gòu)可能會(huì)需要面臨的挑戰(zhàn),以及當(dāng)前流行的技術(shù)方案對(duì)于面臨的問題提出了哪些思想或解決方案 所以第四部分我們就簡(jiǎn)單講講第二部分我們所考慮到的問題在 wujie 的方案中是怎么處理的
其實(shí)我們可以直接看 wujie 的github倉庫 可以發(fā)現(xiàn)它的實(shí)現(xiàn)實(shí)際上非常簡(jiǎn)單,加起來不過3000行代碼,所以打消了我想挑一塊源碼功能的實(shí)現(xiàn)來講用于消磨時(shí)間的打算(滑稽.jpg)
wujie是怎么解決純 iframe 微前端方案所遇到問題的?
wujie 是一個(gè)iframe + webComponent 的解決方案 既然提到了iframe,那么我們肯定要關(guān)注前面提到的iframe存在著通訊、路由、dom割裂嚴(yán)重、白屏?xí)r間長(zhǎng)、難做預(yù)加載、應(yīng)用無法保活、瀏覽器對(duì)相同域的連接有限制,會(huì)影響??的并?加載等問題在wujie中是否被解決
最重要的通訊問題
在wujie中提供了三種通訊方式:
(1 「props 通信」,主應(yīng)用可以通過props注入數(shù)據(jù)和方法
(2 「window 通信」,由于在設(shè)計(jì)上子應(yīng)用運(yùn)行的iframe的src和主應(yīng)用是同域的,所以相互可以直接通信,這個(gè)我們?cè)谙乱徊街屑?xì)述
(3 「eventBus 通信」 其中最有趣的就是這一點(diǎn),wujie 提供了一套去中心化的通訊方式
其中的 eventBus 的功能實(shí)現(xiàn)非常簡(jiǎn)單簡(jiǎn)潔,整塊功能的實(shí)現(xiàn)不過 105 行,除了全部事件的存儲(chǔ)的map會(huì)根據(jù)是否存在wujie進(jìn)行判斷和兼容之外,本質(zhì)上就是一個(gè)非常常規(guī)的發(fā)布訂閱的實(shí)現(xiàn),跟咱們今年校招的筆試題其實(shí)是一樣的,撐死是個(gè)加強(qiáng)版
iframe 問題解決:dom 割裂嚴(yán)重、路由狀態(tài)丟失、通信非常困難的問題
路由狀態(tài)的問題
假如我們?cè)趹?yīng)用a中想要加載應(yīng)用b,那么我們可以怎么做才能避免上述問題?
在應(yīng)用 A 中構(gòu)造一個(gè)shadowRoot 和iframe,然后將應(yīng)用 B 的html寫入shadowRoot中,js運(yùn)行在iframe中,因?yàn)閕frame的js隔離真的很完美。
這時(shí)候我們注意 iframe 的 url,iframe保持和主應(yīng)用同域但是保留子應(yīng)用的路徑信息,這樣子應(yīng)用的js可以運(yùn)行在iframe的location和history中保持路由正確。
「這樣就可以解決了路由狀態(tài)的問題,并且解決可以使用window來通訊」
Shadow DOM API 的 ShadowRoot 接口是一個(gè) DOM 子樹的根節(jié)點(diǎn),它與文檔的主 DOM 樹分開渲染。
「源碼」
dom割裂的問題
那在iframe中dom割裂的問題要怎么處理?
在iframe中攔截document對(duì)象,統(tǒng)一將dom指向shadowRoot,此時(shí)比如新建元素、彈窗或者冒泡組件就可以正常約束在shadowRoot內(nèi)部。
我們可以從源碼中看出,在非降級(jí)的情況下, wujie對(duì)iframe的 document進(jìn)行了代理,從而解決了解決了 dom 割裂嚴(yán)重的問題
首次白屏的問題
那這時(shí)候?qū)嶋H就只剩下了白屏、預(yù)加載和應(yīng)用保活的問題
白屏實(shí)際上可以分為兩個(gè)場(chǎng)景,一是「首次加載白屏」,二是「切換應(yīng)用時(shí)白屏」
「首次白屏的問題」,wujie實(shí)例可以提前實(shí)例化,包括shadowRoot、iframe的創(chuàng)建、js的執(zhí)行,這樣極大的加快子應(yīng)用第一次打開的時(shí)間
「切換白屏的問題」,一旦wujie實(shí)例可以緩存下來,子應(yīng)用的切換成本變的極低,如果采用保活模式,那么相當(dāng)于shadowRoot的插拔
所以以上的整套機(jī)制在wujie中的實(shí)現(xiàn)對(duì)應(yīng)的流程圖如下圖一樣
預(yù)加載的處理
對(duì)于預(yù)加載的處理也非常簡(jiǎn)單,在子應(yīng)用使用fiber模式執(zhí)行的情況下,使用 requestIdleCallback ,在瀏覽器空閑時(shí)間去加載資源
js和css隔離
這個(gè)不用多言,懂的都懂
公共依賴的處理
文檔內(nèi)寫明不推薦,但是硬要的話可以在微應(yīng)用中將公共依賴配置成 external,然后在主應(yīng)用中導(dǎo)入這些公共依賴的
路由管理
跟其他方案不同的是,wujie的路由管理有兩個(gè)比較有意思的點(diǎn)
「1、路由同步」
會(huì)將子應(yīng)用路徑的path+query+hash通過window.encodeURIComponent編碼后掛載在主應(yīng)用url的查詢參數(shù)上,其中key值為子應(yīng)用的 name。
開啟路由同步后,「刷新瀏覽器或者將url分享出去子應(yīng)用的路由狀態(tài)都不會(huì)丟失」,當(dāng)一個(gè)頁面存在多個(gè)子應(yīng)用時(shí)無界支持所有子應(yīng)用路由同步,瀏覽器刷新、前進(jìn)、后退子應(yīng)用路由狀態(tài)也都不會(huì)丟失,并且「提供短路徑的能力」,當(dāng)子應(yīng)用的url過長(zhǎng)時(shí),可以通過配置 prefix 來縮短子應(yīng)用同步到主應(yīng)用的路徑,無界在選取短路徑的時(shí)候,按照匹配最長(zhǎng)路徑原則選取短路徑。
「2、路由跳轉(zhuǎn)」
無界支持子應(yīng)用間的路由的跳轉(zhuǎn),例如子應(yīng)用a可以跳轉(zhuǎn)子應(yīng)用b,也可以從子應(yīng)用a跳轉(zhuǎn)到子應(yīng)用b的指定路由中,這點(diǎn)在其他方案中暫時(shí)還沒看到有。
并且如果子應(yīng)用a要跳轉(zhuǎn)到子應(yīng)用b是保活應(yīng)用,并且已經(jīng)進(jìn)行過初始化了,那也有對(duì)應(yīng)的方案使子應(yīng)用b的狀態(tài)不會(huì)因?yàn)樘D(zhuǎn)而丟失
降級(jí)處理
wujie 和 micro-app 的發(fā)布時(shí)間其實(shí)相距不遠(yuǎn),并且兩者都用到了webComponent的能力,但是wujie 對(duì)于webComponent 特性不支持的情況做了無感知的降級(jí)方案,但是micro-app沒有
結(jié)束
以上內(nèi)容純屬個(gè)人觀點(diǎn),僅供討論,不保證內(nèi)容正確性
往期推薦
最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...
