亚洲乱色熟女一区二区三区丝袜,天堂√中文最新版在线,亚洲精品乱码久久久久久蜜桃图片,香蕉久久久久久av成人,欧美丰满熟妇bbb久久久

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

為iframe正名,你可能并不需要微前端

freeflydom
2023年5月30日 10:10 本文熱度 1603

阿里巴巴終端技術(shù) 2023年01月05日

作者:劉顯安(碼怪)

任何新技術(shù)、新產(chǎn)品都是有一定適用場景的,它可能在當(dāng)下很流行,但它不一定在任何時候都是最優(yōu)解。

前言

最近幾年微前端很火,火到有時候項目里面用到了iframe還要偷偷摸摸地藏起來生怕被別人知道了,因為擔(dān)心被人質(zhì)疑:你為什么不用微前端方案?直到最近筆者接手一個項目,需要將現(xiàn)有的一個系統(tǒng)整體嵌入到另外一個系統(tǒng)(一共20多個頁面),在被微前端坑了幾次之后,回過頭發(fā)現(xiàn),iframe真香!

qiankun的作者有一篇《Why Not Iframe》 介紹了iframe的優(yōu)缺點(不過作者還有一篇《你可能并不需要微前端》給微前端降降火),誠然iframe確實存在很多缺點,但是在選擇一個方案的時候還是要具體場景具體分析,它可能在當(dāng)下很流行,但它不一定在任何時候都是最優(yōu)解:iframe的這些缺點對我來說是否能夠接受?它的缺點是否有其它方法可以彌補?使用它到底是利大于弊還是弊大于利?我們需要在優(yōu)缺點之間找到一個平衡。

優(yōu)缺點分析

iframe適合的場景

由于iframe的一些限制,部分場景并不適合用iframe,比如像下面這種iframe只占據(jù)頁面中間部分區(qū)域,由于父頁面已經(jīng)有一個滾動條了,為了避免出現(xiàn)雙滾動條,只能動態(tài)計算iframe的內(nèi)容高度賦值給iframe,使得iframe高度完全撐滿,但這樣帶來的問題是彈窗很難處理,如果居中的話一般彈窗都相對的是iframe內(nèi)容高度而不是屏幕高度,從而導(dǎo)致彈窗可能看不見,如果固定彈窗top又會導(dǎo)致彈窗跟隨頁面滾動,而且稍有不慎iframe內(nèi)容高度計算有一點點偏差就會出現(xiàn)雙滾動條。

所以:

  • 如果頁面本身比較簡單,是一個沒有彈窗、浮層、高度也是固定的純信息展示頁的話,用iframe一般沒什么問題;

  • 如果頁面是包含彈窗、信息提示、或者高度不是固定的話,需要看iframe是否占據(jù)了全部的內(nèi)容區(qū)域,如果是像下圖這種經(jīng)典的導(dǎo)航+菜單+內(nèi)容結(jié)構(gòu)、并且整個內(nèi)容區(qū)域都是iframe,那么可以放心大膽地嘗試iframe,否則,需要慎重考慮方案選型。

為什么一定要滿足“iframe占據(jù)全部內(nèi)容區(qū)域”這個條件呢?可以想象一下下面這種場景,滾動條出現(xiàn)在頁面中間應(yīng)該大部分人都無法接受:

實戰(zhàn):A系統(tǒng)接入B系統(tǒng)

滿足“iframe占據(jù)全部內(nèi)容區(qū)域”條件的場景,iframe的幾個缺點都比較好解決。下面通過一個實際案例來詳細介紹將一個線上在運行的系統(tǒng)接入到另外一個系統(tǒng)的全過程。以筆者前段時間剛完成的ACP(全稱Alibaba.com Pay,阿里巴巴國際站旗下一站式全球收款平臺,下稱A系統(tǒng))接入生意貸(下稱B系統(tǒng))為例,已知:

  • ACP和生意貸都是MPA頁面;

  • ACP系統(tǒng)在此之前沒有接入其他系統(tǒng)的先例,生意貸是第一個;

  • 生意貸作為被接入系統(tǒng),本次需要接入的一共有20多個頁面,且服務(wù)端包含大量業(yè)務(wù)邏輯以及跳轉(zhuǎn)控制,有些頁面想看看長什么樣子都非常困難,需要在Node層mock大量接口;

  • 接入時需要做功能刪減,部分接口入?yún)⑿枰{(diào)整;

  • 生意貸除了接入到ACP系統(tǒng)中,之前還接入過AMES系統(tǒng),本次接入需要兼容這部分歷史邏輯;

我們希望的效果:

假設(shè)我們新增一個頁面 /fin/base.html?entry=xxx 作為我們A系統(tǒng)承接B系統(tǒng)的地址,A系統(tǒng)有類似如下代碼:

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)航菜單

