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.
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 <iframe /> 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.html → https://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
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
