AngularでModule Federationをやってみよう！！！

# Module Federationとは
> 独立してビルド、デプロイされた複数のプロジェクト間でコードを共有できるようにするWebpack5の機能です。中央の「連合体」モジュールを設定し、他のモジュールが接続できるハブとして機能させることができます。連携モジュールは、そのコードの特定の部分を他の連携モジュールに公開し、そのモジュールを自分のプロジェクトの一部であるかのようにインポートして使用することができます。これにより、複数のプロジェクト間で共通のコードを簡単に共有することができ、各プロジェクトでそのコードを複製する必要はありません。by ChatGPT

つまり、独立した別のアプリから、独立したまた別のアプリへ、特定のコンポーネントを公開し、そして取得することが出来るというハチャメチャクールな新技術というわけです！

実際に仕事でModule Federationを使用させていだいたので、最も簡易的な実装を解説できればと思います。
本来ではModule Federationと言うくらいなので、モジュールの公開が一般的なケースなのですが、
今回は比較的あまり解説されていないコンポーネントの公開を取り扱いたいと思います。

## 要件
Angular 15 (提供側・取得側ともに同じバージョンが好ましい）
@angular-architects/module-federation 15.0.3

## 手順
1. 提供側・取得側のAngularプロジェクトを作成します。
2. 双方のプロジェクトにng add @angular-architects/module-federation@^15.0.3 （ng addが設定してくれるので便利）
3. Webpack.config.jsが自動的に作成されます。

ここで提供側のwebpack.config.jsをいじります。
```javascript
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:`の部分は自分で決めて良いです）
```javascript
        name: "remote",
        filename: "remoteEntry.js",
        exposes: {
            './Component': './src/app/remote/remote.component.ts',
        }
```
これで提供側の設定は終了です。簡単！

次に取得側のwebpack.config.jsをいじります。
```javascript
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)を設定するだけ。
```javascript
        remotes: {
            "remote": "remote@http://localhost:4200/remoteEntry.js",
        },
```

最後に実際にコンポーネントを取得し、レンダリングしましょう。
```typescript
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)
          );
        });
    }
}
```
提供されたコンポーネント名を指定すればそのまま取り出せる！素晴らしい！
何より良いのは提供側のコンポーネントの変更が取得側に即座に反映されること。
これは一つのアプリケーションを作りながら同時に別のアプリケーションの機能の開発を可能にする！
こんな簡単に並列開発が出来るのだ！最高！

以上！

質問等、アドバイスなどあればどしどしでおねがいします！では！