Author: Liu Xian’an (code monster)

Any new technology or new product has certain applicable scenarios. It may be very popular at the moment, but it may not be the optimal solution at any time.

In recent years, micro-frontends have been very popular, so that sometimes iframes are used in the project and hidden secretly for fear of being discovered by others, because they are worried about being questioned: Why don’t you use the micro-frontend solution? Until recently, the author took over a project and needed to embed an existing system into another system (a total of more than 20 pages). After being cheated by the micro-frontend several times, I looked back and found that iframe is really fragrant!

The author of qiankun has an article “Why Not Iframe” that introduces the advantages and disadvantages of iframes (but the author also has an article “You May Not Need Micro Frontends” to reduce the fire of micro frontends). It is true that iframes do have many shortcomings, but when choosing a When designing a solution, it is still necessary to analyze specific scenarios. It may be very popular at the moment, but it may not be the optimal solution at any time: Are these shortcomings of iframe acceptable to me? Is there any other way to make up for its shortcomings? Is using it more beneficial than harmful or more harmful than beneficial? We need to find a balance between advantages and disadvantages.

Suitable scenarios for iframe

Due to some limitations of iframe, some scenarios are not suitable for using iframe. For example, the iframe below only occupies the middle part of the page. Since the parent page already has a scroll bar, in order to avoid double scroll bars, the iframe can only be dynamically calculated. The content height is assigned to the iframe so that the height of the iframe is completely filled, but the problem brought about by this is that the pop-up window is difficult to handle. If it is centered, the pop-up window is generally relative to the iframe content height instead of the screen height, which may cause the pop-up window to be seen No, if the top of the pop-up window is fixed, the pop-up window will scroll along with the page, and if there is a slight deviation in the calculation of the height of the iframe content, double scroll bars will appear.

so:

  • If the page itself is relatively simple, and it is a pure information display page without pop-up windows, floating layers, and a fixed height, it is generally no problem to use iframe;
  • If the page contains pop-up windows, information prompts, or the height is not fixed, you need to seeDoes the iframe occupy the entire content areaif it is a classic navigation + menu + content structure like the picture below, and the entire content area is iframe, then you can try iframe with confidence, otherwise, you need to carefully consider the selection of the scheme.

Why must the condition “iframe occupies the entire content area” be met? You can imagine the following scenario, the scroll bar appearing in the middle of the page should be unacceptable to most people:

In the scenario where the condition of “iframe occupies the entire content area” is met, several shortcomings of iframe are relatively easy to solve. The following uses a practical case to introduce in detail the whole process of connecting an online running system to another system. Take the ACP (full name Alibaba.com Pay, a one-stop global collection platform under Alibaba International Station, hereinafter referred to as System A) that the author just completed some time ago to connect to Business Loan (hereinafter referred to as System B) as an example, it is known that:

  • Both ACP and business loan are MPA pages;
  • There is no precedent for the ACP system to be connected to other systems before, and the business loan is the first;
  • Business Loan is an access system. There are more than 20 pages that need to be accessed this time, and the server contains a lot of business logic and jump control. It is very difficult to see what some pages look like. It needs to be mocked at the Node layer. a large number of interfaces;
  • Functions need to be deleted when accessing, and some interface parameters need to be adjusted;
  • In addition to being connected to the ACP system, the business loan has also been connected to the AMES system before, and this connection needs to be compatible with this part of the historical logic;

The effect we want:

Suppose we add a new page /fin/base.html?entry=xxx As the address of our A system to undertake the B system, the A system has a code similar to the following:

class App extends React.Component {
    state = {
        currentEntry: decodeURIComponent(iutil.getParam('entry') || '') || '',
    };
    render() {
        return <div>
            <iframe id="microFrontIframe" src={this.state.currentEntry}/>
        </div>;
    }
}

Hide the original system navigation menu

Because it is connected to another system, it is necessary to hide the menu and navigation of the original system through a parameter similar to “hideLayout”.

Forward and backward processing

What needs special attention is that although the jump inside the iframe page will not change the address bar of the browser, it will generate an invisible “history record”, that is, click the forward or back button (history.forward()orhistory.back()) can make the iframe page move forward and backward, but there is no change in the address bar.

So to be precise, we don’t need to do any processing to move forward and backward. All we have to do is to let the browser address bar update synchronously.

If you want to disable the above default behavior of the browser, generally you can only notify the parent page to update the entire iframe when the iframe jumps.<iframe />DOMnode.

