🏗️

マイクロフロントエンドのサンプルコードをReactで書いてみました

2024/12/14に公開

はじめに

以前に以下のような記事を書きました。

『Micro Frontends』という記事を読んだのでまとめる

本稿ではマイクロフロントエンドを用いたReactのサンプルコードを書いてみたのでその諸所についてまとめます。

全体のソースコードは以下にあります。

https://github.com/neko3cs/neko3cs-lab/tree/main/src/sample-microfrontend-with-react

マイクロフロントエンドとは

詳細は上記の記事を読んでいただけると幸いです。

マイクロフロントエンドとは、 単体で実行可能な、サイトから切り出された特定のUI領域 であり、Webフロントエンドの新たなアーキテクチャになります。

マイクロフロントエンドは以下のような特徴を持っています。この特徴により、マイクロフロントエンドは再利用性と変更容易性を高めます。

インクリメンタルアップグレード

任意のコンポーネント単位での独立した実行を可能とするため、他のコンポーネントが依存するフレームワークやライブラリの影響を受けません。

シンプルで分離されたコードベース

コンポーネントが独立した実行を可能とすることにより、一般的なモノリシックコードと比べ格段にコードサイズが小さくなります。

独立したデプロイ

コンテキスト毎に実行単位を分けることで、他のコンポーネントとは独立したリリースを可能とします。

自立したチーム

コードとリリースサイクルが分離されることで、チームもコンテキスト単位で分離できます。

サンプルアプリケーションの構成

以下がサンプルアプリケーションのフォルダー構造になります。

フォルダ構造

重要なのは以下のファイルになります。

  1. micro-frontend/vite.config.ts
  2. micro-frontend/tsconfig.json
  3. micro-frontend/src/main.tsx
  4. app-shell/src/hooks/useMicroFrontend.ts
  5. app-shell/src/App.tsx

マイクロフロントエンドとして実装するには

先に取り上げた5つのファイルを設定・実装することで実現できました。

micro-frontend/vite.config.ts

マイクロフロントエンド側のvite.config.tsではライブラリとしてビルドされるように定義しています。

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  build: {
    lib: {
      entry: './src/main.tsx',
      name: 'MicroFrontend',
      fileName: 'micro-frontend',
      formats: ['es'],
    },
  },
  define: {
    // Node環境変数をエミュレート
    'process.env.NODE_ENV': JSON.stringify('production'),
  }
})

micro-frontend/tsconfig.json

ライブラリとして登録しましたが、次の main.tsx のために、 libDOM を追加しておきます。

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": [
      "ESNext",
      "DOM",
    ],
    /// 以下省略
}

micro-frontend/src/main.tsx

マイクロフロントエンドをマウント/アンマウントする関数をマイクロフロントエンド側で用意してあげます。

この時の関数名はアプリケーション全体で統一されていると、マイクロフロントエンド側で呼ぶ際に扱いやすいかなと思います。

import ReactDOM from 'react-dom/client'
import MicroFrontrendComponent from "./MicroFrontrendComponent";

const roots: Record<string, ReactDOM.Root> = {};

export const mount = (containerId: string) => {
  const container = document.getElementById(containerId);
  if (container) {
    const root = ReactDOM.createRoot(container);
    root.render(<MicroFrontrendComponent />);
    roots[containerId] = root;
  }
};

export const unmount = (containerId: string) => {
  const root = roots[containerId];
  if (root) {
    root.unmount();
    delete roots[containerId];
  }
};

app-shell/src/hooks/useMicroFrontend.ts

マイクロフロントエンドを取り扱うフックを用意しています。

URLを渡したらコンポーネントを返してくれます。

import { useState, useEffect } from 'react';

interface MicroFrontendModule {
  mount: (containerId: string) => void;
  unmount: (containerId: string) => void;
}

export const useMicroFrontend = (url: string) => {
  const [microFrontend, setMicroFrontend] = useState<MicroFrontendModule | null>(null);
  useEffect(() => {
    let isMounted = true;
    const loadMicroFrontendComponent = async () => {
      try {
        const { mount, unmount } = await import(/* @vite-ignore */ url);
        if (isMounted) {
          setMicroFrontend({ mount: mount, unmount: unmount });
        }
      } catch (err) {
        console.error(`Failed to load MicroFrontend from ${url}`, err);
      }
    };

    loadMicroFrontendComponent();

    return () => {
      isMounted = false;
    };
  }, [url]);

  return microFrontend;
};

app-shell/src/App.tsx

実際にマイクロフロントエンドを利用する実装です。

useMicroFrontend でコンポーネントを取得できます。

取得が完了したら指定のidのタグへマウントし、利用する側のコンポーネントが破棄されたらマイクロフロントエンドもアンマウントするようにしてます。

import React, { useEffect } from 'react';
import styled from 'styled-components';
import { DefaultButton, Spinner, SpinnerSize, Stack, Text } from '@fluentui/react';
import { useMicroFrontend } from './hooks/useMicroFrontend';

// styled-componentの定義は省略

const App: React.FC = () => {
  const component = useMicroFrontend('http://localhost:5050/micro-frontend.js');

  useEffect(() => {
    if (component) {
      component.mount('micro-frontend');
    }

    return () => {
      if (component) {
        component.unmount('micro-frontend')
      }
    };
  });

  return (
    <Layout>
      <Header style={{ color: 'white' }}>
        ホストアプリケーション
      </Header>

      <Sidebar style={{ width: 200 }}>
        <Text variant="large">
          Menu
        </Text>
        <Stack styles={{ root: { width: 200, margin: '0 auto', padding: 10 } }}>
          <DefaultButton text="Page1" styles={{ root: { width: 180, margin: '10px 0' } }} />
          <DefaultButton text="Page2" styles={{ root: { width: 180, margin: '10px 0' } }} />
        </Stack>
      </Sidebar>

      <ContentArea>
        {component ? (
          <div id='micro-frontend' />
        ) : (
          <Spinner size={SpinnerSize.large} label='Loading...' />
        )}
      </ContentArea>
    </Layout>
  );
};

export default App;

実行

まず、バージョンを確認します。

app-shell側はver.17.0.2が使われています。

app-shellのreactバージョン

micro-frontend側はver.18.3.1が使われています。

micro-frontendのreactバージョン

通常であれば、異なったバージョンのReactが存在することはないかと思います。

実行します。

app-shellの実行

micro-frontendの実行

次のように、異なったバージョンのReactコンポーネントを同一画面に表示することができました。

実行結果

まとめ

今回は Micro Frontends - martinFowler.com にて説明されている、Micro Frontendsについて実際にReactを用いて実装してみました。実際に異なったバージョンのReactコンポーネントを同一アプリ内で表示することに成功しました。

マイクロフロントエンドが必要になるのはある程度大規模になったメガベンチャー級のサービスで機能ごとにチームが分かれるべき規模になった状況ではないかと思っています。ですが、そのメソッドを知っておくことは有用だと思います。

また、このサンプルコードにもいくつか課題を感じている点があります。たとえば、マイクロフロントエンド側でNode環境変数をエミュレートするダミーの変数を定義しています。これは私がうまく解消する実装方法が思いつかなかったためなので、もっと良い方法があるかもしれません。よりよい実装方法があれば、教えていただければ幸いです。

以上です。

Discussion