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