Synchronous update of URL

Two issues need to be dealt with to make the URL update synchronously, one is when to trigger the update action, and the other is the law of URL update, that is, the maintenance of the mapping relationship between the URL address of the parent page (system A) and the URL address of the iframe (system B) .

To ensure that the URL synchronization update function works normally, the following three conditions need to be met:

  • case1: The page is refreshed, and the iframe can load the correct page;
  • case2: The page jumps, and the browser address bar can be updated correctly;
  • case3: Click forward or back of the browser, the address bar and iframe can change synchronously;

When to update the URL address

The first thing that comes to mind must be to send a notification to the parent page after the iframe is loaded, and the parent page passeshistory.replaceStateto update the URL.

Why nothistory.pushStateWoolen cloth? As mentioned earlier, the browser will generate a history record by default, we only need to update the address, and if pushState is used, 2 records will be generated.

System B:

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

System A:

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);
        }
    }
});

Optimize URL update speed

After implementing the method above, you can find that although the URL can be updated, the speed is a bit slow. After clicking to jump, it usually takes 7-800 milliseconds to update the address bar, which is a bit of a fly in the ointment. You can add a “before jump” to the update of the address bar on the basis of “after jump”. For this we must have a global beforeRedirect hook, regardless of its specific implementation:

System B:

function beforeRedirect(href) {
    postMessage('beforeHistoryChange', { url: href });
}

System A:

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);
        }
    }
});

After adding the above code, click the jump link in the iframe, the URL will be updated in real time, and the forward and backward functions of the browser are also normal.

Why do you need to keep both before and after the jump? Because if you only keep before the jump, you can only satisfy the previous case1 and case2, and case3 cannot be satisfied, that is, only the iframe will go back when you click the back button, and the URL address will not be updated.

Beautify the URL address

easy to use/fin/base.html?entry=xxxAlthough such a general address can be used, it is not very beautiful, and it is easy to be seen that it is implemented by an iframe, which is not sincere. Therefore, if the number of pages connected to the system is within the enumerable range, it is recommended to give each address Maintain a new short address.

First, add a new SPA page/fin/*.htmland the previous/fin/base.htmlPoint to the same page, and then maintain a mapping of URL addresses, like this:

// A系统地址到B系统地址映射
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>;
    }
}

At the same time, improve the update URL address part:

// base.html继续用作兜底
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);
}

Omit the parameter transparent transmission part code.

Global jump interception

Why do we have to do global jump interception? One is because we need to pass the hideLayout parameter transparently, otherwise the following double menu will suddenly appear:

The other is that some pages are opened on the current page before being embedded, but cannot continue to be opened in the current iframe after being embedded, such as third-party pages such as Alipay payment. Would it be strange to imagine the following situation? Therefore, this type of page must be specially processed to make it jump out instead of opening the current page.

URL jumps can be divided into server jumps and browser jumps, and browser jumps include A tag jumps, location.href jumps, window.open jumps, historyAPI jumps, etc.;

According to whether a new tab is opened, it can be divided into the following four scenarios:

  1. To continue to open the current iframe, all layouts of the original system need to be hidden;
  2. The current parent page opens a third-party page without any layout;
  3. Open a new tab to open a third-party page (such as Alipay page), no special processing is required;
  4. Open a new tab to open the host page, you need to replace the original system layout with the new layout;

To do this, first define abeforeRedirectmethod, since a new tab opens withtarget="_blank"andwindow.openand so on, the parent page opens withtarget="_parent"andwindow.parent.location.hrefIn other ways, in order to better unify the package, we unify the jumps in special cases inbeforeRedirectHandle it well, and agree that only when there is a return value, you need to continue to process the jump:

// 维护一个需要做特殊处理的第三方页面列表
const thirdPageList = [
    'https://service.alibaba.com/',
    'https://sale.alibaba.com/xxx/',
    'https://alipay.com/xxx/',
    // ...
];
/**
 * 封装统一的跳转拦截钩子,处理参数透传和一些特殊情况
 * @param {*} href 要跳转的地址,允许传入相对路径
 * @param {*} isNewTab 是否要新标签打开
 * @param {*} isParentOpen 是否要在父页面打开
 * @returns 返回处理好的跳转地址,如果没有返回值则表示不需要继续处理跳转
 */
function beforeRedirect(href, isNewTab) {
    if (!href) {
        return;
    }
    // 传过来的href可能是相对路径,为了做统一判断需要转成绝对路径
    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;
    }
    // 需要从当前URL继续往下透传的参数
    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方式嵌入,向父页面发送通知
    postMessage('beforeHistoryChange', { url: href });
    return href;
}

Server jump interception

The server mainly intercepts 301 or 302 redirection jumps. Taking Egg as an example, just rewrite ctx.redirect method.

A label jump interception

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');
        // 没有返回值一般是已经处理了跳转,需要禁用当前A标签的跳转
        if (!newHref) {
            target.target="_self";
            target.href="https://my.oschina.net/alimobile/blog/javascript:;";
        } else if (newHref !== href) {
            target.href = newHref;
        }
    }
}, true);

location.href interception

The interception of location.href has been a problem that plagues the front-end community so far. There can only be a compromise method here:

// 由于 location.href 无法重写,只能实现一个 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;
            }
        },
    });
}

because of U.SNot only the writing of location.href is realized, but also the reading of location.href is realized together, so you can safely and boldly perform global replacement.To find the corresponding front-end project, first search globallywindow.location.hrefbatch replaced with(window.location2 || window.location).hrefand then global searchlocation.hrefbatch replaced with(window.location2 || window.location).href(Think about why it must be in this order).

In addition, it should be noted that some jumps may be written in the npm package. In this case, npm can only replace it, and there is no other better way.

window.open interception

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 interception

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 interception

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);
        }
    }
}

Global loading processing

After completing the above steps, it is basically impossible to see that it is an iframe, but there will be a short white screen in the middle of the jump, which will cause a little frustration, and the experience is not very smooth. At this time, you can add a global loading to the iframe and start Show before jumping, hide after page load:

System B:

document.addEventListener('DOMContentLoaded', function (e) {
    postMessage('iframeDOMContentLoaded', { url: location.href });
});

System A:

window.addEventListener('message', (e) => {
    const { data, type } = e.data || {};
    // iframe 加载完毕
    if (type === 'iframeDOMContentLoaded') {
        this.setState({loading: false});
    }
    if (type === 'beforeHistoryChange') {
        // 此时页面并没有立即跳转,需要再稍微等待一下再显示loading
        setTimeout(() => this.setState({loading: true}), 100);
    }
});

In addition, you need to use the onload that comes with the iframe to add a bottom line to prevent the iframe page from being reported. iframeDOMContentLoaded The event causes loading not to disappear:

// 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>;
}

It should also be noted that loading does not need to be displayed when the page is opened in a new tab, and it needs to be distinguished.

Pop-up window centering problem

In the current scene, I personally think that the pop-up window does not need to be processed, because the width of the menu is limited. If you don’t look carefully, you don’t even notice that the pop-up window is not centered:

If you have to deal with it, it is not troublesome. Overwrite the style of the original page pop-up window. When it containshideLayoutWhen parameterizing, let the position of the pop-up window move to the left respectivelymenuWidth/2,Move upnavbarHeight/2That’s it (the position of the mask cannot be moved, nor can it be moved).

AddedmarginLeft=-120px,marginTop=-30px The final pop-up window effect:

final effect

In fact, it is not difficult to see that the final effect is almost the same as that of SPA, and the menu and navigation are inherently non-refreshing, and there is no sense of fragmentation in page jumps:

There are several points not mentioned in the above scheme:

  • The premise of the solution is that the two systems share a set of user systems. Otherwise, the login systems of the two systems need to be opened up, which generally includes account binding, system A’s default login to system B, etc., which requires a certain amount of extra effort. workload;
  • Transparent transmission and deletion of parameters, for example, I hope that all URL parameters except the hideLayout parameter can be transparently transmitted between parent and child pages;
  • Buried point, when data is reported, an additional parameter needs to be added to identify that the traffic comes from another system;

It may take some time to explore the solution for the first time, but after getting familiar with it, if there is a need to connect system B to system A in the future, it may take 1-2 days if there are no special circumstances and everything goes smoothly. It can be completed. The most important thing is that most of the work takes effect globally. The workload will not increase as the number of pages increases. The cost of testing regression is also very low. You only need to verify that all page jumps and displays are normal. The function itself is generally not a big problem, but if it is a micro front-end solution, it needs to be carefully tested from beginning to end, and the cost of development and testing is immeasurable.

#Rectify #iframe #micro #frontend #Alibaba #Terminal #Technology #News Fast Delivery

Leave a Comment

Your email address will not be published. Required fields are marked *