コンテンツにスキップ

HTML/マイクロフロントエンド

出典: フリー教科書『ウィキブックス(Wikibooks)』

概要

[編集]

マイクロフロントエンドは、マイクロサービスの概念をフロントエンド開発に適用したアーキテクチャパターンである。従来の一枚岩的なフロントエンドアプリケーションを、より小さな独立した部分に分割し、それぞれを異なるチームが独立して開発・デプロイできるようにする手法である。このアプローチにより、大規模なフロントエンド開発における複雑性の管理と、チームの自律性の向上を実現できる。

アーキテクチャの基本原則

[編集]

マイクロフロントエンドの核となる考え方は、フロントエンドアプリケーションを独立して進化可能な単位に分割することである。各ユニットは特定のビジネス領域やユーザージャーニーに焦点を当て、他のユニットとの依存関係を最小限に抑えながら開発される。これにより、大規模な組織でも効率的にフロントエンド開発を進めることが可能になる。

技術スタックの選択においても柔軟性が確保され、各チームは担当する機能に最適なツールやフレームワークを選択できる。ただし、この自由度は組織全体のガバナンスとバランスを取る必要がある。

実装アプローチ

[編集]

ビルドタイム統合

[編集]

ビルドタイム統合では、各マイクロフロントエンドをnpmパッケージとして公開し、メインアプリケーションの依存関係として組み込む。

package.json
{
  "dependencies": {
    "@company/team-a-app": "1.0.0",
    "@company/team-b-app": "1.0.0",
    "@company/shared-components": "1.0.0"
  }
}
main-app.js
import TeamAApp from '@company/team-a-app';
import TeamBApp from '@company/team-b-app';

function MainApplication() {
  return (
    <div>
      <TeamAApp />
      <TeamBApp />
    </div>
  );
}

ランタイム統合

[編集]

ランタイム統合では、各マイクロフロントエンドを独立したアプリケーションとしてデプロイし、実行時に動的に統合する。

container-app.js
class ContainerApp {
  async loadMicroFrontend(name, url) {
    const script = document.createElement('script');
    script.src = url;
    script.type = 'module';
    
    script.onload = () => {
      window[name].mount(`#${name}-container`);
    };
    
    document.head.appendChild(script);
  }
  
  initialize() {
    this.loadMicroFrontend('teamA', 'https://team-a.example.com/app.js');
    this.loadMicroFrontend('teamB', 'https://team-b.example.com/app.js');
  }
}

Module Federation

[編集]

Webpack 5のModule Federationを使用した実装例を示す。

webpack.config.js (container)
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'container',
      remotes: {
        teamA: 'teamA@http://localhost:3001/remoteEntry.js',
        teamB: 'teamB@http://localhost:3002/remoteEntry.js'
      },
      shared: ['react', 'react-dom']
    })
  ]
};

// webpack.config.js (teamA)
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'teamA',
      filename: 'remoteEntry.js',
      exposes: {
        './App': './src/App'
      },
      shared: ['react', 'react-dom']
    })
  ]
};

アプリケーション間通信

[編集]

マイクロフロントエンド間の通信は、カスタムイベントやメッセージングシステムを通じて実現する。

// イベントバスの実装
class EventBus {
  constructor() {
    this.events = {};
  }

  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    
    return () => {
      this.events[event] = this.events[event]
        .filter(cb => cb !== callback);
    };
  }

  publish(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
}

const eventBus = new EventBus();

// TeamAアプリケーションでの使用
eventBus.publish('userSelected', { userId: '123' });

// TeamBアプリケーションでの使用
eventBus.subscribe('userSelected', (data) => {
  console.log('Selected user:', data.userId);
});

状態管理

[編集]

マイクロフロントエンドにおける状態管理は、各アプリケーションの独立性を保ちながら、必要な情報を共有する必要がある。

// グローバル状態の実装
class GlobalState {
  constructor() {
    this.state = {};
    this.listeners = new Set();
  }

  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.notifyListeners();
  }

  getState() {
    return this.state;
  }

  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  notifyListeners() {
    this.listeners.forEach(listener => listener(this.state));
  }
}

const globalState = new GlobalState();

// 使用例
globalState.setState({ user: { id: '123', name: 'John' } });
const unsubscribe = globalState.subscribe(state => {
  console.log('State updated:', state);
});

エラーハンドリングと障害分離

[編集]

マイクロフロントエンドでは、一部のアプリケーションの障害が全体に影響を及ぼさないよう、適切なエラーバウンダリーを実装する必要がある。

class MicroFrontendErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Micro Frontend Error:', error, errorInfo);
    // エラー監視サービスへの通知など
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>アプリケーションの読み込みに失敗しました</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            再試行
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

パフォーマンス最適化

[編集]

マイクロフロントエンドのパフォーマンスを最適化するためには、共有依存関係の管理、リソースの遅延読み込み、適切なキャッシング戦略が重要である。

// 遅延ローディングの実装例
const loadComponent = async (url) => {
  const script = document.createElement('script');
  script.src = url;
  
  return new Promise((resolve, reject) => {
    script.onload = () => resolve();
    script.onerror = () => reject(new Error(`Failed to load ${url}`));
    document.head.appendChild(script);
  });
};

// キャッシュの実装
class ResourceCache {
  constructor() {
    this.cache = new Map();
  }

  async get(key, fetcher) {
    if (!this.cache.has(key)) {
      const resource = await fetcher();
      this.cache.set(key, resource);
    }
    return this.cache.get(key);
  }

  invalidate(key) {
    this.cache.delete(key);
  }
}

まとめ

[編集]

マイクロフロントエンドは、大規模なフロントエンド開発を管理可能な単位に分割し、チームの自律性と開発効率を向上させる効果的なアプローチである。適切な実装戦略と運用プラクティスを採用することで、スケーラブルで保守性の高いフロントエンドアーキテクチャを実現できる。