因為是接入到另外一個系統(tǒng),所以需要將原系統(tǒng)的菜單和導(dǎo)航等都通過一個類似“hideLayout”的參數(shù)去隱藏。

前進后退處理

需要特別注意的是,iframe頁面內(nèi)部的跳轉(zhuǎn)雖然不會讓瀏覽器地址欄發(fā)生變化,但是卻會產(chǎn)生一個看不見的“history記錄”,也就是點擊前進或后退按鈕(history.forward()history.back())可以讓iframe頁面也前進后退,但是地址欄無任何變化。

所以準確來說前進后退無需我們做任何處理,我們要做的就是讓瀏覽器地址欄同步更新即可。

如果要禁用瀏覽器的上述默認行為,一般只能在iframe跳轉(zhuǎn)時通知父頁面更新整個<iframe />DOM節(jié)點。

URL的同步更新

讓URL同步更新需要處理2個問題,一個是什么時候去觸發(fā)更新的動作,一個是URL更新的規(guī)律,即父頁面的URL地址(A系統(tǒng))與iframe的URL地址(B系統(tǒng))映射關(guān)系的維護。

保證URL同步更新功能正常需要滿足這3種情況:

  • case1: 頁面刷新,iframe能夠加載正確頁面;

  • case2: 頁面跳轉(zhuǎn),瀏覽器地址欄能夠正確更新;

  • case3: 點擊瀏覽器的前進或后退,地址欄和iframe都能夠同步變化;

什么時候更新URL地址

首先想到的肯定是在iframe加載完發(fā)送一個通知給父頁面,父頁面通過history.replaceState去更新URL。

為什么不是history.pushState呢?因為前面提到過,瀏覽器默認會產(chǎn)生一條歷史記錄,我們只需要更新地址即可,如果用pushState會產(chǎn)生2條記錄。

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) {

        // 這里先采用一個兜底的URL承接任意地址

        const entry = `/fin/base.html?entry=${encodeURIComponent(data.url)}`;

        // 地址不一樣才需要更新

        if (location.pathname + location.search !== entry) {

            window.history.replaceState(null, '', entry);

        }

    }

});


優(yōu)化URL的更新速度

按照上面的方法實現(xiàn)后可以發(fā)現(xiàn),URL雖然可以更新但是速度有點慢,點擊跳轉(zhuǎn)后一般需要等待7-800毫秒地址欄才會更新,有點美中不足。可以把地址欄的更新在“跳轉(zhuǎn)后”基礎(chǔ)之上再加一個“跳轉(zhuǎn)前”。為此我們必須有一個全局的beforeRedirect鉤子,先不考慮它的具體實現(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) {

        // 這里先采用一個兜底的URL承接任意地址

        const entry = `/fin/base.html?entry=${encodeURIComponent(data.url)}`;

        // 地址不一樣才需要更新

        if (location.pathname + location.search !== entry) {

            window.history.replaceState(null, '', entry);

        }

    }

});

加上上述代碼之后,點擊iframe中的跳轉(zhuǎn)鏈接,URL會實時更新,瀏覽器的前進后退功能也正常。

為什么需要同時保留跳轉(zhuǎn)前和跳轉(zhuǎn)后呢?因為如果只保留跳轉(zhuǎn)前,只能滿足前面的case1和case2,case3無法滿足,也就是點擊后退按鈕只有iframe會后退,URL地址不會更新。

美化URL地址

