Module Federationとは
独立してビルド、デプロイされた複数のプロジェクト間でコードを共有できるようにするWebpack5の機能です。中央の「連合体」モジュールを設定し、他のモジュールが接続できるハブとして機能させることができます。連携モジュールは、そのコードの特定の部分を他の連携モジュールに公開し、そのモジュールを自分のプロジェクトの一部であるかのようにインポートして使用することができます。これにより、複数のプロジェクト間で共通のコードを簡単に共有することができ、各プロジェクトでそのコードを複製する必要はありません。by ChatGPT
つまり、独立した別のアプリから、独立したまた別のアプリへ、特定のコンポーネントを公開し、そして取得することが出来るというハチャメチャクールな新技術というわけです!
実際に仕事でModule Federationを使用させていだいたので、最も簡易的な実装を解説できればと思います。
本来ではModule Federationと言うくらいなので、モジュールの公開が一般的なケースなのですが、
今回は比較的あまり解説されていないコンポーネントの公開を取り扱いたいと思います。
要件
Angular 15 (提供側・取得側ともに同じバージョンが好ましい)
@angular-architects/module-federation 15.0.3
手順
- 提供側・取得側のAngularプロジェクトを作成します。
- 双方のプロジェクトにng add @angular-architects/module-federation@^15.0.3 (ng addが設定してくれるので便利)
- Webpack.config.jsが自動的に作成されます。
ここで提供側のwebpack.config.jsをいじります。
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const mf = require("@angular-architects/module-federation/webpack");
const path = require("path");
const share = mf.share;
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
path.join(__dirname, 'tsconfig.json'),
[/* mapped paths to share */]);
module.exports = {
output: {
uniqueName: "remote",
publicPath: "auto",
scriptType: 'text/javascript'
},
optimization: {
runtimeChunk: false
},
resolve: {
alias: {
...sharedMappings.getAliases(),
}
},
experiments: {
outputModule: true
},
plugins: [
new ModuleFederationPlugin({
//library: { type: "module" },
// For remotes (please adjust)
name: "remote",
filename: "remoteEntry.js",
exposes: {
'./Component': './src/app/remote/remote.component.ts',
},
// For hosts (please adjust)
// remotes: {
// "mfe1": "http://localhost:3000/remoteEntry.js",
// },
shared: share({
"@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
...sharedMappings.getDescriptors()
})
}),
sharedMappings.getPlugin()
],
};
ポイントはscriptType: 'text/javascript'
の追加とlibrary: { type: "module" }
のコメントアウト。
あとは下記のように、提供したいコンポーネントのパスを設定してあげるだけ!(./Component:
の部分は自分で決めて良いです)
name: "remote",
filename: "remoteEntry.js",
exposes: {
'./Component': './src/app/remote/remote.component.ts',
}
これで提供側の設定は終了です。簡単!
次に取得側のwebpack.config.jsをいじります。
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const mf = require("@angular-architects/module-federation/webpack");
const path = require("path");
const share = mf.share;
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
path.join(__dirname, 'tsconfig.json'),
[/* mapped paths to share */]);
module.exports = {
output: {
uniqueName: "host",
publicPath: "auto",
scriptType: 'text/javascript'
},
optimization: {
runtimeChunk: false
},
resolve: {
alias: {
...sharedMappings.getAliases(),
}
},
experiments: {
outputModule: true
},
plugins: [
new ModuleFederationPlugin({
//library: { type: "module" },
// For remotes (please adjust)
// name: "host",
// filename: "remoteEntry.js",
// exposes: {
// './Component': './/src/app/app.component.ts',
// },
// For hosts (please adjust)
remotes: {
"remote": "remote@http://localhost:4200/remoteEntry.js",
},
shared: share({
"@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
...sharedMappings.getDescriptors()
})
}),
sharedMappings.getPlugin()
],
};
提供側と同じように、scriptType: 'text/javascript'
の追加とlibrary: { type: "module" }
のコメントアウト。
今度は取得する提供名とアドレス(remoteEntry.js)を設定するだけ。
remotes: {
"remote": "remote@http://localhost:4200/remoteEntry.js",
},
最後に実際にコンポーネントを取得し、レンダリングしましょう。
import { loadRemoteModule } from '@angular-architects/module-federation';
import {
Component,
ComponentFactoryResolver,
OnInit,
ViewContainerRef,
} from '@angular/core';
import { RemoteCompDirective } from './remote-comp.directive';
@Component({
selector: 'app-root',
template: ``,
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private viewContainerRef: ViewContainerRef
) {}
ngOnInit(): void {
loadRemoteModule({
remoteEntry: 'http://localhost:4200/remoteEntry.js',
remoteName: 'remote',
exposedModule: './Component',
}).then((m) => {
const { RemoteComponent } = m;
const componentRef = this.viewContainerRef.createComponent(
this.componentFactoryResolver.resolveComponentFactory(RemoteComponent)
);
});
}
}
提供されたコンポーネント名を指定すればそのまま取り出せる!素晴らしい!
何より良いのは提供側のコンポーネントの変更が取得側に即座に反映されること。
これは一つのアプリケーションを作りながら同時に別のアプリケーションの機能の開発を可能にする!
こんな簡単に並列開発が出来るのだ!最高!
以上!
質問等、アドバイスなどあればどしどしでおねがいします!では!