# 【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」 ![image](https://hackmd.io/_uploads/rkb9zJ1Ca.png) :::warning 另行建置 project,是因為套件 @angular-architects/module-federation 需設定 project name ::: * 建立 main 頁面,並加進路由 ``` ng g c pages/main ``` ![image](https://hackmd.io/_uploads/H1GaQ11Cp.png) ![image](https://hackmd.io/_uploads/rykAQyyCT.png) ![image](https://hackmd.io/_uploads/ryi5m1JAa.png) * 執行確認專案有建置成功,且元件有綁上路由 ``` ng serve root ``` http://localhost:4200/ ![image](https://hackmd.io/_uploads/H1sfLJ10p.png) * 加入套件 @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 ``` ![image](https://hackmd.io/_uploads/BJuAIyk0a.png) #### 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」 ![image](https://hackmd.io/_uploads/HJ2pE1JRT.png) :::warning 另行建置 project,是因為套件 @angular-architects/module-federation 需設定 project name ::: * 建立 layout module,並建立 header 頁面,再加進路由 ``` ng g m layout ng g c layout/header ``` ![image](https://hackmd.io/_uploads/BJw6a0yRp.png) ![image](https://hackmd.io/_uploads/rkO-CJJC6.png) * 加入套件 @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 ``` ![image](https://hackmd.io/_uploads/HJ2xvkJAp.png) #### 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', }, ``` ![image](https://hackmd.io/_uploads/r1Pw6flAT.png) * Example * 將子模組匯入父模組 ![image](https://hackmd.io/_uploads/BkjjdyyRp.png) ![image](https://hackmd.io/_uploads/rk_m1e106.png) * 新增檔案「angular-mfe-demo-root\projects\root\src\app\decl.d.ts」裝載子模組 ![image](https://hackmd.io/_uploads/ryLQdky06.png) * 新增路由導到子模組 ![image](https://hackmd.io/_uploads/Hym-yye06.png) * 新增 tsconfig include ![image](https://hackmd.io/_uploads/SkPjXekR6.png) * 執行確認是否有成功連結兩模組 ``` cd angular-mfe-demo-root ng serve root ``` ``` cd angular-mfe-demo-layout ng serve layout ``` http://localhost:4200/ ![image](https://hackmd.io/_uploads/Hyf2GJgAa.png) http://localhost:4200/header ![image](https://hackmd.io/_uploads/B1ykmJxCa.png) * 執行有錯誤可至[疑難排解](#疑難排解) ### 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/ ![image](https://hackmd.io/_uploads/H1vex7UbR.png) * 新增 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 中的路徑 ![image](https://hackmd.io/_uploads/rkg6nnjxR.png) * 執行完會新增檔案 * .gitmodules ![image](https://hackmd.io/_uploads/Skzg6nse0.png) * .git ![image](https://hackmd.io/_uploads/Byosp2olR.png) * 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``` ![image](https://hackmd.io/_uploads/H1hzb1x0T.png) * 移除 ssr 匯入(angular.json) ![image](https://hackmd.io/_uploads/ryyPZyg06.png) * 移除檔案「server.ts」、「/src/main.server.ts」、「/src/app/app.config.server.ts」 * 移除檔案匯入(tsconfig.app.json) ![image](https://hackmd.io/_uploads/H1G-WgxAT.png) * Module does not exist in container ![image](https://hackmd.io/_uploads/HJFRhfgCT.png) * 凡更改「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 } // 加上這行! ] }); ```