基于 iframe 的全新微前端方案
作者:damyxu,騰訊 PCG 前端開(kāi)發(fā)工程師
iframe是一個(gè)天然的微前端方案,但受限于跨域的嚴(yán)格限制而無(wú)法很好的應(yīng)用,本文介紹一種基于 iframe 的全新微前端方案,繼承iframe的優(yōu)點(diǎn),補(bǔ)足 iframe 的缺點(diǎn),讓 iframe 煥發(fā)新生。
背景
前端開(kāi)發(fā)中我們對(duì)iframe已經(jīng)非常熟悉了,那么iframe的作用是什么?可以歸納如下:
在一個(gè)web應(yīng)用中可以獨(dú)立的運(yùn)行另一個(gè)web應(yīng)用
這個(gè)概念已經(jīng)和微前端不謀而合,相對(duì)于目前配置復(fù)雜、高適配成本的微前端方案來(lái)說(shuō),采用iframe方案具有一些顯著的優(yōu)點(diǎn):
非常簡(jiǎn)單,使用沒(méi)有任何心智負(fù)擔(dān)
隔離完美,無(wú)論是 js、css、dom 都完全隔離開(kāi)來(lái)
多應(yīng)用激活,頁(yè)面上可以擺放多個(gè)iframe來(lái)組合業(yè)務(wù)
但是開(kāi)發(fā)者又厭惡使用iframe,因?yàn)?strong>缺點(diǎn)也非常明顯:
路由狀態(tài)丟失,刷新一下,iframe 的 url 狀態(tài)就丟失了
dom 割裂嚴(yán)重,彈窗只能在 iframe 內(nèi)部展示,無(wú)法覆蓋全局
通信非常困難,只能通過(guò) postmessage 傳遞序列化的消息
白屏?xí)r間太長(zhǎng),對(duì)于SPA 應(yīng)用應(yīng)用來(lái)說(shuō)無(wú)法接受
能否打造一個(gè)完美的iframe,保留所有的優(yōu)點(diǎn)的同時(shí),解決掉所有的缺點(diǎn)呢?
無(wú)界方案
無(wú)界微前端框架通過(guò)繼承iframe的優(yōu)點(diǎn),解決iframe的缺點(diǎn),打造一個(gè)接近完美的iframe方案。
來(lái)看無(wú)界如何一步一步的解決iframe的問(wèn)題,假設(shè)我們有 A 應(yīng)用,想要加載 B 應(yīng)用:
在應(yīng)用 A 中構(gòu)造一個(gè)shadow和iframe,然后將應(yīng)用 B 的html寫(xiě)入shadow中,js運(yùn)行在iframe中,注意iframe的url,iframe保持和主應(yīng)用同域但是保留子應(yīng)用的路徑信息,這樣子應(yīng)用的js可以運(yùn)行在iframe的location和history中保持路由正確。
image-20211206160113792在iframe中攔截document對(duì)象,統(tǒng)一將dom指向shadowRoot,此時(shí)比如新建元素、彈窗或者冒泡組件就可以正常約束在shadowRoot內(nèi)部。
接下來(lái)的三步分別解決iframe的三個(gè)缺點(diǎn):
? dom 割裂嚴(yán)重的問(wèn)題,主應(yīng)用提供一個(gè)容器給到shadowRoot插拔,shadowRoot內(nèi)部的彈窗也就可以覆蓋到整個(gè)應(yīng)用 A
? 路由狀態(tài)丟失的問(wèn)題,瀏覽器的前進(jìn)后退可以天然的作用到iframe上,此時(shí)監(jiān)聽(tīng)iframe的路由變化并同步到主應(yīng)用,如果刷新瀏覽器,就可以從 url 讀回保存的路由
? 通信非常困難的問(wèn)題,iframe和主應(yīng)用是同域的,天然的共享內(nèi)存通信,而且無(wú)界提供了一個(gè)去中心化的事件機(jī)制
將這套機(jī)制封裝進(jìn)wujie框架:
我們可以發(fā)現(xiàn):
? 首次白屏的問(wèn)題,wujie實(shí)例可以提前實(shí)例化,包括shadowRoot、iframe的創(chuàng)建、js的執(zhí)行,這樣極大的加快子應(yīng)用第一次打開(kāi)的時(shí)間
? 切換白屏的問(wèn)題,一旦wujie實(shí)例可以緩存下來(lái),子應(yīng)用的切換成本變的極低,如果采用保活模式,那么相當(dāng)于shadowRoot的插拔
由于子應(yīng)用完全獨(dú)立的運(yùn)行在iframe內(nèi),路由依賴iframe的location和history,我們還可以在一張頁(yè)面上同時(shí)激活多個(gè)子應(yīng)用,由于iframe和主應(yīng)用處于同一個(gè)top-level browsing context,因此瀏覽器前進(jìn)、后退都可以作用到到子應(yīng)用:
image-20211206160244704通過(guò)以上方法,無(wú)界方案可以做到:
? 非常簡(jiǎn)單,使用沒(méi)有任何心智負(fù)擔(dān)
? 隔離完美,無(wú)論是 js、css、dom 都完全隔離開(kāi)來(lái)
? 多應(yīng)用激活,頁(yè)面上可以擺放多個(gè)iframe來(lái)組合業(yè)務(wù)
路由狀態(tài)丟失,刷新一下,iframe 的 url 狀態(tài)就丟失了
dom 割裂嚴(yán)重,彈窗只能在 iframe 內(nèi)部展示,無(wú)法覆蓋全局
通信非常困難,只能通過(guò) postmessage 傳遞序列化的消息
白屏?xí)r間太長(zhǎng),對(duì)于SPA 應(yīng)用應(yīng)用來(lái)說(shuō)無(wú)法接受
使用無(wú)界
如果主應(yīng)用是vue框架:
安裝
`npm?i?@tencent/wujie-vue?-S`引入
mport?WujieVue?from?"@tencent/wujie-vue"; Vue.use(WujieVue);使用
<WujieVuewidth="100%"height="100%"name="xxx"url="xxx":sync="true":fetch="fetch":props="props"@xxx="handleXXX" ></WujieVue>其他框架也會(huì)在近期上線
適配成本
無(wú)界的適配成本非常低
對(duì)于主應(yīng)用無(wú)需做任何改造
對(duì)于子應(yīng)用:
前提,必須開(kāi)放跨域配置,因?yàn)樽討?yīng)用是在主應(yīng)用域內(nèi)請(qǐng)求和運(yùn)行的
對(duì)webpack應(yīng)用,修改動(dòng)態(tài)加載路徑
如果子應(yīng)用保活模式則無(wú)需進(jìn)一步修改,非保活則需要將實(shí)例化掛載到無(wú)界生命周期內(nèi)
實(shí)現(xiàn)細(xì)節(jié)
實(shí)現(xiàn)一個(gè)純凈的 iframe
子應(yīng)用運(yùn)行在一個(gè)和主應(yīng)用同域的iframe中,設(shè)置src為替換了主域名host的子應(yīng)用url,子應(yīng)用路由只取location的pathname和hash
但是一旦設(shè)置src后,iframe由于同域,會(huì)加載主應(yīng)用的html、js,所以必須在iframe實(shí)例化完成并且還沒(méi)有加載完html時(shí)中斷加載,防止污染子應(yīng)用
此時(shí)可以采用輪詢監(jiān)聽(tīng)document.readyState狀態(tài)來(lái)及時(shí)中斷,對(duì)于一些瀏覽器比如safari狀態(tài)不準(zhǔn)確,可以在wujie主動(dòng)拋錯(cuò)來(lái)防止有主應(yīng)用的js運(yùn)行
iframe 數(shù)據(jù)劫持和注入
子應(yīng)用的代碼 code 在 iframe 內(nèi)部訪問(wèn) window,document、location 都被劫持到相應(yīng)的 proxy,并且還會(huì)注入$wujie對(duì)象供子應(yīng)用調(diào)用
const?script?=?`(function(window,?self,?global,?document,?location,?$wujie)?{${code}\n}).bind(window.__WUJIE.proxy)(window.__WUJIE.proxy,window.__WUJIE.proxy,window.__WUJIE.proxy,window.__WUJIE.proxy.document,window.__WUJIE.proxy.location,window.__WUJIE.provide);`;iframe 和 shadowRoot 副作用的處理
iframe 內(nèi)部的副作用處理在初始化iframe時(shí)進(jìn)行,主要分為如下幾部
/***?1、location劫持后的數(shù)據(jù)修改回來(lái),防止跨域錯(cuò)誤*?2、同步路由到主應(yīng)用*/ patchIframeHistory(iframeWindow,?appPublicPath,?mainPublicPath); /***?對(duì)window.addEventListener進(jìn)行劫持,比如resize事件必須是監(jiān)聽(tīng)主應(yīng)用的*/ patchIframeEvents(iframeWindow); /***?注入私有變量*/ patchIframeVariable(iframeWindow,?appPublicPath); /***?將有DOM副作用的統(tǒng)一在此修改,比如mutationObserver必須調(diào)用主應(yīng)用的*/ patchIframeDomEffect(iframeWindow); /***?子應(yīng)用前進(jìn)后退,同步路由到主應(yīng)用*/ syncIframeUrlToWindow(iframeWindow);ShadowRoot 內(nèi)部的副作用必須進(jìn)行處理,主要處理的就是shadowRoot的head和body
shadowRoot.head.appendChild?=?getOverwrittenAppendChildOrInsertBefore({rawDOMAppendOrInsertBefore:?rawHeadAppendChild})?as?typeof?rawHeadAppendChildshadowRoot.head.insertBefore?=?getOverwrittenAppendChildOrInsertBefore({rawDOMAppendOrInsertBefore:?rawHeadInsertBefore?as?any})?as?typeof?rawHeadInsertBeforeshadowRoot.body.appendChild?=?getOverwrittenAppendChildOrInsertBefore({rawDOMAppendOrInsertBefore:?rawBodyAppendChild})?as?typeof?rawBodyAppendChildshadowRoot.body.insertBefore?=?getOverwrittenAppendChildOrInsertBefore({rawDOMAppendOrInsertBefore:?rawBodyInsertBefore?as?any})?as?typeof?rawBodyInsertBeforegetOverwrittenAppendChildOrInsertBefore主要是處理四種類型標(biāo)簽:
link/style標(biāo)簽
收集stylesheetElement并用于子應(yīng)用重新激活重建樣式
script標(biāo)簽
動(dòng)態(tài)插入的script標(biāo)簽必須從ShadowRoot轉(zhuǎn)移至iframe內(nèi)部執(zhí)行
iframe標(biāo)簽
修復(fù)iframe的指向?qū)?yīng)子應(yīng)用iframe的window
iframe 的 document 改造
由于js在iframe運(yùn)行需要和shadowRoot,包括元素創(chuàng)建、事件綁定等等,將iframe的document進(jìn)行劫持:
所有的元素的查詢?nèi)看淼絪hadowRoot內(nèi)去查詢
head和body代理到shadowRoot的對(duì)應(yīng)html元素上
iframe 的 location 改造
將iframe的location進(jìn)行劫持:
由于iframe的url的host是主應(yīng)用的,所以需要將host改回子應(yīng)用自己的
對(duì)于location.href特殊邏輯的處理
總結(jié)
通過(guò)上面原理以及細(xì)節(jié)的闡述,我們可以得出無(wú)界微前端框架的幾點(diǎn)優(yōu)勢(shì):
多應(yīng)用同時(shí)激活在線框架具備同時(shí)激活多應(yīng)用,并保持這些應(yīng)用路由同步的能力
組件式的使用方式無(wú)需注冊(cè),更無(wú)需路由適配,在組件內(nèi)使用,跟隨組件裝載、卸載
應(yīng)用級(jí)別的 keep-alive子應(yīng)用開(kāi)啟保活模式后,應(yīng)用發(fā)生切換時(shí)整個(gè)子應(yīng)用的狀態(tài)可以保存下來(lái)不丟失,結(jié)合預(yù)執(zhí)行模式可以獲得類似ssr的打開(kāi)體驗(yàn)
純凈無(wú)污染
無(wú)界利用iframe和ShadowRoot來(lái)搭建天然的js隔離沙箱和css隔離沙箱
利用iframe的history和主應(yīng)用的history在同一個(gè)top-level browsing context來(lái)搭建天然的路由同步機(jī)制
副作用局限在沙箱內(nèi)部,子應(yīng)用切換無(wú)需任何清理工作,沒(méi)有額外的切換成本
性能和體積兼具
子應(yīng)用執(zhí)行性能和原生一致,子應(yīng)用實(shí)例instance運(yùn)行在iframe的window上下文中,避免with(proxyWindow){code}這樣指定代碼執(zhí)行上下文導(dǎo)致的性能下降,但是多了實(shí)例化iframe的一次性的開(kāi)銷,可以通過(guò)proloadApp提前實(shí)例化
包體積只有11kb,非常輕量,借助iframe和ShadowRoot來(lái)實(shí)現(xiàn)沙箱,極大的減小了代碼量
開(kāi)箱即用不管是樣式的兼容、路由的處理、彈窗的處理、熱更新的加載,子應(yīng)用完成接入即可開(kāi)箱即用無(wú)需額外處理,應(yīng)用接入成本也極低
相應(yīng)的也有所不足:
內(nèi)存占用較高,為了降低子應(yīng)用的白屏?xí)r間,將未激活子應(yīng)用的shadowRoot和iframe常駐內(nèi)存并且保活模式下每張頁(yè)面都需要獨(dú)占一個(gè)wujie實(shí)例,內(nèi)存開(kāi)銷較大
兼容性一般,目前用到了瀏覽器的shadowRoot和proxy能力,并且沒(méi)有做降級(jí)方案
iframe劫持document到shadowRoot時(shí),某些第三方庫(kù)可能無(wú)法兼容導(dǎo)致穿透
近期好文:
微信支付團(tuán)隊(duì)精益研發(fā)實(shí)踐總結(jié)
梳理正則表達(dá)式發(fā)展史
程序員媽媽的“work-life balance”,直面想象中的困難
程序員教你做咖啡
總結(jié)
以上是生活随笔為你收集整理的基于 iframe 的全新微前端方案的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 送技术、链资源、配资金……腾讯技术公益创
- 下一篇: 2021 大前端技术回顾及未来展望