Browser Module Systems
ES Modules
ES Modules (ECMAScript Modules) is JavaScript’s officially standardized module system, introduced with ES6 to address the long-standing lack of native module support in JavaScript. ES Modules provide a static structure, allowing dependencies to be determined at compile time, which enables optimization by browsers and build tools.
Basic Syntax
ES Modules use the import and export keywords to import and export module members:
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// app.js
import { add, subtract } from './math.js';
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
ES Modules support both named and default exports:
// math.js
export default function multiply(a, b) {
return a * b;
}
// app.js
import multiply from './math.js';
console.log(multiply(2, 3)); // 6
Dynamic Imports
ES Modules also support dynamic imports, allowing modules to be loaded on demand at runtime:
// app.js
button.addEventListener('click', async () => {
const { default: multiply } = await import('./math.js');
console.log(multiply(2, 3)); // 6
});
Dynamic imports return a Promise, which can be handled using async/await or .then().
Browser Support
Modern browsers natively support ES Modules, which can be used by adding the type="module" attribute to the <script> tag:
<script type="module" src="app.js"></script>
ES Modules in browsers have the following characteristics:
- Support cross-origin requests (requires CORS).
- Automatically deferred execution (equivalent to adding the
deferattribute). - Access to the current module’s URL via
import.meta.url.
Differences from CommonJS
ES Modules differ significantly from the CommonJS module system commonly used in Node.js:
- Static vs. Dynamic: ES Modules are static, while CommonJS is dynamic.
- Value Copy vs. Live Binding: ES Modules export a copy of values (for primitive types) or references (for objects), while CommonJS exports live bindings.
- Synchronous vs. Asynchronous: CommonJS loads modules synchronously, while ES Modules support asynchronous loading.
Webpack Module System
Webpack is a powerful module bundler that extends JavaScript’s modular capabilities, supporting multiple module types, including ES Modules, CommonJS, and AMD.
Core Concepts
- Entry: Specifies the starting point for bundling.
- Output: Specifies the location and name of the bundled files.
- Loaders: Process non-JavaScript files.
- Plugins: Perform broader tasks such as optimization, asset management, and environment variable injection.
Configuration Example
A basic Webpack configuration file:
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
}
]
},
plugins: [
// Plugin instances
]
};
Module Resolution
Webpack uses its own module resolution algorithm to locate modules:
- Resolves relative paths (e.g.,
./moduleor../module). - Resolves absolute paths.
- Searches in
node_modules.
Module resolution can be customized using the resolve configuration:
resolve: {
extensions: ['.js', '.jsx'], // Automatically resolve these extensions
alias: {
'@': path.resolve(__dirname, 'src/') // Create aliases
}
}
Code Splitting
Webpack supports code splitting to load modules on demand:
// Dynamic import
import('./math.js').then(math => {
console.log(math.add(2, 3));
});
// Or using dynamic import() syntax
button.addEventListener('click', () => {
import('./math.js').then(math => {
console.log(math.add(2, 3));
});
});
Webpack splits dynamically imported modules into separate chunks.
Module Federation
Introduced in Webpack 5, Module Federation allows different Webpack builds to share code:
// Host application configuration
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js'
},
shared: ['react', 'react-dom']
});
// Remote application configuration
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button'
},
shared: ['react', 'react-dom']
});
The host application can dynamically import modules from the remote application:
import('app1/Button').then(Button => {
// Use Button component
});
SystemJS
SystemJS is a universal module loader that supports multiple module formats (ES Modules, CommonJS, AMD, etc.) and runs in both browsers and Node.js.
Features
- Dynamic module loading.
- Supports multiple module formats.
- Can be used directly in browsers.
- Supports module mapping and redirection.
Basic Usage
First, include SystemJS:
<script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.min.js"></script>
Then load a module:
System.import('./module.js').then(module => {
module.default(); // Call default export
});
Configuration
SystemJS can be configured with module mappings and base URLs:
SystemJS.config({
baseURL: '/base',
map: {
'jquery': 'libs/jquery.js',
'lodash': 'libs/lodash.js'
},
packages: {
'app': {
main: 'main.js',
defaultExtension: 'js'
}
}
});
System.import('app/main').then(module => {
// Use module
});
Relationship with ES Modules
SystemJS can load ES Modules, but in modern browsers, using native ES Modules is typically more efficient. SystemJS’s primary advantage is its support for older browsers and compatibility with multiple module formats.
Cross-Origin Communication Mechanisms
postMessage API
The postMessage API, introduced in HTML5, enables secure cross-document communication between different windows or iframes.
Basic Usage
Sending a message:
const targetWindow = document.getElementById('iframe').contentWindow;
targetWindow.postMessage('Hello from parent', 'https://example.com');
Receiving a message:
window.addEventListener('message', (event) => {
// Verify origin
if (event.origin !== 'https://trusted-domain.com') return;
console.log('Received:', event.data);
});
Security Considerations
- Always verify
event.originandevent.source. - Do not trust messages from unknown sources.
- Use specific target domains instead of
*.
Complex Data Transfer
postMessage can transfer complex objects:
const data = {
type: 'update',
payload: { count: 42 }
};
targetWindow.postMessage(data, 'https://example.com');
CustomEvent
CustomEvent allows the creation and dispatching of custom events for communication within the same document.
Creating and Dispatching Events
// Create custom event
const event = new CustomEvent('myEvent', {
detail: { message: 'Hello World' }
});
// Dispatch event
document.dispatchEvent(event);
Listening for Events
document.addEventListener('myEvent', (e) => {
console.log('Received:', e.detail.message);
});
Crossing Shadow DOM Boundaries
CustomEvent can propagate across shadow DOM boundaries if composed: true is set:
const event = new CustomEvent('myEvent', {
detail: { message: 'Hello' },
bubbles: true,
composed: true // Allows crossing shadow DOM boundaries
});
BroadcastChannel
BroadcastChannel enables communication between different browsing contexts (windows, iframes, workers) of the same origin.
Creating and Using
// Create channel
const channel = new BroadcastChannel('my-channel');
// Send message
channel.postMessage({ type: 'update', data: 42 });
// Receive message
channel.onmessage = (event) => {
console.log('Received:', event.data);
};
// Close channel
channel.close();
Features
- Simple and easy to use.
- Automatically handles message delivery.
- Supports multicast (one message sent to all subscribers).
- Channel closes automatically when the last listener is removed.
Service Worker Communication
Service Workers are scripts running in the background of the browser, capable of communicating with pages.
Basic Communication Pattern
Sending a message from a page to a Service Worker:
navigator.serviceWorker.controller.postMessage({
type: 'FETCH',
url: '/api/data'
});
Receiving a message in the Service Worker:
self.addEventListener('message', (event) => {
if (event.data.type === 'FETCH') {
fetch(event.data.url).then(response => {
event.waitUntil(
response.json().then(data => {
// Cache data or perform other operations
})
);
});
}
});
Sending a message from the Service Worker to a page:
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage({ type: 'DATA_READY', data: { /* ... */ } });
});
});
Practical Use Cases
- Cache strategy control.
- Background synchronization.
- Push notifications.
- Cross-page state sharing.
Micro Frontend Isolation Techniques
Style Isolation Solutions
Shadow DOM
Shadow DOM provides true style isolation, ensuring that internal styles do not affect the outside and vice versa.
const shadowHost = document.createElement('div');
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
p { color: red; }
</style>
<p>This is inside shadow DOM</p>
`;
document.body.appendChild(shadowHost);
Advantages:
- True style isolation.
- Does not affect global styles.
Disadvantages:
- Compatibility issues (not supported in Internet Explorer).
- May require extra work for integration with some frameworks.
Scoped CSS
Scoped CSS achieves style isolation by adding unique attribute selectors to components.
<style>
/* Using attribute selectors */
[data-v-123456] button {
color: blue;
}
</style>
<div data-v-123456>
<button>Click me</button>
</div>
Implementation Methods:
- Webpack’s
vue-loaderorcss-loadercan automatically add scoped attributes. - Manually add unique attributes to components.
Advantages:
- Simple to implement.
- Good compatibility.
Disadvantages:
- Style selectors can become complex.
- Potential for style leakage in some cases.
CSS-in-JS
CSS-in-JS libraries (e.g., styled-components, emotion) generate unique class names at runtime for style isolation.
import styled from 'styled-components';
const Button = styled.button`
color: blue;
`;
function Component() {
return <Button>Click me</Button>;
}
Advantages:
- Complete style isolation.
- Supports dynamic styles.
- Componentized styling.
Disadvantages:
- Runtime performance overhead.
- May increase bundle size.
Namespace Isolation
Add namespace prefixes to each component/module through conventions or tools.
/* Using BEM or other naming conventions */
.app-header__title { ... }
.user-profile__avatar { ... }
Tool Support:
- CSS Modules.
- PostCSS plugins.
Advantages:
- Simple to implement.
- Good compatibility.
Disadvantages:
- Requires strict adherence to naming conventions.
- Can be hard to maintain in large projects.
JavaScript Execution Isolation
iframe Sandbox
iframes provide a natural isolation environment for loading independent applications.
<iframe src="app.html" sandbox="allow-scripts allow-same-origin"></iframe>
Sandbox Attributes:
allow-scripts: Allows script execution.allow-same-origin: Allows same-origin access.allow-forms: Allows form submissions.allow-popups: Allows pop-up windows.
Advantages:
- Complete isolation.
- Easy to use.
Disadvantages:
- High performance overhead.
- Complex communication.
- Not SEO-friendly.
Proxy Sandbox
Use Proxy to intercept object operations, creating a secure execution environment.
function createSandbox(context) {
const proxy = new Proxy(context, {
get(target, prop) {
if (prop === 'dangerousMethod') {
throw new Error('Access denied');
}
return target[prop];
},
set(target, prop, value) {
if (prop === 'readOnlyProp') {
throw new Error('Read-only property');
}
target[prop] = value;
return true;
}
});
return proxy;
}
const context = { name: 'Sandbox', count: 0 };
const sandbox = createSandbox(context);
sandbox.count++; // Works fine
sandbox.dangerousMethod(); // Throws error
Advantages:
- Flexible control.
- Highly customizable.
Disadvantages:
- Complex implementation.
- Cannot fully isolate native objects.
Snapshot Sandbox
Achieves isolation by saving and restoring global state.
class SnapshotSandbox {
constructor() {
this.proxy = window;
this.modifyPropsMap = {};
this.windowSnapshot = {};
}
active() {
this.windowSnapshot = {};
for (const prop in window) {
if (window.hasOwnProperty(prop)) {
this.windowSnapshot[prop] = window[prop];
}
}
return this.proxy;
}
inactive() {
for (const prop in window) {
if (window.hasOwnProperty(prop) && window[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
}
// Restore modified properties
Object.keys(this.modifyPropsMap).forEach(prop => {
window[prop] = this.modifyPropsMap[prop];
});
this.modifyPropsMap = {};
}
}
Advantages:
- Relatively simple implementation.
- Minimal performance impact.
Disadvantages:
- Incomplete isolation.
- May affect global state.
Web Worker Isolation
Web Workers run in a separate thread, isolated from the main thread.
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ type: 'calculate', data: 42 });
worker.onmessage = (event) => {
console.log('Result:', event.data);
};
// worker.js
self.onmessage = (event) => {
if (event.data.type === 'calculate') {
const result = heavyCalculation(event.data.data);
self.postMessage(result);
}
};
function heavyCalculation(data) {
// Heavy computation
return data * 2;
}
Advantages:
- True isolation.
- Does not block the main thread.
Disadvantages:
- Cannot access the DOM.
- Communication overhead.
- Compatibility issues.
Micro Frontend Routing Solutions
Hash Routing
Hash routing uses the hash portion of the URL (# and beyond) to represent routing state.
// Listen for hash changes
window.addEventListener('hashchange', () => {
const hash = location.hash.substring(1);
console.log('Current hash:', hash);
// Load corresponding module based on hash
});
// Navigate
function navigateTo(hash) {
location.hash = hash;
}
Advantages:
- Good compatibility.
- No server configuration required.
Disadvantages:
- URLs are less aesthetically pleasing.
- Not SEO-friendly.
History Routing
The History API provides a modern routing approach, allowing URL modifications without page refreshes.
// Initialize
if (history.pushState) {
history.replaceState({ page: 1 }, '', window.location.href);
}
// Listen for popstate events
window.addEventListener('popstate', (event) => {
console.log('State:', event.state);
// Load corresponding module based on state
});
// Navigate
function navigateTo(path, state) {
history.pushState(state, '', path);
// Load corresponding module
}
Advantages:
- Clean URLs.
- Supports forward/back navigation.
Disadvantages:
- Requires server configuration (SPAs typically need to return index.html).
- Slightly lower compatibility (IE10+).
Routing Distribution Mechanisms
In micro frontend architectures, routing distribution typically follows these approaches:
- Base App Routing Distribution: The base application handles route parsing and loads sub-applications based on routing rules.
- Sub-App Autonomous Routing: Each sub-application manages its own routing, with the base app only handling initial loading.
- Hybrid Mode: The base app handles main routes, while sub-applications handle their own sub-routes.
Base App Routing Distribution Example
// Base application routing configuration
const routes = [
{ path: '/app1', app: 'app1' },
{ path: '/app2', app: 'app2' }
];
// Listen for route changes
window.addEventListener('popstate', handleRouteChange);
window.addEventListener('hashchange', handleRouteChange);
function handleRouteChange() {
const path = location.pathname || location.hash.substring(1);
const route = routes.find(r => path.startsWith(r.path));
if (route) {
loadApp(route.app);
}
}
function loadApp(appName) {
// Dynamically load sub-application
import(`./apps/${appName}/entry.js`).then(module => {
module.mount();
});
}
Sub-App Routing Example
Sub-applications can use their own routing libraries (e.g., React Router, Vue Router):
// Sub-application routing configuration
const childRoutes = [
{ path: '/dashboard', component: Dashboard },
{ path: '/settings', component: Settings }
];
// In sub-application entry file
function mount() {
const app = createApp(App);
app.use(createRouter({ routes: childRoutes }));
app.mount('#app');
}
Micro Frontend Build Toolchains
Webpack Basic Configuration
Webpack is the most popular JavaScript bundler. A sample configuration:
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000
}
};
Key Configuration Items
- entry: Entry file.
- output: Output configuration.
- module.rules: Loader configuration.
- plugins: Plugin configuration.
- devServer: Development server configuration.
Rollup Basic Configuration
Rollup is a bundler focused on JavaScript libraries. A sample configuration:
// rollup.config.js
import babel from '@rollup/plugin-babel';
import { terser } from 'rollup-plugin-terser';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'umd',
name: 'MyLibrary',
sourcemap: true
},
plugins: [
resolve(),
commonjs(),
babel({ babelHelpers: 'bundled' }),
process.env.NODE_ENV === 'production' && terser()
]
};
Key Configuration Items
- input: Entry file.
- output: Output configuration (format can be umd, esm, cjs, etc.).
- plugins: Plugin configuration.
Vite Basic Configuration
Vite is a build tool based on native ES Modules. A sample configuration:
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
},
build: {
outDir: 'dist',
sourcemap: true
}
});
Key Configuration Items
- plugins: Plugin configuration.
- server: Development server configuration.
- build: Production build configuration.
Parcel Basic Configuration
Parcel is a zero-configuration bundler. Basic usage:
npm install -g parcel-bundler
parcel index.html
Parcel automatically detects file types and applies appropriate transformers. For custom configuration, create a .parcelrc file:
{
"extends": "@parcel/config-default",
"transformers": {
"*.{js,jsx}": ["@parcel/transformer-babel"]
}
}
Key Features
- Zero configuration.
- Automatic file type detection.
- Parallel processing.
- Hot module replacement.
Summary
This document explores various technologies and implementation approaches related to micro frontends, including:
- Browser Module Systems: Features and usage of ES Modules, Webpack module system, and SystemJS.
- Cross-Origin Communication Mechanisms: Implementation and security considerations for postMessage API, CustomEvent, BroadcastChannel, and Service Worker communication.
- Micro Frontend Isolation Techniques: Principles and pros/cons of style isolation (Shadow DOM, Scoped CSS, CSS-in-JS, namespace isolation) and JavaScript execution isolation (iframe sandbox, Proxy sandbox, snapshot sandbox).
- Micro Frontend Routing Solutions: Implementation of Hash routing, History routing, and routing distribution mechanisms.
- Micro Frontend Build Toolchains: Basic configurations and features of Webpack, Rollup, Vite, and Parcel.
Micro frontend architecture enables modular, independent development and deployment for large-scale frontend applications but introduces challenges in communication, isolation, and build processes. Choosing the right solution requires balancing project scale, team capabilities, and specific requirements. As browser technologies evolve, micro frontend implementations will become more efficient and straightforward.



