33
loading...
This website collects cookies to deliver better user experience
Better way to share code. Expose any code from any application that Webpack supports.
Environment-Independent. Use shared code in different environment web, Node.js etc.
Resolves Dependency Issues. Federated code defines their dependencies and if Webpack can’t find it in the scope, will download it.
import {Configuration, container} from 'webpack';
export const webpackConfig: Configuration = {
plugins: [
new container.ModuleFederationPlugin({
name: '',
shared: []
})
]
};
export default webpackConfig;
plugins: [
new container.ModuleFederationPlugin({
name: 'shell',
})
]
};
plugins: [
new container.ModuleFederationPlugin({
filename: 'shell/remoteHome.js'
})
]
plugins: [
new container.ModuleFederationPlugin({
remotes: {
ShellModule: 'ShellModule@http://localhost:4400/remoteHome.js'
}
})
]
ShellModule
and the value is the URL where the container will be loaded from. The property name is used as the request scope ShellModule@[appUrl].filename.js
.plugins: [
new container.ModuleFederationPlugin({
exposes: {
HomeComponent: './projects/app1-home/src/app/home/home.component.ts',
ShellModule: './projects/app1-home/src/app/shell/shell.module.ts'
}
})
]
HomeComponent
exposes a single Angular Component file while the second ShellModule
exposes a module.@angular/core
, @angular/common
, and @angular/router
.plugins: [
new container.ModuleFederationPlugin({
shared: {
'@angular/core': {eager: true, singleton: true},
'@angular/common': {eager: true, singleton: true},
'@angular/router': {eager: true, singleton: true},
'place-my-order-assets': {eager: true, singleton: true},
}
})
]
sharedPlugin
which has its own set of configuration properties. This helps manage how libraries are shared in the shared scope.eager: Allows Webpack to include the shared packages directly instead of fetching the library via an asynchronous request. When Eager is set as true
, all shared modules will be compiled with the exposed module.
singleton: Allows only a single version of the shared module in the shared scope. This means at every instance, only one version of the package will be loaded on the page. If a scope already has a version of @angular/core, and the imported module uses a different version of @angular/core, Webpack will ignore the new version and use the version already present in the scope.
StrictVersion: Allows Webpack to reject the shared module if its version is not valid. This is useful when the required version is specified.
RequiredVersion: This option states the required version of the shared module. Learn more about the shared option on the Webpack official documentation.
Output: Enables you to set your public path and the unique name for the build. This is useful when you load multiple modules from different remotes. Learn More.
Experiments: The ‘experiments’ feature was also introduced in Webpack 5. It enables some Webpack experimental features like topLevelAwait etc. Learn More.
Optimization: The ‘optimization’ option adds more optimizing features to help make your all build size smaller. Learn More.
import {Configuration, container} from 'webpack';
export const webpackConfig: Configuration = {
output: {
publicPath: 'http://localhost:4400/',
uniqueName: 'shell',
},
experiments: {
topLevelAwait: true,
},
optimization: {
runtimeChunk: false,
}
// ....
}
HomeComponent.ts
file and consumes RestaurantModule
from app2-restaurant and OrderModule
from app3-orders.RestaurantModule
and consumes HomeComponent.ts
from app1-home and OrderModule
from app3-orders.OrderModule
and OrderComponent.ts
file and consumes HomeComponent.ts
from app1-home and ResturantModule
from app2-restaurant.npm install
ng g application app2-restaurant
ng g application app3-orders
angular.json
file. You will see three Angular applications.webpack.config.ts
and webpack.config.prod.ts
at the root of each project directory.touch webpack.config.ts, webpack.config.prod.ts
touch projects/app2-restaurant/webpack.config.ts, projects/app2-restaurant/webpack.config.prod.ts
touch projects/app3-orders/webpack.config.ts, projects/app3-orders/webpack.config.prod.ts
npm i -D @angular-builders/custom-webpack
/tailwind.config.js
module.exports = {
// ....
purge: {
enabled: true,
content: [
'./src/**/*.{html,ts}',
'./projects/app2-restaurant/src/**/*.{html,ts}',
'./projects/app3-orders/src/**/*.{html,ts}'
]
}
// ...
};
/module-federation-starter/src/app/pages/restaurant
to app2 app2-restaurant directory projects/app2-restaurant/src/app/
. Your folder structure should look like this:projects/app2-restaurant/src/app/app-routing.module.ts
file in app2 to include the route path for restaurants
// ...
const routes: Routes = [
{
path: 'restaurants',
loadChildren: () => import('./restaurant/restaurant.module').then(m => m.RestaurantModule),
},
];
// ...
/module-federation-starter/src/app/pages/order
to app3-order directory projects/app3-orders/src/app
. Your folder structure should look like this:projects/app3-orders/src/app/app-routing.module.ts
in app3 to include the order route:
{
path: 'order',
loadChildren: () => import('./order/order.module').then(m => m.OrderModule),
},
src/app/app-routing.module.ts
in the main project app1-home to:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
loadChildren: () => import('./pages/home/home.module').then(m => m.HomeModule),
}
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
scrollPositionRestoration: "enabled"
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
package.json
file to be able to serve and build each application separately:// ......
"scripts": {
"ng": "ng",
"start:app1": "ng serve",
"start:app2": "ng serve app2-restaurant",
"start:app3": "ng serve app3-orders",
"build:app1": "ng build",
"build:app2": "ng build app2-restaurant"
"build:app3": "ng build app3-orders"
"watch": "ng build --watch --configuration development",
"test": "ng test",
"start:all": "npm run start:app1 & npm run start:app2 & npm run start:app3"
}
// ......
/webpack.config.ts
in project app1-home:
import {Configuration, container} from 'webpack';
import dep from 'package.json';
export const webpackConfig: Configuration = {
output: {
publicPath: 'http://localhost:4203/',
uniqueName: 'home',
},
experiments: {
topLevelAwait: true,
},
optimization: {
runtimeChunk: false,
},
plugins: [
new container.ModuleFederationPlugin({
name: 'home',
library: {type: 'var', name: 'home'},
filename: 'remoteHome.js',
exposes: {
HomeComponent: './src/app/pages/home/home.component.ts'
},
shared: {
'@angular/core': {
eager: true,
singleton: true,
strictVersion: true,
requiredVersion: dep.dependencies['@angular/core']
},
'@angular/common': {
eager: true,
singleton: true,
strictVersion: true,
requiredVersion: dep.dependencies["@angular/common"]
},
'@angular/router': {
eager: true,
singleton: true,
strictVersion: true,
requiredVersion: dep.dependencies["@angular/router"],
},
'place-my-order-assets': {eager: true, singleton: true, strictVersion: true},
}
})
]
};
export default webpackConfig;
/webpack.config.prod.ts
to:
import webpackConfig from './webpack.config';
import {Configuration} from 'webpack';
export const prodWebpackConfig: Configuration = {
...webpackConfig,
output: {
publicPath: 'http://localhost:80/', // production server,
uniqueName: 'home',
},
};
export default prodWebpackConfig;
projects/app2-restauran/webpack.config.ts
in project app2-restaurant
import {Configuration, container} from 'webpack';
import dep from 'package.json';
export const webpackConfig: Configuration = {
output: {
publicPath: 'http://localhost:4204/',
uniqueName: 'restaurant',
},
experiments: {
topLevelAwait: true,
},
optimization: {
runtimeChunk: false,
},
plugins: [
new container.ModuleFederationPlugin({
name: 'restaurant',
library: {type: 'var', name: 'restaurant',},
filename: 'remoteRestaurant.js',
exposes: {
RestaurantModule: './projects/app2-restaurant/src/app/restaurant/restaurant.module.ts'
},
shared: {
'@angular/core': {
eager: true,
singleton: true,
strictVersion: true,
requiredVersion: dep.dependencies["@angular/router"]
},
'@angular/common': {
eager: true,
singleton: true,
strictVersion: true,
requiredVersion: dep.dependencies["@angular/common"]
},
'@angular/router': {
eager: true,
singleton: true,
strictVersion: true,
requiredVersion: dep.dependencies["@angular/router"]
},
'place-my-order-assets': {eager: true, singleton: true},
}
})
]
};
export default webpackConfig;
/app2-restaurant/webpack.config.prod.ts
to:
import webpackConfig from './webpack.config';
import {Configuration} from 'webpack';
export const prodWebpackConfig: Configuration = {
...webpackConfig,
output: {
publicPath: 'http://localhost:81/', // production server,
uniqueName: 'restaurant',
},
};
export default prodWebpackConfig;
projects/app3-orders/webpack.config.ts
in project app3-orders
import {Configuration, container} from 'webpack';
import dep from 'package.json';
export const webpackConfig: Configuration = {
output: {
publicPath: 'http://localhost:4205/',
uniqueName: 'orders',
},
experiments: {
topLevelAwait: true,
},
optimization: {
runtimeChunk: false,
},
plugins: [
new container.ModuleFederationPlugin({
name: 'orders',
library: {type: 'var', name: 'orders'},
filename: 'remoteOrders.js',
exposes: {
OrderModule: './projects/app3-orders/src/app/order/order.module.ts',
OrderComponent: './projects/app3-orders/src/app/order/order.component.ts'
},
shared: {
'@angular/core': {
eager: true,
singleton: true,
strictVersion: true,
requiredVersion: dep.dependencies['@angular/core']
},
'@angular/common': {
eager: true,
singleton: true,
strictVersion: true,
requiredVersion: dep.dependencies['@angular/common']
},
'@angular/router': {
eager: true,
singleton: true,
strictVersion: true,
requiredVersion: dep.dependencies["@angular/router"]
},
'place-my-order-assets': {eager: true, singleton: true},
}
})
]
};
export default webpackConfig;
projects/app3-orders/webpack.config.prod.ts
to:
import webpackConfig from './webpack.config';
import {Configuration} from 'webpack';
export const prodWebpackConfig: Configuration = {
...webpackConfig,
output: {
publicPath: 'http://localhost:82/', // production server,
uniqueName: 'orders',
},
};
export default prodWebpackConfig;
ng g library utils
src/app/core/header
files to the Shared Library. Because you broke your app into three different apps that would run independently you should share the app header among all three apps so you do not have to duplicate the header component in all the applications. Navigate to projects /utils/src/lib/utils.component.ts
and update it to:
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-header',
template: `
<header>
<nav class="bg-picton-blue sticky top-0 z-50 w-full dark:bg-gray-700 mb-4 py-1">
<div class="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
<div class="relative flex items-center sm:justify-start justify-center header-height-50">
<svg
class="flex sm:hidden cursor-pointer justify-start stroke-current text-white hover:text-nav-hover-blue h-6 w-6"
style="max-width: 20px; margin-left: 20px"
(click)="showMobileMenu = !showMobileMenu"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
<div class="flex items-center flex-wrap justify-center pl-3 sm:items-stretch sm:justify-start">
<div class="flex-shrink-0 flex items-center">
<span [routerLink]="'/home'" class="text-white text-lg cursor-pointer"> Place-my-order.com</span>
</div>
<div class="hidden sm:block sm:ml-6 ">
<div class="flex space-x-4">
<a routerLink="/" routerLinkActive="bg-nav-menu-active"
class="border hover:bg-nav-hover-blue hover:text-white cursor-pointer border-white-100 text-white px-3 py-2 rounded-md text-sm font-medium">Home</a>
<a routerLink="/restaurants" routerLinkActive="bg-nav-menu-active"
class="border border-white-100 text-white hover:bg-nav-hover-blue hover:text-white px-3 py-2 rounded-md text-sm font-medium">Restaurant</a>
<a routerLink="/order/order-history" routerLinkActive="bg-nav-menu-active"
class="border border-white-100 text-white hover:bg-nav-hover-blue hover:text-white px-3 py-2 rounded-md text-sm font-medium">Order History</a>
</div>
</div>
</div>
</div>
</div>
<!--Mobile Menu-->
<div class="sm:hidden" id="mobile-menu" [hidden]="!showMobileMenu">
<div class="px-2 pt-2 pb-3 space-y-1">
<!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" --> <a routerLink="/home" routerLinkActive="bg-nav-menu-active"
class="text-white block px-3 py-2 rounded-md text-base font-medium" aria-current="page">Home</a>
<a routerLink="/restaurants" routerLinkActive="bg-nav-menu-active"
class="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">Restaurant</a>
<a routerLink="/order/order-history" routerLinkActive="bg-nav-menu-active"
class="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium">Order History</a>
</div>
</div>
</nav>
</header>
`,
styles: []
})
export class UtilsComponent implements OnInit {
showMobileMenu = false;
constructor() {
}
ngOnInit(): void {
}
}
To implement this, create a new file called mfe-utils.ts
in the shared library lib
folder, and add the following in the file:
projects /utils/src/lib/mfe.utils.ts
Declare Webpack Variables Type:
/* Webpack types */
type Factory = () => any;
interface Container {
init(shareScope: any): void;
get(module: string): Factory;
}
declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>;
declare const __webpack_share_scopes__: { default: any };
/* MFE*/
export enum FileType {
Component = 'Component',
Module = 'Module',
Css = 'CSS',
Html = 'Html'
}
export interface LoadRemoteFileOptions {
remoteEntry: string;
remoteName: string;
exposedFile: string;
exposeFileType: FileType;
}
get
and init
methods.MfeUtil
and add a function into it.private fileMap: Record<string, boolean> = {};
private loadRemoteEntry = async (remoteEntry: string): Promise<void> => {
return new Promise<void>((resolve, reject) =>
if (this.fileMap[remoteEntry]) {
resolve();
return;
}
const script = document.createElement("script");
script.src = remoteEntry;
script.onerror = (error: string | Event) => {
console.error(error, 'unable to load remote entry');
reject();
}
script.onload = () => {
this.fileMap[remoteEntry] = true;
resolve(); // window is the global namespace
};
document.body.append(script);
});
}
"default"
. This name can be changed. Learn More.findExposedModule = async <T>(uniqueName: string, exposedFile: string): Promise<T | undefined> => {
let Module: T | undefined;
// Initializes the shared scope. Fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__('default');
const container: Container = (window as any)[uniqueName]; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const factory = await container.get(exposedFile);
Module = factory();
return Module
}
public loadRemoteFile = async (loadRemoteModuleOptions: LoadRemoteFileOptions): Promise<any> => {
await this.loadRemoteEntry(loadRemoteModuleOptions.remoteEntry);
return await this.findExposedModule<any>(
loadRemoteModuleOptions.remoteName,
loadRemoteModuleOptions.exposedFile
);
}
type Factory = () => any;
interface Container {
init(shareScope: string): void;
get(module: string): Factory;
}
declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>;
declare const __webpack_share_scopes__: { default: string };
export enum FileType {
Component = 'Component',
Module = 'Module',
Css = 'CSS',
Html = 'Html'
}
export interface LoadRemoteFileOptions {
remoteEntry: string;
remoteName: string;
exposedFile: string;
exposeFileType: FileType;
}
export class MfeUtil {
// holds list of loaded script
private fileMap: Record<string, boolean> = {};
findExposedModule = async <T>(uniqueName: string, exposedFile: string): Promise<T | undefined> => {
let Module: T | undefined;
// Initializes the shared scope. Fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__('default');
const container: Container = (window as any)[uniqueName]; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const factory = await container.get(exposedFile);
Module = factory();
return Module
}
public loadRemoteFile = async (loadRemoteModuleOptions: LoadRemoteFileOptions): Promise<any> => {
await this.loadRemoteEntry(loadRemoteModuleOptions.remoteEntry);
return await this.findExposedModule<any>(
loadRemoteModuleOptions.remoteName,
loadRemoteModuleOptions.exposedFile
);
}
private loadRemoteEntry = async (remoteEntry: string): Promise<void> => {
return new Promise<void>((resolve, reject) => {
if (this.fileMap[remoteEntry]) {
resolve();
return;
}
const script = document.createElement("script");
script.src = remoteEntry;
script.onerror = (error: string | Event) => {
console.error(error, 'unable to load remote entry');
reject();
}
script.onload = () => {
this.fileMap[remoteEntry] = true;
resolve(); // window is the global namespace
};
document.body.append(script);
});
}
}
mfe-utils.ts
in the public-api.ts
file./utils/src/public-api.ts
and add the following :
// ....
export * from './lib/mfe-util'
package.json
file to include a build script for the new lib:/package.json
"scripts": {
// .....
"build:util-lib": "ng build utils",
// .....
},
app1-home
src/app/app.module.ts
app2-restaurant
projects/app2-restaurant/src/app/app.module.ts
app3-orders
projects/app3-orders/src/app/app.module.ts
@NgModule({
imports: [
// ...
UtilsModule,
],
// ...
})
app.component.html
to use the Shared Header.
Project 1 app1-home
src/app/app.component.html
app2-restaurant
projects/app2-restaurant/src/app/app.component.html
app3-orders
projects/app3-orders/src/app/app.component.html
<app-header></app-header>
<router-outlet></router-outlet>
app1-home
src/app/app-routing.module.ts
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {FileType, MfeUtil} from "utils";
export const mef = new MfeUtil();
const routes: Routes = [
{
path: '',
loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
},
{
path: 'restaurants',
loadChildren: () => mef.loadRemoteFile({
remoteName: "restaurant",
remoteEntry: `http://localhost:4204/remoteRestaurant.js`,
exposedFile: "RestaurantModule",
exposeFileType: FileType.Module
}).then((m) => m.RestaurantModule),
},
{
path: 'order',
loadChildren: () => mef.loadRemoteFile({
remoteName: "orders",
remoteEntry: `http://localhost:4205/remoteOrders.js`,
exposedFile: "OrderModule",
exposeFileType: FileType.Module
}).then((m) => m.OrderModule),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
scrollPositionRestoration: "enabled"
})],
exports: [RouterModule]
})
export class AppRoutingModule {
}
app2-restaurant
topLevelAwait
prop in your webpack.config.ts
file, which you've already done. See above for steps./tsconfig.json
{
// .....
"compilerOptions": {
// .....
"target": "es2017",
"module": "esnext",
// .....
"resolveJsonModule": true,
"esModuleInterop": true
},
// ....
}
projects/app2-restaurant/src/app/app-routing.module.ts
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {FileType, MfeUtil} from "utils";
export const mfe = new MfeUtil();
const routes: Routes = [
{
path: 'restaurants',
loadChildren: () => import('./restaurant/restaurant.module').then(m => m.RestaurantModule),
},
{
path: '',
component: await mfe.loadRemoteFile({
remoteName: 'home',
remoteEntry: `http://localhost:4203/remoteHome.js`,
exposedFile: "HomeComponent",
exposeFileType: FileType.Component,
}).then((m) => m.HomeComponent),
},
{
path: 'order',
loadChildren: () => mfe.loadRemoteFile({
remoteName: "orders",
remoteEntry: `http://localhost:4205/remoteOrders.js`,
exposedFile: "OrderModule",
exposeFileType: FileType.Module
}).then((m) => m.OrderModule),
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}
/app2-restaurant/src/app/restaurant/restaurant.module.ts
app3-orders
OrderComponent
. Here are the steps needed to achieve this:app2-restaurant
ng g c restaurant/mfeOrderComponent --project=app2-restaurant
ngOnInit
function projects/app2-restaurant/src/app/restaurant/mfe-order-component/mfe-order-component.component.ts
async ngOnInit() {
const OrderComponent = await mfe.loadRemoteFile({
remoteName: "orders",
remoteEntry: `http://localhost:4205/remoteOrders.js`,
exposedFile: "OrderComponent",
exposeFileType: FileType.Component,
}).then((m) => m.OrderComponent);
}
ViewContainerRef
and ComponentFactoryResolver
from '@angular/core'
then add the following code to the constructor.order-component/mfe-order-component.component.ts
//......
constructor(private viewCRef: ViewContainerRef,
private componentFR : ComponentFactoryResolver) { }
//......
ngOnInit
function to this:order-component/mfe-order-component.component.ts
async ngOnInit() {
const OrderComponent = await mfe.loadRemoteFile({
remoteName: "orders",
remoteEntry: `http://localhost:4205/remoteOrders.js`,
exposedFile: "OrderComponent",
exposeFileType: FileType.Component,
}).then((m) => m.OrderComponent);
this.viewCRef.createComponent(
this.componentFR.resolveComponentFactory(OrderComponent)
);
}
projects/app2-restaurant/src/app/restaurant/restaurant.module.ts
to include the new component.projects/app2-restaurant/src/app/restaurant/restaurant.module.ts
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RestaurantComponent} from './restaurant.component';
import {RouterModule, Routes} from "@angular/router";
import {RestaurantDetailComponent} from './detail/detail.component';
import {FileType} from "utils";
import {mfe} from "../app-routing.module";
const routes: Routes = [
{
path: '',
component: RestaurantComponent
},
{
path: ':slug',
component: RestaurantDetailComponent
},
{
path: ':slug/order',
component: MfeOrderComponent
}
];
@NgModule({
declarations: [
RestaurantComponent,
RestaurantDetailComponent
],
imports: [
CommonModule,
RouterModule.forChild(routes)
]
})
export class RestaurantModule {
}
app3-orders
projects/app3-orders/src/app/app-routing.module.ts
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {FileType, MfeUtil} from "utils";
export const mfe = new MfeUtil();
const routes: Routes = [
{
path: '',
component: await new MfeUtil().loadRemoteFile({
remoteName: "home",
remoteEntry: `http://localhost:4203/remoteHome.js`,
exposedFile: "HomeComponent",
exposeFileType: FileType.Component,
}).then((m) => m.HomeComponent),
},
{
path: 'restaurants',
loadChildren: () => new MfeUtil().loadRemoteFile({
remoteName: "restaurant",
remoteEntry: `http://localhost:4204/remoteRestaurant.js`,
exposedFile: "RestaurantModule",
exposeFileType: FileType.Module
}).then((m) => m.RestaurantModule),
},
{
path: 'order',
loadChildren: () => import('./order/order.module').then(m => m.OrderModule),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}