簡單的使用/fin/base.html?entry=xxx這樣的通用地址雖然能用,但是不太美觀,而且很容易被人看出來是iframe實現(xiàn)的,比較沒有誠意,所以如果被接入系統(tǒng)的頁面數(shù)量在可枚舉范圍內(nèi),建議給每個地址維護一個新的短地址。

首先,新增一個SPA頁面/fin/*.html,和前面的/fin/base.html指向同一個頁面,然后維護一個URL地址的映射,類似這樣:

// 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 = {}; // 同時再維護一個子頁面 -> 父頁面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>;

    }

}

同時完善一下更新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);

}

省略參數(shù)透傳部分代碼。

全局跳轉(zhuǎn)攔截

為什么一定要做全局跳轉(zhuǎn)攔截呢?一個因為我們需要把hideLayout參數(shù)一直透傳下去,否則就會點著點著突然出現(xiàn)下面這種雙菜單的情況:

另一個是有些頁面在被嵌入前是當(dāng)前頁面打開的,但是被嵌入后不能繼續(xù)在當(dāng)前iframe打開,比如支付寶付款這種第三方頁面,想象一下下面這種情況會不會覺得很怪?所以這類頁面一定要做特殊處理讓它跳出去而不是當(dāng)前頁面打開。

URL跳轉(zhuǎn)可以分為服務(wù)端跳轉(zhuǎn)和瀏覽器跳轉(zhuǎn),瀏覽器跳轉(zhuǎn)又包括A標簽跳轉(zhuǎn)、location.href跳轉(zhuǎn)、window.open跳轉(zhuǎn)、historyAPI跳轉(zhuǎn)等;

而根據(jù)是否新標簽打開又可以分為以下4種場景:

  1. 繼續(xù)當(dāng)前iframe打開,需要隱藏原系統(tǒng)的所有l(wèi)ayout;

  2. 當(dāng)前父頁面打開第三方頁面,不需要任何layout;

  3. 新開標簽打開第三方頁面(如支付寶頁面),不需要做特殊處理;

  4. 新開標簽打開宿主頁面,需要把原系統(tǒng)layout替換成新layout;

為此,先定義好一個beforeRedirect方法,由于新標簽打開有target="_blank"window.open等方式,父頁面打開有target="_parent"window.parent.location.href等方式,為了更好的統(tǒng)一封裝,我們把特殊情況的跳轉(zhuǎn)統(tǒng)一在beforeRedirect處理好,并約定只有有返回值的情況才需要后續(xù)繼續(xù)處理跳轉(zhuǎn):

// 維護一個需要做特殊處理的第三方頁面列表

const thirdPageList = [

    'https://service.alibaba.com/',

    'https://sale.alibaba.com/xxx/',

    'https://alipay.com/xxx/',

    // ...

];

/**

 * 封裝統(tǒng)一的跳轉(zhuǎn)攔截鉤子,處理參數(shù)透傳和一些特殊情況

 * @param {*} href 要跳轉(zhuǎn)的地址,允許傳入相對路徑

 * @param {*} isNewTab 是否要新標簽打開

 * @param {*} isParentOpen 是否要在父頁面打開

 * @returns 返回處理好的跳轉(zhuǎn)地址,如果沒有返回值則表示不需要繼續(xù)處理跳轉(zhuǎn)

 */

function beforeRedirect(href, isNewTab) {

    if (!href) {

        return;

    }

    // 傳過來的href可能是相對路徑,為了做統(tǒng)一判斷需要轉(zhuǎn)成絕對路徑

    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 {

            // 第三方頁面如果不是新標簽打開就一定是父頁面打開

            window.parent.location.href = href;

        }

        return;

    }

    // 需要從當(dāng)前URL繼續(xù)往下透傳的參數(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ù)端主要是對301或302重定向跳轉(zhuǎn)進行攔截,以Egg為例,只要重寫 ctx.redirect 方法即可。

A標簽跳轉(zhuǎn)攔截

document.addEventListener('click', function (e) {

    var target = e.target || {};

    // A標簽可能包含子元素,點擊目標可能不是A標簽本身,這里只簡單判斷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標簽

        if (!href || href.indexOf('javascript') === 0) {

            return;

        }

        var newHref = beforeRedirect(href, target.target === '_blank');

        // 沒有返回值一般是已經(jīng)處理了跳轉(zhuǎn),需要禁用當(dāng)前A標簽的跳轉(zhuǎn)

        if (!newHref) {

            target.target = '_self';

            target.href = 'javascript:;';

        } else if (newHref !== href) {

            target.href = newHref;

        }

    }

}, true);

