Micro-frontends with Module Federation
Build scalable micro-frontend architectures using Aurelia 2 with Webpack 5 Module Federation and Vite federation plugins.
Module Federation enables independent deployment and development of micro-frontends by allowing different Aurelia applications to share code and components at runtime. This guide covers implementing Module Federation with both Webpack 5 and Vite for maximum flexibility.
Understanding Module Federation
Module Federation allows you to:
Share components between Aurelia applications at runtime
Deploy independently without coordinating releases
Load modules dynamically from remote applications
Avoid code duplication across micro-frontends
Mix technology stacks (Aurelia with React, Vue, etc.)
Architecture Overview
Webpack 5 Module Federation
1. Install Dependencies
npm install webpack@5 webpack-cli webpack-dev-server html-webpack-plugin
npm install aurelia ts-loader html-loader
2. Configure the Remote Application (Product App)
webpack.config.js:
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devServer: {
port: 4001,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.html$/i,
use: 'html-loader',
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'productApp',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/product-list',
'./ProductDetail': './src/components/product-detail',
'./ProductModule': './src/product-module',
},
shared: {
aurelia: {
singleton: true,
requiredVersion: '^2.0.0',
},
},
}),
new HtmlWebpackPlugin({
template: './src/index.html',
}),
],
resolve: {
extensions: ['.ts', '.js'],
},
};
3. Create Exposed Components
src/components/product-list.ts:
import { customElement, bindable } from 'aurelia';
export interface Product {
id: number;
name: string;
price: number;
}
@customElement({
name: 'product-list',
template: `
<template>
<div class="product-grid">
<div class="product-card" repeat.for="product of products">
<h3>\${product.name}</h3>
<p>$\${product.price}</p>
<button click.trigger="selectProduct(product)">
View Details
</button>
</div>
</div>
</template>
`
})
export class ProductList {
@bindable public products: Product[] = [];
@bindable public onProductSelect?: (product: Product) => void;
public selectProduct(product: Product): void {
this.onProductSelect?.(product);
}
}
src/product-module.ts:
import { IContainer, IRegistry } from 'aurelia';
import { ProductList } from './components/product-list';
import { ProductDetail } from './components/product-detail';
export const ProductModule: IRegistry = {
register(container: IContainer): void {
container.register(ProductList, ProductDetail);
}
};
4. Configure the Host Application (Shell App)
webpack.config.js:
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devServer: {
port: 4000,
historyApiFallback: true,
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.html$/i,
use: 'html-loader',
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
productApp: 'productApp@http://localhost:4001/remoteEntry.js',
userApp: 'userApp@http://localhost:4002/remoteEntry.js',
},
shared: {
aurelia: {
singleton: true,
requiredVersion: '^2.0.0',
},
},
}),
new HtmlWebpackPlugin({
template: './src/index.html',
}),
],
resolve: {
extensions: ['.ts', '.js'],
},
};
5. Dynamic Loading in Host Application
src/components/micro-frontend-loader.ts:
import { customElement, bindable, IContainer } from 'aurelia';
@customElement({
name: 'micro-frontend-loader',
template: `
<template>
<div if.bind="loading">Loading micro-frontend...</div>
<div if.bind="error" class="error">
Failed to load micro-frontend: \${error}
</div>
<div if.bind="!loading && !error" ref="container"></div>
</template>
`
})
export class MicroFrontendLoader {
@bindable public remoteName: string = '';
@bindable public moduleName: string = '';
@bindable public componentName: string = '';
private container!: HTMLElement;
private loading = false;
private error: string | null = null;
constructor(private aurelia: IContainer) {}
public async attached(): Promise<void> {
if (!this.remoteName || !this.moduleName) {
this.error = 'Remote name and module name are required';
return;
}
await this.loadMicroFrontend();
}
private async loadMicroFrontend(): Promise<void> {
this.loading = true;
this.error = null;
try {
// Dynamic import from remote
const remoteModule = await import(
`${this.remoteName}/${this.moduleName}`
);
if (this.componentName) {
// Load specific component
const ComponentClass = remoteModule[this.componentName];
if (ComponentClass) {
// Register and render component
this.aurelia.register(ComponentClass);
// Custom rendering logic here
}
} else {
// Load entire module registry
const moduleRegistry = remoteModule.default || remoteModule[this.moduleName];
if (moduleRegistry && typeof moduleRegistry.register === 'function') {
moduleRegistry.register(this.aurelia);
}
}
} catch (err) {
console.error('Failed to load micro-frontend:', err);
this.error = err instanceof Error ? err.message : 'Unknown error';
} finally {
this.loading = false;
}
}
}
src/my-app.ts:
import { route } from '@aurelia/router';
@route({
routes: [
{ path: '', redirectTo: 'home' },
{ path: 'home', component: () => import('./views/home') },
{
path: 'products',
component: () => import('./views/products-shell'),
title: 'Products'
},
{
path: 'users',
component: () => import('./views/users-shell'),
title: 'Users'
}
]
})
export class MyApp {
public message = 'Shell Application';
}
src/views/products-shell.html:
<template>
<div class="products-page">
<h2>Products (Micro-frontend)</h2>
<micro-frontend-loader
remote-name="productApp"
module-name="ProductList"
component-name="ProductList">
</micro-frontend-loader>
</div>
</template>
Vite Module Federation
1. Install Dependencies
npm install vite @vitejs/plugin-legacy
npm install @originjs/vite-plugin-federation
# Alternative: npm install @module-federation/vite
2. Configure Remote Application
vite.config.ts:
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
federation({
name: 'productApp',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/product-list.ts',
'./ProductModule': './src/product-module.ts',
},
shared: {
aurelia: {
singleton: true,
},
},
}),
],
server: {
port: 4001,
cors: true,
},
build: {
target: 'esnext',
minify: false,
cssCodeSplit: false,
},
});
3. Configure Host Application
vite.config.ts:
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
federation({
name: 'shell',
remotes: {
productApp: 'http://localhost:4001/assets/remoteEntry.js',
userApp: 'http://localhost:4002/assets/remoteEntry.js',
},
shared: {
aurelia: {
singleton: true,
},
},
}),
],
server: {
port: 4000,
},
build: {
target: 'esnext',
minify: false,
cssCodeSplit: false,
},
});
Advanced Patterns
Error Boundaries and Fallbacks
src/components/micro-frontend-boundary.ts:
import { customElement, bindable } from 'aurelia';
@customElement({
name: 'micro-frontend-boundary',
template: `
<template>
<div if.bind="hasError" class="error-boundary">
<h3>Something went wrong</h3>
<p>\${errorMessage}</p>
<button click.trigger="retry()">Retry</button>
</div>
<div if.bind="!hasError">
<slot></slot>
</div>
</template>
`
})
export class MicroFrontendBoundary {
@bindable public onError?: (error: Error) => void;
private hasError = false;
private errorMessage = '';
public handleError(error: Error): void {
this.hasError = true;
this.errorMessage = error.message;
this.onError?.(error);
console.error('Micro-frontend error:', error);
}
public retry(): void {
this.hasError = false;
this.errorMessage = '';
// Trigger re-render of child content
}
}
Shared State Management
src/services/micro-frontend-state.ts:
import { IEventAggregator, singleton } from 'aurelia';
export interface MicroFrontendMessage {
source: string;
type: string;
payload: any;
}
@singleton()
export class MicroFrontendState {
constructor(private eventAggregator: IEventAggregator) {}
public publish(message: MicroFrontendMessage): void {
this.eventAggregator.publish('micro-frontend:message', message);
}
public subscribe(callback: (message: MicroFrontendMessage) => void): void {
this.eventAggregator.subscribe('micro-frontend:message', callback);
}
public shareData(key: string, data: any): void {
(window as any).__SHARED_STATE__ = (window as any).__SHARED_STATE__ || {};
(window as any).__SHARED_STATE__[key] = data;
}
public getData(key: string): any {
return (window as any).__SHARED_STATE__?.[key];
}
}
Performance Optimizations
Preloading Remote Modules
// Preload critical micro-frontends
const preloadModules = async () => {
try {
// Preload but don't execute
await import(/* webpackPreload: true */ 'productApp/ProductModule');
} catch (error) {
console.warn('Failed to preload module:', error);
}
};
// Call during app initialization
preloadModules();
Lazy Loading Strategy
@customElement({
name: 'lazy-micro-frontend',
template: `
<template>
<div class="intersection-observer" ref="trigger">
<div if.bind="visible">
<micro-frontend-loader
remote-name.bind="remoteName"
module-name.bind="moduleName">
</micro-frontend-loader>
</div>
</div>
</template>
`
})
export class LazyMicroFrontend {
@bindable public remoteName: string = '';
@bindable public moduleName: string = '';
private trigger!: HTMLElement;
private visible = false;
public attached(): void {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.visible = true;
observer.disconnect();
}
});
observer.observe(this.trigger);
}
}
Best Practices
1. Versioning Strategy
Use semantic versioning for shared dependencies
Pin major versions to avoid breaking changes
Test compatibility across micro-frontends
2. Development Workflow
# Start all micro-frontends in development
npm run dev:shell # Port 4000
npm run dev:products # Port 4001
npm run dev:users # Port 4002
3. Production Deployment
Deploy each micro-frontend independently
Use CDN for shared dependencies
Implement health checks for remote modules
Set up monitoring for failed module loads
4. Testing Strategy
Unit test components in isolation
Integration test the shell application
E2E test critical user journeys
Contract testing between micro-frontends
This Module Federation setup enables scalable micro-frontend architectures with Aurelia 2, supporting both Webpack 5 and Vite build systems while maintaining independent development and deployment capabilities.
Last updated
Was this helpful?