owned this note
owned this note
Published
Linked with GitHub
# 【NOTE】Module Federation
> [name=呂芯萍 Aka] [time=2025 03 26]
> Mail: NT96153@cathaybk.com.tw
:::spoiler 學習歷程
* 2024/03/13
* create
* 2024/03/14
* import module/component
* 2024/04/24
* react, vue in angular root
* 2025/02/11
* Module Federation in single-spa
:::
:::spoiler 參考資料
* 主要
> [The Microfrontend Revolution: Module Federation with Angular|angulararchitects](https://www.angulararchitects.io/en/blog/the-microfrontend-revolution-part-2-module-federation-with-angular/)
> [Module Federation with Angular’s Standalone Components|angulararchitects](https://www.angulararchitects.io/en/blog/module-federation-with-angulars-standalone-components/)
> [Angular, React, and Vue in 2023 - all in one app using Webpack 5 Module Federation](https://www.darklimeteam.com/articles/blog/angular-react-vue-2023)
> [single-spa import Module Federation | single-spa](https://single-spa.js.org/docs/recommended-setup/#module-federation)
* 次要
> [Implementing MicroFrontends in Angular 15](https://medium.com/@gurunadhpukkalla/implementing-microfrontends-in-angular-15-35c4d2307e36)
> [Solve: Container initialization failed as it has already been initialized with a different share scope](https://juejin.cn/post/7309678898027233334)
:::
## What's new
* Module Federation 19+ 有三種可選建置方式
* Module Federation with webpack (classic)
* Module Federation with rsbuild (experimental nextgen); no Angular 20 support yet!
* Native Federation with esbuild (bundler-agnostic)
* 自動改用套件 [@angular-architects/native-federation](https://www.npmjs.com/package/@angular-architects/native-federation)
## Host
### Angular host
* use Package: @angular-architects/module-federation
> Angular CLI: 17.2.3
> Node: 20.9.0
> Package Manager: npm 10.1.0
> OS: win32 x64
> Angular: 17.2.4
#### 環境建置
* 既有專案
* 加入套件 @angular-architects/module-federation
:::warning
* 根據 Angular 版本下載[對應版本](https://www.npmjs.com/package/@angular-architects/module-federation?activeTab=readme)
* Angular 12: @angular-architects/module-federation: ^12.0.0
* Angular 14: @angular-architects/module-federation: ^14.3.0
* Angular 16: @angular-architects/module-federation: ^16.0.0
* Angular 17: @angular-architects/module-federation: ^17.0.0
* Angular 18: @angular-architects/module-federation: ^18.0.0
:::
```ng add @angular-architects/module-federation@<version> --project <host-project-name> --type host --port <host-port>```
:::warning
遇到「Unable to install package」,手動將「@angular-architects/module-federation@<version\>」加到 package.json 後,再執行上述指令。
:::
* 新專案
* 建置 root 專案
```
ng new angular-mfe-demo-root --create-application="false"
```
```
cd angular-mfe-demo-root
ng g application root
```
* SSR 應選「No」

:::warning
另行建置 project,是因為套件 @angular-architects/module-federation 需設定 project name
:::
* 建立 main 頁面,並加進路由
```
ng g c pages/main
```



* 執行確認專案有建置成功,且元件有綁上路由
```
ng serve root
```
http://localhost:4200/

* 加入套件 @angular-architects/module-federation
:::warning
* 根據 Angular 版本下載[對應版本](https://www.npmjs.com/package/@angular-architects/module-federation?activeTab=readme)
* Angular 12: @angular-architects/module-federation: ^12.0.0
* Angular 14: @angular-architects/module-federation: ^14.3.0
* Angular 16: @angular-architects/module-federation: ^16.0.0
* Angular 17: @angular-architects/module-federation: ^17.0.0
* Angular 18: @angular-architects/module-federation: ^18.0.0
:::
```
cd angular-mfe-demo-root
ng add @angular-architects/module-federation@17 --project root --port 4200 --type host
```

#### Import
* Module
```typescript
// src/decl.d.ts
declare module 'layout/*';
```
```typescript
loadChildren: () => import('layout/Module').then((m) => m.LayoutModule)
```
* Module(Dynamic)
```typescript
loadComponent: () =>
loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
exposedModule: './Module'
})
.then(m => m.LayoutModule)
```
* Component
```typescript
// src/decl.d.ts
declare module 'layout/*';
```
```html
<div #header></div>
```
```typescript
@ViewChild('header', { read: ViewContainerRef })
viewContainer!: ViewContainerRef;
async loadHeader() {
const m = await import('layout/Component/Header');
const ref = this.viewContainer.createComponent(m.HeaderComponent);
// const compInstance = ref.instance;
// compInstance.ngOnInit()
}
```
* Component(Dynamic)
```typescript
import { loadRemoteModule } from '@angular-architects/module-federation';
@ViewChild('header', { read: ViewContainerRef })
viewContainer!: ViewContainerRef;
async loadHeader() {
const m = await loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
exposedModule: './Component/Header'
});
const ref = this.viewContainer.createComponent(m.HeaderComponent);
// const compInstance = ref.instance;
// compInstance.ngOnInit()
}
```
## Remote
### Angular remote
* use Package: @angular-architects/module-federation
#### 環境建置
> Angular CLI: 17.2.3
> Node: 20.9.0
> Package Manager: npm 10.1.0
> OS: win32 x64
> Angular: 17.2.4
* 建置 layout 專案
```
ng new angular-mfe-demo-layout --create-application="false"
```
```
cd angular-mfe-demo-layout
ng g application layout
```
* SSR 應選「No」

:::warning
另行建置 project,是因為套件 @angular-architects/module-federation 需設定 project name
:::
* 建立 layout module,並建立 header 頁面,再加進路由
```
ng g m layout
ng g c layout/header
```


* 加入套件 @angular-architects/module-federation
:::warning
* 根據 Angular 版本下載[對應版本](https://www.npmjs.com/package/@angular-architects/module-federation?activeTab=readme)
* Angular 12: @angular-architects/module-federation: ^12.0.0
* Angular 14: @angular-architects/module-federation: ^14.3.0
* Angular 16: @angular-architects/module-federation: ^16.0.0
* Angular 17: @angular-architects/module-federation: ^17.0.0
* Angular 18: @angular-architects/module-federation: ^18.0.0
:::
```
cd angular-mfe-demo-layout
ng add @angular-architects/module-federation@17 --project layout --port 4201 --type remote
```

#### Export
* Module
```json
// projects/layout/webpack.config.js
exposes: {
...
'./Module': './projects/layout/src/app/layout/layout.module.ts',
},
```
* 新增 tsconfig include
```json
// projects/layout/tsconfig.app.json
{
// ...
"include": [
"src/**/*.d.ts",
"src/app/layout/layout.module.ts" // or "src/**/*.module.ts",
]
}
```
* Component
```json
// webpack.config.js
exposes: {
...
'./Component/Header': './projects/layout/src/app/layout/header/header.component.ts',
},
```

* Example
* 將子模組匯入父模組


* 新增檔案「angular-mfe-demo-root\projects\root\src\app\decl.d.ts」裝載子模組

* 新增路由導到子模組

* 新增 tsconfig include

* 執行確認是否有成功連結兩模組
```
cd angular-mfe-demo-root
ng serve root
```
```
cd angular-mfe-demo-layout
ng serve layout
```
http://localhost:4200/

http://localhost:4200/header

* 執行有錯誤可至[疑難排解](#疑難排解)
### React remote
#### 環境建置
> "react": "^18"
> "react-dom": "^18"
> "next": "14.2.2"
* 建立 mfe-demo-react 專案
```
mkdir mfe-demo-react
cd mfe-demo-react
npm init -y
```
* 加入套件
```npm install react react-dom webpack@5 webpack-cli webpack-dev-server html-webpack-plugin```
* 建立 src/index.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Module Federation App</title>
</head>
<body>
<div id="app"></div>S
</body>
</html>
```
* 建立 webpack.config.js
```padding...```
* 執行確認專案有建置成功
```npm run dev```
http://localhost:3000/

* 新增 webpack.config.js
```padding...```
#### Export
```padding...```
## Webpack Package: ModuleFederationPlugin
### Single-spa import Module Federation
> [【NOTE】Single Spa](/NNWopYUJThuaQpmuJNFjBA)
#### Angular application
* 加入套件 @angular-architects/module-federation
```npm i -D @angular-architects/module-federation@17```
:::warning
* 根據 Angular 版本下載[對應版本](https://www.npmjs.com/package/@angular-architects/module-federation?activeTab=readme)
* Angular 12: @angular-architects/module-federation: ^12.0.0
* Angular 14: @angular-architects/module-federation: ^14.3.0
* Angular 16: @angular-architects/module-federation: ^16.0.0
* Angular 17: @angular-architects/module-federation: ^17.0.0
* Angular 18: @angular-architects/module-federation: ^18.0.0
:::
* 使用 webpackMerge,修改 Angular 專案的檔案 extra-webpack.config.ts
```typescript
const singleSpaAngularWebpack =
require("single-spa-angular/lib/webpack").default;
const webpackMerge = require("webpack-merge");
const { ModuleFederationPlugin } = require("webpack").container;
const { shareAll } = require("@angular-architects/module-federation/webpack");
module.exports = (config, options) => {
const singleSpaWebpackConfig = singleSpaAngularWebpack(config, options);
return webpackMerge.default(singleSpaWebpackConfig, {
plugins: [
new ModuleFederationPlugin({
name: "angular-app-name",
filename: "remoteEntry.js",
remotes: {
modal: "layout@http://localhost:4201/remoteEntry.js",
},
shared: {
...shareAll({
singleton: true,
strictVersion: true,
requiredVersion: "auto",
eager: true, // 需開啟同步載入
}),
},
}),
],
});
};
```
:::info
* References for 'eager'
* [Webpack Troubleshooting: Shared module is not available for eager consumption](https://webpack.js.org/concepts/module-federation/#troubleshooting)
* [babel-loader version is too old](https://alexocallaghan.com/module-not-available-for-eager-consumption)
:::
* 需使用動態載入,於欲使用 Module Federation 的元件中加入
```html
<div #header></div>
```
```typescript
import { loadRemoteModule } from '@angular-architects/module-federation';
@ViewChild('header', { read: ViewContainerRef })
viewContainer!: ViewContainerRef;
async loadHeader() {
const m = await loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
exposedModule: './Component/Header'
});
const ref = this.viewContainer.createComponent(m.HeaderComponent);
// const compInstance = ref.instance;
// compInstance.ngOnInit()
}
```
## Git Submodule
* 將父子模組各自建立 git repository
* 於父模組加入子模組
```git submodule add <remote repository> <local path>```
* \<remote repository>:子模組 git repository 位址
* \<local path>:子模組存放 .git 中的路徑

* 執行完會新增檔案
* .gitmodules

* .git

* commit and push
* [使用方式/指令參考](https://blog.kennycoder.io/2020/06/14/Git-submodule-%E4%BD%BF%E7%94%A8%E6%95%99%E5%AD%B8/)
## 疑難排解
* SSR Error when ```ng serve```

* 移除 ssr 匯入(angular.json)

* 移除檔案「server.ts」、「/src/main.server.ts」、「/src/app/app.config.server.ts」
* 移除檔案匯入(tsconfig.app.json)

* Module does not exist in container

* 凡更改「webpack.config.js」皆須重啟 server
* 'ReadableByteStreamController' is referenced directly or indirectly in its own type annotation.
* ng serve 時發生,為第三方套件與 node 版本衝突
* 設定不要檢查第三方套件版本衝突
```json
// tsconfig.json
{
"angularCompilerOptions": {
"skipLibCheck": true
}
}
```
* Error: Uncaught (in promise): ScriptExternalLoadError: Loading script failed.
* single-spa child project inport Module federation error
* single-spa child project 匯入 MF 時加上 share 屬性
```json
// angular 為例
plugins: [
new ModuleFederationPlugin({
remotes: {
// ...
},
shared: {
"@angular/core": { singleton: true },
"@angular/common": { singleton: true },
"@angular/router": { singleton: true },
// ...
},
}),
],
```
* Error: application '@single-spa-demo18/main' died in status LOADING_SOURCE_CODE: Unsatisfied version 0 from 0 of shared singleton module *@angular/core (required ^18.2.0)*
```json
// angular 為例
plugins: [
new ModuleFederationPlugin({
remotes: {
// ...
},
shared: {
// 僅留錯誤訊息跳的套件,並加上 eager
"@angular/core": { eager: true },
},
}),
],
```
* 參考資料:[Module Federation Series Part 1: A Little in-depth](https://vugar-005.medium.com/module-federation-series-part-1-a-little-in-depth-258f331bc11e)
* NullInjectorError: No provider for InjectionToken DocumentToken!
* 使用第三方套件共享時發生(angular standalone 模式會發生)
* 在 Angular 18 的 Standalone 架構下,沒有 AppModule 或 BrowserModule 時,**Angular 不會自動提供 DocumentToken 或 DOCUMENT**。但很多 UI 套件(像 PrimeNG)會在內部呼叫:```inject(DOCUMENT)``` 或 ```inject(DocumentToken)```。在 micro frontend 架構(尤其是 Module Federation + Single-spa)中,每個 remote app 要自己提供這些平台 token,不然會 DI 失敗。
```typescript
import { DOCUMENT } from '@angular/common';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(),
{ provide: DOCUMENT, useValue: document } // 加上這行!
]
});
```