Astroでのモジュールフェデレーションを用いたReactおよびVueアプリケーションのシームレスな統合
Grace Collins
Solutions Engineer · Leapcell

はじめに
急速に進化するフロントエンド開発の状況において、大規模アプリケーションの構築は、しばしば複数のチームがプロジェクトの異なる部分に、異なるフレームワークを使用する可能性のある状況で作業することを伴います。このシナリオは、パフォーマンスや保守性を犠牲にすることなく、これらの異なる部分を統一されたユーザーエクスペリエンスに統合するという課題を頻繁に引き起こします。IFramesやサーバーサイドインクルードのような従来のアプローチは、通信、スタイリング、アプリケーションコンテキスト共有の点で限界があります。アイランドアーキテクチャで最適なパフォーマンスのために設計されたAstroのようなメタフレームワークの出現は、効果的な統合戦略の必要性をさらに増幅させます。この記事では、モジュールフェデレーションがこの問題に対するエレガントなソリューションをどのように提供するかを掘り下げ、Astroプロジェクト内で独立したReactおよびVueアプリケーションをシームレスに統合できるようにし、それによってより大きな柔軟性を解き放ち、真にコンポーザブルなフロントエンドアーキテクチャを育成します。
コアコンセプト解説
実装の詳細に入る前に、議論の中心となるいくつかの重要な用語を明確にしましょう。
-
モジュールフェデレーション: Webpackの機能で、JavaScriptアプリケーションが別のアプリケーションからコードを動的にロードしたり、その逆も可能にします。これにより、コードベースを異なるアプリケーション間で共有および統合できます。これらはしばしば「リモート」(モジュールを提供するアプリケーション)と「ホスト」(それらを利用するアプリケーション)と呼ばれます。主要なイノベーションは、アプリケーション全体またはコンポーネントを、実行時に共有および利用できるモジュールとして扱うことです。
-
マイクロフロントエンド: Webアプリケーションが独立したフロントエンドアプリケーションで構成されるアーキテクチャスタイルであり、それぞれが個別のチームによって保守されます。モジュールフェデレーションは、マイクロフロントエンドアーキテクチャを実現するための強力なツールです。
-
Astro: 高速性のために設計されたモダンな静的サイトビルダー/メタフレームワークです。これは「アイランドアーキテクチャ」を使用しており、最終ビルドからはほとんどのJavaScriptが削除され、インタラクティブなUIコンポーネント(アイランド)が必要なときにのみJavaScriptでハイドレーションされます。このアプローチは、通常、React、Vue、SvelteなどのさまざまなUIフレームワークで動作します。
-
ホストアプリケーション: モジュールフェデレーションのコンテキストでは、これは他のアプリケーションからモジュールを利用するアプリケーションです。この場合、Astroプロジェクトがホストとして機能します。
-
リモートアプリケーション: 他のアプリケーションに利用されるためにモジュールを公開するアプリケーションです。ここでは、独立したReactおよびVueアプリケーションがリモートとして機能します。
原理と実装
この統合の背後にあるコア原理は簡単です。独立したReactおよびVueアプリケーションを、コンポーネントを公開するモジュールフェデレーションリモートとして構成します。ホストとして機能するAstroプロジェクトは、これらの公開されたコンポーネントを利用します。AstroのさまざまなUIフレームワークをサポートする固有の機能により、モジュールフェデレーション経由でインポートされたこれらのReactおよびVueコンポーネントを、ローカルコンポーネントをレンダリングするのと同じようにレンダリングできます。
リモートアプリケーション(ReactおよびVue)の設定
まず、ReactおよびVueアプリケーションをコンポーネント公開できるように準備しましょう。Webpack 5のモジュールフェデレーションプラグインを使用します。
Reactリモートアプリケーションの例
Counter
コンポーネントを公開したいシンプルなReactアプリがあると仮定します。
react-remote/webpack.config.js
:
const HtmlWebPackPlugin = require('html-webpack-plugin'); const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); const deps = require('./package.json').dependencies; module.exports = { output: { publicPath: 'http://localhost:3001/', // Astroがリモートを見つけるためにこれが重要です }, resolve: { extensions: ['.jsx', '.js', '.json'], }, devServer: { port: 3001, }, module: { rules: [ { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/, options: { presets: ['@babel/preset-react'], }, }, ], }, plugins: [ new ModuleFederationPlugin({ name: 'react_remote', filename: 'remoteEntry.js', exposes: { './CounterApp': './src/CounterApp', // メインコンポーネントを公開します }, shared: { ...deps, react: { singleton: true, requiredVersion: deps.react, }, 'react-dom': { singleton: true, requiredVersion: deps['react-dom'], }, }, }), new HtmlWebPackPlugin({ template: './public/index.html', }), ], };
react-remote/src/CounterApp.jsx
:
import React, { useState } from 'react'; const CounterApp = () => { const [count, setCount] = useState(0); return ( <div style={{ padding: '10px', border: '1px solid blue' }}> <h2>React Counter Component</h2> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }; export default CounterApp;
Vueリモートアプリケーションの例
同様に、VueアプリはGreeting
コンポーネントを公開できます。
vue-remote/webpack.config.js
:
const { VueLoaderPlugin } = require('vue-loader'); const HtmlWebPackPlugin = require('html-webpack-plugin'); const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); const deps = require('./package.json').dependencies; module.exports = { output: { publicPath: 'http://localhost:3002/', // Astroにとって重要 }, resolve: { extensions: ['.vue', '.js', '.json'], }, devServer: { port: 3002, }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', }, { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/, }, { test: /\.css$/, use: ['style-loader', 'css-loader'], }, ], }, plugins: [ new VueLoaderPlugin(), new ModuleFederationPlugin({ name: 'vue_remote', filename: 'remoteEntry.js', exposes: { './GreetingApp': './src/GreetingApp.vue', // メインコンポーネントを公開します }, shared: { ...deps, vue: { singleton: true, requiredVersion: deps.vue, }, }, }), new HtmlWebPackPlugin({ template: './public/index.html', }), ], };
vue-remote/src/GreetingApp.vue
:
<template> <div style="padding: 10px; border: 1px solid green;"> <h2>Vue Greeting Component</h2> <p>Hello from Vue!</p> <button @click="greet">Click Me</button> </div> </template> <script> export default { methods: { greet() { alert('Vue says hi!'); } } } </script> <style scoped> /* このコンポーネント固有のスコープ付きスタイル */ </style>
両方のリモートアプリケーションが実行されていることを確認してください(例:各ディレクトリでnpm start
)。
Astro(ホストアプリケーション)での利用
次に、Astroプロジェクトを設定して、これらのリモートを利用できるようにしましょう。Astroのsrc/pages
またはsrc/components
は、リモートコンポーネントをインポートしてレンダリングする場所になります。Astroは通常Viteを使用するため、Viteにモジュールフェデレーションを処理する方法を伝える必要があります。vite-plugin-federation
を使用します。
astro-host/vite.config.ts
:
import { defineConfig } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; // 他のフレームワークの例、MF自体には厳密には必要ありません import react from '@vitejs/plugin-react'; import vue from '@vitejs/plugin-vue'; import federation from '@module-federation/vite'; // モジュールフェデレーションのプラグイン export default defineConfig({ plugins: [ react(), vue(), federation({ name: 'astro_host', remotes: { react_remote: 'http://localhost:3001/remoteEntry.js', vue_remote: 'http://localhost:3002/remoteEntry.js', }, shared: ['react', 'react-dom', 'vue'], // 共有依存関係を確保します。Astroはこれらを直接使用しないかもしれませんが、リモートアプリケーションには重要です。 }), ], });
astro-host/astro.config.mjs
:
import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; import vue from '@astrojs/vue'; // https://astro.build/config export default defineConfig({ integrations: [react(), vue()], vite: { plugins: [ // vite.config.ts のフェデレーションプラグインが正しくリンクされていることを確認してください // または、すべてのvite設定にvite.config.tsを使用しない場合は、ここでフェデレーション設定を複製します // 簡単のため、Astroがvite.config.tsを拾うと仮定します。 // そうでない場合、ここで手動で`federation`をインポートして設定する必要があるかもしれません。 ], }, });
次に、これらのフェデレーションコンポーネントをインポートしてレンダリングするAstroコンポーネントを作成しましょう。
astro-host/src/pages/index.astro
:
--- import Layout from '../layouts/Layout.astro'; import ReactCounter from '../components/ReactCounter.astro'; import VueGreeting from '../components/VueGreeting.astro'; --- <Layout title="Module Federation in Astro"> <main> <h1>Welcome to Astro with Module Federation!</h1> <p>This page integrates components from independent React and Vue applications.</p> <div style="margin-top: 20px;"> <h2>React Component Hosted in Astro</h2> <ReactCounter client:load /> {/* client:load はReactコンポーネントをハイドレーションします */} </div> <div style="margin-top: 20px;"> <h2>Vue Component Hosted in Astro</h2> <VueGreeting client:load /> {/* client:load はVueコンポーネントをハイドレーションします */} </div> </main> </Layout> <style> main { margin: auto; padding: 1.5rem; max-width: 60ch; } h1 { font-size: 3rem; font-weight: 800; margin: 0; } </style>
astro-host/src/components/ReactCounter.astro
:
--- import { createRemoteComponent } from '../lib/module-federation'; const RemoteCounter = await createRemoteComponent('react_remote', './CounterApp'); --- <RemoteCounter client:load />
astro-host/src/components/VueGreeting.astro
:
--- import { createRemoteComponent } from '../lib/module-federation'; const RemoteGreeting = await createRemoteComponent('vue_remote', './GreetingApp'); --- <RemoteGreeting client:load />
astro-host/src/lib/module-federation.ts
: (リモートコンポーネントを動的にロードするためのユーティリティ)
import { createComponent } from '@module-federation/sdk'; import type { AstroComponentFactory } from 'astro/runtime/server/index.js'; export async function createRemoteComponent(remoteName: string, moduleName: string): Promise<AstroComponentFactory> { const Component = await createComponent({ remote: remoteName, module: moduleName, scope: remoteName, // スコープは通常リモート名と一致します // 重要: Astroの場合、リモートコンポーネントがクライアントサイドアイランドとして扱われるようにする必要があります // フレームワークアダプター(例: @astrojs/react、@astrojs/vue)が実際のハイドレーションを処理します }); // この変換は、@module-federation/sdkがAstroとどのように統合されるかに基づいて異なる場合があります。 // 目標は、Astro互換のコンポーネントを取得することです。 // ほとんどのフレームワークでは、インポートされたコンポーネントをclient:ディレクティブで使用するために直接ラップできます。 return Component as AstroComponentFactory; }
注意: createRemoteComponent
の実装は、@module-federation/sdk
とAstroの内部コンポーネント処理の正確なバージョンと使用法に基づいて調整が必要になる場合があります。基本的な考え方は、リモートモジュールを動的にインポートし、それをAstro内でレンダリングすることです。client:load
ディレクティブは、Astroがこれらのコンポーネントをクライアントサイドでハイドレーションするために重要であり、それらをインタラクティブにします。
アプリケーションシナリオ
- マイクロフロントエンドアーキテクチャ: 各独立したアプリケーション(React、Vue)は、開発、デプロイ、および別々のチームによって所有できます。Astroは、それらを単一の製品に構成するオーケストレーションレイヤーとして機能します。
- 段階的な移行: 大規模アプリケーションをあるフレームワークから別のフレームワークに移行する際に、アプリケーションの部分を新しいマイクロフロントエンドとして段階的に書き直し、既存のシェル(この場合はAstro)に統合できます。
- 共有コンポーネントライブラリ: チームは共通のUIコンポーネント、あるいはビジネスロジックモジュールをフェデレーションモジュールとして公開でき、他のリモートまたはホストアプリケーションがそれらを利用して、一貫性と再利用性を確保できます。
- ベストオブブリードの活用: 異なるチームが特定の機能に最適なフレームワークを選択できるようにし、プロジェクト全体にモノリシックなフレームワークの決定を強制しません。
結論
Astroプロジェクト内で複数の独立したReactおよびVueアプリケーションをモジュールフェデレーションを使用して統合することは、モダンなフロントエンド開発のための強力で柔軟なアプローチを提供します。実行時に動的なコード共有を可能にすることで、モジュールフェデレーションはマイクロフロントエンドアーキテクチャを強化し、段階的な移行を容易にし、さまざまなフレームワークエコシステム全体でのコード再利用を促進します。この戦略は、開発者エクスペリエンスを向上させ、チームの自律性を高めるだけでなく、Astroの高速化の利点とクロスフレームワークコンポーネント統合の汎用性を活用することで、堅牢でパフォーマンスが高く、保守可能なWebアプリケーションを提供します。フロントエンド開発の未来はますますコンポーザブルになっており、モジュールフェデレーションはこのパラダイムの礎石です。