location.href攔截

location.href攔截至今是一個困擾前端界的難題,這里只能采用一個折中的方法:

// 由于 location.href 無法重寫,只能實現(xiàn)一個 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;

            }

        },

    });

}

因為我們不僅實現(xiàn)了location.href的寫,location.href的讀也一起實現(xiàn)了,所以可以放心大膽的進行全局替換。找到對應(yīng)前端工程,首先全局搜索window.location.href,批量替換成(window.location2 || window.location).href,然后再全局搜索location.href,批量替換成(window.location2 || window.location).href(思考一下為什么一定是這個順序呢)。

另外需要注意,有些跳轉(zhuǎn)可能是寫在npm包里面的,這種情況只能npm也跟著替換一下了,并沒有其它更好辦法。

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)的時候中間有短暫的白屏?xí)幸稽c頓挫感,體驗不算很流暢,這時候可以給iframe加一個全局的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') {

        // 此時頁面并沒有立即跳轉(zhuǎn),需要再稍微等待一下再顯示loading

        setTimeout(() => this.setState({loading: true}), 100);

    }

});

除此之外還需要利用iframe自帶的onload加一個兜底,防止iframe頁面沒有上報 iframeDOMContentLoaded 事件導(dǎo)致loading不消失:

// 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)新標簽頁打開頁面時并不需要顯示loading,需要注意區(qū)分。

彈窗居中問題

當(dāng)前場景下彈窗個人覺得并不需要處理,因為菜單的寬度有限,不仔細看的話甚至都沒注意到彈窗沒有居中:

如果非要處理的話也不麻煩,覆蓋一下原來頁面彈窗的樣式,當(dāng)包含hideLayout參數(shù)時,讓彈窗的位置分別向左移動menuWidth/2、向上移動navbarHeight/2即可(遮罩位置不能動、也動不了)。

添加了marginLeft=-120px、marginTop=-30px 后的彈窗效果:

最終效果

其實不難看出,最終效果和SPA幾乎無異,而且菜單和導(dǎo)航本來就是無刷新的,頁面跳轉(zhuǎn)沒有割裂感

查看效果

結(jié)語

上述方案有幾個沒有提到的點:

  • 方案成立的前提是建立在2個系統(tǒng)共用一套用戶體系,否則需要對2個系統(tǒng)的登錄體系進行打通,一般包括賬號綁定、A系統(tǒng)默認免登B系統(tǒng),等等,這需要一定額外的工作量;

  • 參數(shù)的透傳與刪除,例如我希望除了hideLayout參數(shù)之外其它URL參數(shù)全部在父子頁面之間透傳;

  • 埋點,數(shù)據(jù)上報的時候需要增加一個額外參數(shù)來標識流量來自另外一個系統(tǒng);

在第一次摸索方案時可能需要花費一些時間,但是在熟悉之后,如果后續(xù)還有類似把B系統(tǒng)接入A系統(tǒng)的需求,在沒有特殊情況且順利的前提下可能花費1-2天時間即可完成,最重要的是大部分工作都是全局生效的,不會隨著頁面的增多而導(dǎo)致工作量增加,測試回歸的成本也非常低,只需要驗證所有頁面跳轉(zhuǎn)、展示等是否正常,功能本身一般不會有太大問題,而如果是微前端方案的話需要從頭到尾全部仔仔細細測試一遍,開發(fā)和測試的成本都不可估量。



————————————————————

https://juejin.cn/post/7185070739064619068


該文章在 2023/5/30 10:34:46 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點晴ERP是一款針對中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點晴PMS碼頭管理系統(tǒng)主要針對港口碼頭集裝箱與散貨日常運作、調(diào)度、堆場、車隊、財務(wù)費用、相關(guān)報表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點,圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點晴WMS倉儲管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務(wù)都免費,不限功能、不限時間、不限用戶的免費OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved