When Iframe Beats Micro‑Frontend: A Practical Guide to Seamless System Integration

This article examines the advantages and drawbacks of using iframes for embedding one system into another, outlines suitable scenarios, and provides a step‑by‑step implementation—including URL synchronization, navigation handling, and loading management—using React and plain JavaScript to achieve a smooth, micro‑frontend‑free integration.

Alibaba Terminal Technology
Alibaba Terminal Technology
Alibaba Terminal Technology
When Iframe Beats Micro‑Frontend: A Practical Guide to Seamless System Integration

Preface

In recent years micro‑frontends have become popular, sometimes leading teams to hide iframes to avoid criticism. After several pitfalls with micro‑frontends, the author discovered that an iframe can be a simple and effective solution for embedding an entire system (over 20 pages) into another.

The author of qiankun wrote "Why Not Iframe" and "You Might Not Need Micro‑Frontend", highlighting both pros and cons of iframes. Choosing a solution should be based on concrete scenarios rather than trends.

Pros and Cons Analysis

Suitable Scenarios for iframe

Because of iframe limitations, it is unsuitable when the iframe occupies only a middle portion of the page and the parent already has a scrollbar, leading to double scrollbars and difficult modal handling. If the page is simple, static, and without pop‑ups, an iframe works fine. If the page contains pop‑ups, dynamic height, or only part of the layout, careful analysis is needed to ensure the iframe occupies the entire content area.

If the page is a simple information display without pop‑ups or dynamic height, using an iframe is generally safe.

If the page includes pop‑ups or variable height, verify that the iframe covers the full content region (e.g., classic navigation + menu + content structure) before proceeding.

Practical Example: Embedding System A into System B

The author describes integrating Alibaba.com Pay (System A) into Business Loan (System B), which involves more than 20 pages, extensive server‑side logic, and the need to mock many interfaces.

Both systems are MPA pages.

System A had no prior integrations; System B is the first.

System B’s pages require extensive backend logic and redirects, making direct preview difficult.

Some functionality must be removed, and certain API parameters adjusted.

System B previously integrated with another system, so legacy logic must be preserved.

Desired effect: embed System B seamlessly within System A.

Assume a new page /fin/base.html?entry=xxx serves as the entry point. System A includes the following React component:

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

Hiding Original System Navigation

The parent system’s navigation and layout are hidden via a "hideLayout" parameter.

Forward/Back Navigation Handling

Iframe internal navigation creates a hidden history record. The browser’s forward/back buttons affect the iframe history, but the address bar remains unchanged. Therefore, the address bar must be synchronized with iframe navigation.

If you need to disable the browser’s default behavior, you must notify the parent page to update the &lt;iframe /&gt; DOM node.

URL Synchronization

Two problems must be solved: when to trigger updates and how to map parent and iframe URLs. The solution must handle three cases:

Page refresh – iframe loads the correct page.

Page navigation – address bar updates correctly.

Browser forward/back – both address bar and iframe stay in sync.

When to Update the URL

After the iframe loads, it sends a message to the parent, which calls history.replaceState to update the URL.

Why not use history.pushState ? Because pushState would create an extra history entry.

Implementation in System B

var postMessage = function(type, data) {
  if (window.parent !== window) {
    window.parent.postMessage({ type: type, data: data }, '*');
  }
};
postMessage('afterHistoryChange', { url: location.href });

Implementation in System A

window.addEventListener('message', e => {
  const { data, type } = e.data || {};
  if (type === 'afterHistoryChange' && data?.url) {
    const entry = `/fin/base.html?entry=${encodeURIComponent(data.url)}`;
    if (location.pathname + location.search !== entry) {
      window.history.replaceState(null, '', entry);
    }
  }
});

Optimizing URL Update Speed

To reduce the 700‑800 ms delay, a "beforeRedirect" hook is added to send a notification before the iframe navigation.

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

The parent listens for both "beforeHistoryChange" and "afterHistoryChange" to update the URL promptly.

Beautifying URLs

Instead of using generic /fin/base.html?entry=xxx, short, meaningful URLs are mapped to each target page, e.g., /fin/home.htmlhttps://fs.alibaba.com/.../home.htm?hideLayout=1. A mapping object is maintained and used to translate URLs.

// Mapping from parent to child URLs
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',
  // ...
};
const iframeMap = {};
for (const entry in entryMap) {
  iframeMap[entryMap[entry].split('?')[0]] = entry;
}

Global Navigation Interception

To ensure "hideLayout" is always passed, a global beforeRedirect function intercepts all navigation types (A tags, location.href, window.open, history API) and injects required parameters or handles special third‑party pages.

function beforeRedirect(href, isNewTab) {
  if (!href) return;
  if (!href.startsWith('http')) {
    const a = document.createElement('a');
    a.href = href;
    href = a.href;
  }
  // Handle whitelisted third‑party pages
  if (thirdPageList.some(item => href.indexOf(item) === 0)) {
    if (isNewTab) window._rawOpen(href);
    else window.parent.location.href = href;
    return;
  }
  // Propagate required params
  const params = ['hideLayout', 'tracelog'];
  params.forEach(p => {
    const v = getParam(p, location.href);
    if (v) href = setParam(p, v, href);
  });
  if (isNewTab) {
    // Open in new tab with mapped entry
    let entry = `/fin/base.html?entry=${encodeURIComponent(href)}`;
    const [path, search] = href.split('?');
    if (iframeMap[path]) entry = `${iframeMap[path]}?${search || ''}`;
    window._rawOpen(`https://payment.alibaba.com${entry}`);
    return;
  }
  postMessage('beforeHistoryChange', { url: href });
  return href;
}

Intercepting Various Navigation APIs

Examples include overriding document.addEventListener('click') for A‑tag clicks, redefining location.href, wrapping window.open, and monkey‑patching history.pushState and history.replaceState to call beforeRedirect before proceeding.

document.addEventListener('click', function(e) {
  var target = e.target || {};
  if (target.tagName === 'A' || (target.parentNode && target.parentNode.tagName === 'A')) {
    target = target.tagName === 'A' ? target : target.parentNode;
    var href = target.href;
    if (!href || href.indexOf('javascript') === 0) return;
    var newHref = beforeRedirect(href, target.target === '_blank');
    if (!newHref) {
      target.target = '_self';
      target.href = 'javascript:;';
    } else if (newHref !== href) {
      target.href = newHref;
    }
  }
}, true);

Global Loading Handling

To avoid a white‑screen flash during navigation, a loading indicator is shown before the iframe loads and hidden after the iframe dispatches an "iframeDOMContentLoaded" message.

// In System B
document.addEventListener('DOMContentLoaded', function() {
  postMessage('iframeDOMContentLoaded', { url: location.href });
});
// In System A
window.addEventListener('message', e => {
  const { data, type } = e.data || {};
  if (type === 'iframeDOMContentLoaded') {
    this.setState({ loading: false });
  }
  if (type === 'beforeHistoryChange') {
    setTimeout(() => this.setState({ loading: true }), 100);
  }
});

The iframe also uses its native onload event as a fallback to hide the loading spinner.

iframeOnLoad = () => {
  this.setState({ loading: false });
};
render() {
  return <div>
    <Loading visible={this.state.loading} tip="Loading..." inline={false}>
      <iframe id="microFrontIframe" src={this.state.currentEntry} onLoad={this.iframeOnLoad} />
    </Loading>
  </div>;
}

Modal Centering Issue

In the described layout, modal centering is not critical; however, if needed, margins can be adjusted based on the hidden layout parameters.

Conclusion

The presented approach, while requiring initial effort to set up URL mapping, parameter propagation, and loading handling, results in an integration that feels like a single‑page application. Once the pattern is understood, similar system‑to‑system embeddings can be completed in 1‑2 days with minimal regression testing, offering a lower cost alternative to full micro‑frontend implementations.

References

Why Not Iframe: https://www.yuque.com/kuitos/gky7yw/gesexv?spm=ata.21736010.0.0.25c06df01VID5V

You Might Not Need Micro‑Frontend: https://zhuanlan.zhihu.com/p/391248835

frontendmicro-frontendSystem Integrationiframeurl synchronization
Alibaba Terminal Technology
Written by

Alibaba Terminal Technology

Official public account of Alibaba Terminal

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.