The core concept of micro frontends is to treat the frontend application as a whole, composed of multiple independent parts. Each part is considered a micro-frontend application, which can have its own technology stack, development process, and team organization. This approach allows teams to develop and deploy sub-applications independently, reducing the complexity of coordination and integration.
Why IFrames Are Not Suitable for Micro Frontends
IFrames, commonly used in traditional frontend development to embed other web pages or applications, are not ideal for micro frontends due to several limitations:
- Isolation and Communication Complexity: While IFrames provide an isolated environment, they complicate communication and data exchange. Each sub-application runs in its own IFrame, requiring specific mechanisms like message passing, which adds to the development and maintenance complexity.
- Performance and Load Time: Each IFrame needs to load and render its own HTML, CSS, and JavaScript. This results in multiple network requests and increased resource usage, potentially affecting performance and load time.
- Styling and Layout Limitations: The content within an IFrame has its own CSS styles and layout context, making it difficult to achieve consistent global styling and coordinate layout and interactions between sub-applications.
- Browser Security Restrictions: Cross-origin communication and resource access in IFrames can be restricted by security policies, leading to complex security issues in micro frontend architectures.
Given these limitations, micro frontend architectures typically use other technologies such as Web Components and JavaScript module loaders to achieve better isolation, communication, and performance.
Micro Frontend Operation Principles
- Monitor route changes
- Match sub-applications
- Load sub-applications
- Render sub-applications
Monitoring Route Changes
1. Monitor hash routes: window.onhashchange
2. Monitor history routes: window.addEventListener('popstate', () => {})
To intercept pushState and replaceState, you can override these methods:
const rawPushState = window.history.pushState;
window.history.pushState = function(...args) {
rawPushState.apply(window.history, args);
// Additional logic
}
const rawReplaceState = window.history.replaceState;
window.history.replaceState = function(...args) {
rawReplaceState.apply(window.history, args);
// Additional logic
}
Matching Sub-Applications
After monitoring route changes, get the current route path window.location.pathname and find the matching sub-application based on the apps parameter in registerMicroApps.
const currentApp = apps.find(app => window.location.pathname.startsWith(app.activeRule));
Loading Sub-Applications
Once a matching sub-application is found, load its resources.
async function handleRouter() {
const html = await fetch(currentApp.entry).then(res => res.text());
const container = document.querySelector(currentApp.container);
// Process the HTML
}
Note that directly setting container.innerHTML = html will not work because browsers do not execute inline scripts for security reasons. We need to handle the script tags manually.
Loading Resources and Handling Scripts
We can create a function importHTML to handle the HTML text and scripts.
export const importHTML = async (url) => {
const html = await fetch(url).then(res => res.text());
const template = document.createElement('div');
template.innerHTML = html;
const scripts = template.querySelectorAll('script');
const getExternalScripts = async () => {
return Promise.all(Array.from(scripts).map(async (script) => {
const src = script.getAttribute('src');
if (!src) {
return script.innerHTML;
} else {
return fetch(src.startsWith('http') ? src : `${url}${src}`).then(res => res.text());
}
}));
};
const execScripts = async () => {
const scripts = await getExternalScripts();
scripts.forEach(code => eval(code));
};
return {
template,
getExternalScripts,
execScripts,
};
};
qiankun
qiankun is built on top of single-spa and uses the import-html-entry package to handle HTML and CSS.
"dependencies": {
"import-html-entry": "^1.14.0",
"single-spa": "^5.9.2"
// ...
},
Integrating Sub-Applications into the Main Application
registerMicroApps([
{
name: 'react16',
entry: '//localhost:7100',
container: '#subapp-viewport',
loader,
activeRule: '/react16',
},
{
name: 'react15',
entry: '//localhost:7102',
container: '#subapp-viewport',
loader,
activeRule: '/react15',
},
]);
1. The container loads the sub-application based on the matched route. 2. The sub-application is packaged in UMD format and must allow cross-origin access.
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
UMD Format
UMD (Universal Module Definition) is a common module definition format used in frontend development. It supports CommonJS, AMD, and global definitions, making it versatile for different environments.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.qiankun = {}));
}(this, (function (exports) {
// Application code
})));
Why qiankun Requires UMD Libray Format?
This is to allow the main application to access the lifecycle hooks (bootstrap, mount, unmount) exported from the sub-application's entry file, which is crucial for communication between the main and sub-applications.
Handling Sub-Application Resources - import-html-entry
qiankun uses import-html-entry to load sub-applications, similar to how IFrames work but with more flexibility.
import importHTML from 'import-html-entry';
importHTML('./subApp/index.html')
.then(res => {
console.log(res.template);
res.execScripts().then(exports => {
const mobx = exports;
const { observable } = mobx;
observable({
name: 'kuitos'
});
});
});
JavaScript Sandbox
A JavaScript sandbox is a security mechanism that isolates and restricts the execution environment of JavaScript code to prevent malicious or unintended behavior.
SnapshotSandbox
SnapshotSandbox records and restores the state of the window object.
class SnapshotSandbox {
constructor() {
this.windowSnapshot = {};
this.modifyPropsMap = {};
}
active() {
for (let prop in window) {
if (window.hasOwnProperty(prop)) {
this.windowSnapshot[prop] = window[prop];
}
}
Object.keys(this.modifyPropsMap).forEach(prop => {
window[prop] = this.modifyPropsMap[prop];
});
}
inactive() {
for (let prop in window) {
if (window.hasOwnProperty(prop)) {
if (window[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = window[prop];
}
window[prop] = this.windowSnapshot[prop];
}
}
}
}
ProxySandbox
ProxySandbox uses a proxy to manage the window object without modifying it directly.
class ProxySandbox {
constructor() {
this.isRunning = false;
const fakeWindow = Object.create(null);
this.proxyWindow = new Proxy(fakeWindow, {
set(target, prop, value) {
if (this.isRunning) {
target[prop] = value;
return true;
}
},
get(target, prop) {
return prop in target ? target[prop] : window[prop];
}
});
}
active() {
this.isRunning = true;
}
inactive() {
this.isRunning = false;
}
}
qiankun Style Isolation
Without style isolation, styles in the main and sub-applications can conflict. qiankun provides two options for style isolation:
- Shadow DOM (strictStyleIsolation): Wraps each micro-application's container in a
shadow domnode to isolate styles. - Scoped CSS (experimentalStyleIsolation): Adds a unique selector to all styles to limit their scope.
start({
sanbox: {
strictStyleIsolation: true,
experimentalStyleIsolation: true
}
});
CSS Sandboxing Solutions
Several solutions exist for CSS sandboxing:
- BEM Naming Convention: Use specific class names and naming conventions to apply styles to specific elements.
- CSS Modules: Generate unique class names for each module to ensure styles are scoped.
- CSS-in-JS: Write CSS styles in JavaScript, binding them to components for local scoping.
- Shadow DOM: Create an isolated DOM subtree to encapsulate styles and scripts.
Monorepo Architecture
Monorepo architecture can help manage shared dependencies and components in micro frontends by centralizing them in a single repository. Tools like Yarn or Lerna can be used to manage and update these shared resources efficiently.