為iframe正名,你可能并不需要微前端
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
阿里巴巴終端技術(shù) 2023年01月05日 作者:劉顯安(碼怪)
前言最近幾年微前端很火,火到有時(shí)候項(xiàng)目里面用到了iframe還要偷偷摸摸地藏起來生怕被別人知道了,因?yàn)閾?dān)心被人質(zhì)疑:你為什么不用微前端方案?直到最近筆者接手一個(gè)項(xiàng)目,需要將現(xiàn)有的一個(gè)系統(tǒng)整體嵌入到另外一個(gè)系統(tǒng)(一共20多個(gè)頁面),在被微前端坑了幾次之后,回過頭發(fā)現(xiàn),iframe真香! qiankun的作者有一篇《Why Not Iframe》 介紹了iframe的優(yōu)缺點(diǎn)(不過作者還有一篇《你可能并不需要微前端》給微前端降降火),誠然iframe確實(shí)存在很多缺點(diǎn),但是在選擇一個(gè)方案的時(shí)候還是要具體場(chǎng)景具體分析,它可能在當(dāng)下很流行,但它不一定在任何時(shí)候都是最優(yōu)解:iframe的這些缺點(diǎn)對(duì)我來說是否能夠接受?它的缺點(diǎn)是否有其它方法可以彌補(bǔ)?使用它到底是利大于弊還是弊大于利?我們需要在優(yōu)缺點(diǎn)之間找到一個(gè)平衡。 優(yōu)缺點(diǎn)分析iframe適合的場(chǎng)景由于iframe的一些限制,部分場(chǎng)景并不適合用iframe,比如像下面這種iframe只占據(jù)頁面中間部分區(qū)域,由于父頁面已經(jīng)有一個(gè)滾動(dòng)條了,為了避免出現(xiàn)雙滾動(dòng)條,只能動(dòng)態(tài)計(jì)算iframe的內(nèi)容高度賦值給iframe,使得iframe高度完全撐滿,但這樣帶來的問題是彈窗很難處理,如果居中的話一般彈窗都相對(duì)的是iframe內(nèi)容高度而不是屏幕高度,從而導(dǎo)致彈窗可能看不見,如果固定彈窗top又會(huì)導(dǎo)致彈窗跟隨頁面滾動(dòng),而且稍有不慎iframe內(nèi)容高度計(jì)算有一點(diǎn)點(diǎn)偏差就會(huì)出現(xiàn)雙滾動(dòng)條。 所以:
為什么一定要滿足“iframe占據(jù)全部?jī)?nèi)容區(qū)域”這個(gè)條件呢?可以想象一下下面這種場(chǎng)景,滾動(dòng)條出現(xiàn)在頁面中間應(yīng)該大部分人都無法接受: 實(shí)戰(zhàn):A系統(tǒng)接入B系統(tǒng)滿足“iframe占據(jù)全部?jī)?nèi)容區(qū)域”條件的場(chǎng)景,iframe的幾個(gè)缺點(diǎn)都比較好解決。下面通過一個(gè)實(shí)際案例來詳細(xì)介紹將一個(gè)線上在運(yùn)行的系統(tǒng)接入到另外一個(gè)系統(tǒng)的全過程。以筆者前段時(shí)間剛完成的ACP(全稱Alibaba.com Pay,阿里巴巴國(guó)際站旗下一站式全球收款平臺(tái),下稱A系統(tǒng))接入生意貸(下稱B系統(tǒng))為例,已知:
我們希望的效果: 假設(shè)我們新增一個(gè)頁面 class App extends React.Component { state = { currentEntry: decodeURIComponent(iutil.getParam('entry') || '') || '', }; render() { return <div> <iframe id="microFrontIframe" src={this.state.currentEntry}/> </div>; } } 隱藏原系統(tǒng)導(dǎo)航菜單因?yàn)槭墙尤氲搅硗庖粋€(gè)系統(tǒng),所以需要將原系統(tǒng)的菜單和導(dǎo)航等都通過一個(gè)類似“hideLayout”的參數(shù)去隱藏。 前進(jìn)后退處理需要特別注意的是,iframe頁面內(nèi)部的跳轉(zhuǎn)雖然不會(huì)讓瀏覽器地址欄發(fā)生變化,但是卻會(huì)產(chǎn)生一個(gè)看不見的“history記錄”,也就是點(diǎn)擊前進(jìn)或后退按鈕( 所以準(zhǔn)確來說前進(jìn)后退無需我們做任何處理,我們要做的就是讓瀏覽器地址欄同步更新即可。
URL的同步更新讓URL同步更新需要處理2個(gè)問題,一個(gè)是什么時(shí)候去觸發(fā)更新的動(dòng)作,一個(gè)是URL更新的規(guī)律,即父頁面的URL地址(A系統(tǒng))與iframe的URL地址(B系統(tǒng))映射關(guān)系的維護(hù)。 保證URL同步更新功能正常需要滿足這3種情況:
什么時(shí)候更新URL地址首先想到的肯定是在iframe加載完發(fā)送一個(gè)通知給父頁面,父頁面通過
B系統(tǒng): <script> var postMessage = function(type, data) { if (window.parent !== window) { window.parent.postMessage({ type: type, data: data, }, '*'); } } // 為了讓URL地址盡早地更新,這段代碼需要盡可能前置,例如可以直接放在document.head中 postMessage('afterHistoryChange', { url: location.href }); </script> A系統(tǒng): window.addEventListener('message', e => { const { data, type } = e.data || {}; if (type === 'afterHistoryChange' && data?.url) { // 這里先采用一個(gè)兜底的URL承接任意地址 const entry = `/fin/base.html?entry=${encodeURIComponent(data.url)}`; // 地址不一樣才需要更新 if (location.pathname + location.search !== entry) { window.history.replaceState(null, '', entry); } } }); 優(yōu)化URL的更新速度按照上面的方法實(shí)現(xiàn)后可以發(fā)現(xiàn),URL雖然可以更新但是速度有點(diǎn)慢,點(diǎn)擊跳轉(zhuǎn)后一般需要等待7-800毫秒地址欄才會(huì)更新,有點(diǎn)美中不足??梢园训刂窓诘母略凇疤D(zhuǎn)后”基礎(chǔ)之上再加一個(gè)“跳轉(zhuǎn)前”。為此我們必須有一個(gè)全局的beforeRedirect鉤子,先不考慮它的具體實(shí)現(xiàn): B系統(tǒng): function beforeRedirect(href) { postMessage('beforeHistoryChange', { url: href }); } A系統(tǒng): window.addEventListener('message', e => { const { data, type } = e.data || {}; if ((type === 'beforeHistoryChange' || type === 'afterHistoryChange') && data?.url) { // 這里先采用一個(gè)兜底的URL承接任意地址 const entry = `/fin/base.html?entry=${encodeURIComponent(data.url)}`; // 地址不一樣才需要更新 if (location.pathname + location.search !== entry) { window.history.replaceState(null, '', entry); } } }); 加上上述代碼之后,點(diǎn)擊iframe中的跳轉(zhuǎn)鏈接,URL會(huì)實(shí)時(shí)更新,瀏覽器的前進(jìn)后退功能也正常。
美化URL地址簡(jiǎn)單的使用 首先,新增一個(gè)SPA頁面 // A系統(tǒng)地址到B系統(tǒng)地址映射 const entryMap = { '/fin/home.html': 'https://fs.alibaba.com/xxx/home.htm?hideLayout=1', '/fin/apply.html': 'https://fs.alibaba.com/xxx/apply?hideLayout=1', '/fin/failed.html': 'https://fs.aibaba.com/xxx/failed?hideLayout=1', // 省略 }; const iframeMap = {}; // 同時(shí)再維護(hù)一個(gè)子頁面 -> 父頁面URL映射 for (const entry in entryMap) { iframeMap[entryMap[entry].split('?')[0]] = entry; } class App extends React.Component { state = { currentEntry: decodeURIComponent(iutil.getParam('entry') || '') || entryMap[location.pathname] || '', }; render() { return <div> <iframe id="microFrontIframe" src={this.state.currentEntry}/> </div>; } } 同時(shí)完善一下更新URL地址部分: // base.html繼續(xù)用作兜底 let entry = `/fin/base.html?entry=${encodeURIComponent(data.url)}`; const [path, search] = data.url.split('?'); if (iframeMap[path]) { entry = `${iframeMap[path]}?${search || ''}`; } // 地址不一樣才需要更新 if (location.pathname + location.search !== entry) { window.history.replaceState(null, '', entry); }
全局跳轉(zhuǎn)攔截為什么一定要做全局跳轉(zhuǎn)攔截呢?一個(gè)因?yàn)槲覀冃枰裩ideLayout參數(shù)一直透?jìng)飨氯?,否則就會(huì)點(diǎn)著點(diǎn)著突然出現(xiàn)下面這種雙菜單的情況: 另一個(gè)是有些頁面在被嵌入前是當(dāng)前頁面打開的,但是被嵌入后不能繼續(xù)在當(dāng)前iframe打開,比如支付寶付款這種第三方頁面,想象一下下面這種情況會(huì)不會(huì)覺得很怪?所以這類頁面一定要做特殊處理讓它跳出去而不是當(dāng)前頁面打開。 URL跳轉(zhuǎn)可以分為服務(wù)端跳轉(zhuǎn)和瀏覽器跳轉(zhuǎn),瀏覽器跳轉(zhuǎn)又包括A標(biāo)簽跳轉(zhuǎn)、location.href跳轉(zhuǎn)、window.open跳轉(zhuǎn)、historyAPI跳轉(zhuǎn)等; 而根據(jù)是否新標(biāo)簽打開又可以分為以下4種場(chǎng)景:
為此,先定義好一個(gè) // 維護(hù)一個(gè)需要做特殊處理的第三方頁面列表 const thirdPageList = [ 'https://service.alibaba.com/', 'https://sale.alibaba.com/xxx/', 'https://alipay.com/xxx/', // ... ]; /** * 封裝統(tǒng)一的跳轉(zhuǎn)攔截鉤子,處理參數(shù)透?jìng)骱鸵恍┨厥馇闆r * @param {*} href 要跳轉(zhuǎn)的地址,允許傳入相對(duì)路徑 * @param {*} isNewTab 是否要新標(biāo)簽打開 * @param {*} isParentOpen 是否要在父頁面打開 * @returns 返回處理好的跳轉(zhuǎn)地址,如果沒有返回值則表示不需要繼續(xù)處理跳轉(zhuǎn) */ function beforeRedirect(href, isNewTab) { if (!href) { return; } // 傳過來的href可能是相對(duì)路徑,為了做統(tǒng)一判斷需要轉(zhuǎn)成絕對(duì)路徑 if (href.indexOf('http') !== 0) { var a = document.createElement('a'); a.href = href; href = a.href; } // 如果命中白名單 if (thirdPageList.some(item => href.indexOf(item) === 0)) { if (isNewTab) { // _rawOpen參見后面 window.open 攔截 window._rawOpen(href); } else { // 第三方頁面如果不是新標(biāo)簽打開就一定是父頁面打開 window.parent.location.href = href; } return; } // 需要從當(dāng)前URL繼續(xù)往下透?jìng)鞯膮?shù) var params = ['hideLayout', 'tracelog']; for (var i = 0; i < params.length; i++) { var value = getParam(params[i], location.href); if (value) { href = setParam(params[i], value, href); } } if (isNewTab) { let entry = `/fin/base.html?entry=${encodeURIComponent(href)}`; const [path, search] = href.split('?'); if (iframeMap[path]) { entry = `${iframeMap[path]}?${search || ''}`; } href = `https://payment.alibaba.com${entry}`; window._rawOpen(href); return; } // 如果是以iframe方式嵌入,向父頁面發(fā)送通知 postMessage('beforeHistoryChange', { url: href }); return href; } 服務(wù)端跳轉(zhuǎn)攔截服務(wù)端主要是對(duì)301或302重定向跳轉(zhuǎn)進(jìn)行攔截,以Egg為例,只要重寫 A標(biāo)簽跳轉(zhuǎn)攔截document.addEventListener('click', function (e) { var target = e.target || {}; // A標(biāo)簽可能包含子元素,點(diǎn)擊目標(biāo)可能不是A標(biāo)簽本身,這里只簡(jiǎn)單判斷2層 if (target.tagName === 'A' || (target.parentNode && target.parentNode.tagName === 'A')) { target = target.tagName === 'A' ? target : target.parentNode; var href = target.href; // 不處理沒有配置href或者指向JS代碼的A標(biāo)簽 if (!href || href.indexOf('javascript') === 0) { return; } var newHref = beforeRedirect(href, target.target === '_blank'); // 沒有返回值一般是已經(jīng)處理了跳轉(zhuǎn),需要禁用當(dāng)前A標(biāo)簽的跳轉(zhuǎn) if (!newHref) { target.target = '_self'; target.href = 'javascript:;'; } else if (newHref !== href) { target.href = newHref; } } }, true); location.href攔截location.href攔截至今是一個(gè)困擾前端界的難題,這里只能采用一個(gè)折中的方法: // 由于 location.href 無法重寫,只能實(shí)現(xiàn)一個(gè) location2.href = '' if (Object.defineProperty) { window.location2 = {}; Object.defineProperty(window.location2, 'href', { get: function() { return location.href; }, set: function(href) { var newHref = beforeRedirect(href); if (newHref) { location.href = newHref; } }, }); } 因?yàn)槲覀?span style="font-weight: 700">不僅實(shí)現(xiàn)了location.href的寫,location.href的讀也一起實(shí)現(xiàn)了,所以可以放心大膽的進(jìn)行全局替換。找到對(duì)應(yīng)前端工程,首先全局搜索
window.open攔截var tempOpenName = '_rawOpen'; if (!window[tempOpenName]) { window[tempOpenName] = window.open; window.open = function(url, name, features) { url = beforeRedirect(url, true); if (url) { window[tempOpenName](url, name, features); } } } history.pushState攔截var tempName = '_rawPushState'; if (!window.history[tempName]) { window.history[tempName] = window.history.pushState; window.history.pushState = function(state, title, url) { url = beforeRedirect(url); if (url) { window.history[tempName](state, title, url); } } } history.replaceState攔截var tempName = '_rawReplaceState'; if (!window.history[tempName]) { window.history[tempName] = window.history.replaceState; window.history.replaceState = function(state, title, url) { url = beforeRedirect(url); if (url) { window.history[tempName](state, title, url); } } } 全局loading處理完成上述步驟后,基本上已經(jīng)看不出來是iframe了,但是跳轉(zhuǎn)的時(shí)候中間有短暫的白屏?xí)幸稽c(diǎn)頓挫感,體驗(yàn)不算很流暢,這時(shí)候可以給iframe加一個(gè)全局的loading,開始跳轉(zhuǎn)前顯示,頁面加載完再隱藏: B系統(tǒng): document.addEventListener('DOMContentLoaded', function (e) { postMessage('iframeDOMContentLoaded', { url: location.href }); }); A系統(tǒng): window.addEventListener('message', (e) => { const { data, type } = e.data || {}; // iframe 加載完畢 if (type === 'iframeDOMContentLoaded') { this.setState({loading: false}); } if (type === 'beforeHistoryChange') { // 此時(shí)頁面并沒有立即跳轉(zhuǎn),需要再稍微等待一下再顯示loading setTimeout(() => this.setState({loading: true}), 100); } }); 除此之外還需要利用iframe自帶的onload加一個(gè)兜底,防止iframe頁面沒有上報(bào) // iframe自帶的onload做兜底 iframeOnLoad = () => { this.setState({loading: false}); } render() { return <div> <Loading visible={this.state.loading} tip="正在加載..." inline={false}> <iframe id="microFrontIframe" src={this.state.currentEntry} onLoad={this.iframeOnLoad}/> </Loading> </div>; } 還需要注意,當(dāng)新標(biāo)簽頁打開頁面時(shí)并不需要顯示loading,需要注意區(qū)分。 彈窗居中問題當(dāng)前場(chǎng)景下彈窗個(gè)人覺得并不需要處理,因?yàn)椴藛蔚膶挾扔邢?,不仔?xì)看的話甚至都沒注意到彈窗沒有居中: 如果非要處理的話也不麻煩,覆蓋一下原來頁面彈窗的樣式,當(dāng)包含 添加了 最終效果其實(shí)不難看出,最終效果和SPA幾乎無異,而且菜單和導(dǎo)航本來就是無刷新的,頁面跳轉(zhuǎn)沒有割裂感 結(jié)語上述方案有幾個(gè)沒有提到的點(diǎn):
在第一次摸索方案時(shí)可能需要花費(fèi)一些時(shí)間,但是在熟悉之后,如果后續(xù)還有類似把B系統(tǒng)接入A系統(tǒng)的需求,在沒有特殊情況且順利的前提下可能花費(fèi)1-2天時(shí)間即可完成,最重要的是大部分工作都是全局生效的,不會(huì)隨著頁面的增多而導(dǎo)致工作量增加,測(cè)試回歸的成本也非常低,只需要驗(yàn)證所有頁面跳轉(zhuǎn)、展示等是否正常,功能本身一般不會(huì)有太大問題,而如果是微前端方案的話需要從頭到尾全部仔仔細(xì)細(xì)測(cè)試一遍,開發(fā)和測(cè)試的成本都不可估量。 ———————————————————— https://juejin.cn/post/7185070739064619068 該文章在 2023/5/30 10:34:46 編輯過 |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |