見出し画像

Vue2 と Vue3 を共存させるためのマイクロフロントエンドアプローチ (第二回) SystemJS による依存関係の動的解決~

※ 今回の記事は single-spa 導入の第 2 回となります。
マイクロフロントエンドや single-spa の概要については
第 1 回の記事をご覧ください。

はじめまして、株式会社ラキールの LaKeel DX Engine Group に所属する海老と申します。
我々のチームでは、UI コンポーネントを一元管理し、自由に組み合わせてアプリケーションを構築できるフロントエンド開発基盤「LaKeel Visual Mosaic」を開発しています。

Visual Mosaic では、Vue2 と Vue3 のコンポーネントを同一アプリケーションで動作させるためのアプローチとして single-spa というマイクロフロントエンドを実現する OSS を採用しました。

前回の記事に引き続き、single-spaを使用したマイクロフロントエンドのモジュール依存関係の解決方法について実際のプロジェクト経験に基づいてご紹介いたします。


背景

single-spa は、フロントエンドアプリケーションに複数の JavaScript マイクロフロントエンドをまとめるためのフレームワークです。
single-spa において Vue2 と Vue3 のような、異なるフレームワークを一つのアプリケーション内で動作させるには、これらのフレームワークがお互いに干渉しないようにする工夫が必要があります。

今回は、SystemJS を活用した single-spa の依存関係の解決方法を、実際のプロジェクト経験をもとにご紹介します。

当初の構成

当初、私たちは npm を使用してインポートしたモジュールを
クライアントに組み込んでバンドルするアプローチを採用していました。

この方法では、バンドルされたライブラリなどのモジュールを、 single-spa に登録されたアプリケーションに受け渡し、Vue2 や Vue3 のアプリケーションにおける依存関係の解決を行なっています。

しかし、この構成にはいくつかの課題がありました。

  • Vue2 と Vue3 という異なるライブラリが、一つのコードに混在することで、予期しない動作が発生する。

  • Vue3 アプリケーション を使用している際にも、不要な Vue2 ライブラリがロードされてしまう。

  • アプリケーションがクライアントに依存しているため、アプリケーションの更新には、クライアント側の更新が必要である。

  • アプリケーション間の独立したビルドやデプロイが難しいため、チーム間での作業が衝突するリスクがある。

これらの課題を解決するために、npm を用いた依存関係の解決から、SystemJS を用いた依存関係の解決にシフトしました。

SystemJS とは

SystemJS は、JavaScript モジュールの動的インポートと管理をサポートするライブラリです。これにより、異なるモジュールフォーマットや依存関係を効率的に扱うことが可能となります。

最初に、静的インポート動的インポートに触れてから、SystemJS について説明していきます。

静的インポートとは

静的インポートは、モジュールのインポート時に依存関係を解決し、アプリケーションの起動時にすべての必要なモジュールがロードされる方法です。

import { moduleA } from 'library';

// libraryはコンパイル時に読み込まれる
const module = moduleA();
  • 上記のコードでインポートした library モジュールはコードがコンパイルされる際に読み込まれ、moduleA の依存関係が構築されます。

SystemJS を使用した動的インポート

一方で、SystemJS を使用すると、モジュールを実際にそのモジュールが呼ばれるタイミングで読み込むことができます。

System.import('library').then(module => {
    // モジュールの使用
    const moduleInstance = module.moduleA();
});

このケースでは、library モジュールはコードが実行される際に読み込まれ、moduleA の依存関係が構築されます。
これにより、必要なときに必要なモジュールのみをロードし、初期のページロード時のパフォーマンスを向上させることができます。

Import Map の利用と SystemJS

SystemJS は Import Map という機能を提供し、これによりモジュールの名前と URL をマッピングし、コード内でのモジュール参照を簡単に管理できます。Import Map を使用することで、モジュールの URL を柔軟に変更でき、コード内の import ステートメントの参照名はそのままで済みます。
以下に具体的な使用例を示します。

Import Map の作成

Import Map は、モジュールの参照名と URL を JSON 形式で定義します。

<script type="systemjs-importmap">
{
    "imports": {
        "libraryA": "https://example.com/npm/library1.js",
        "libraryB": "https://example.com/npm/library2.js"
    }
}
</script>

この定義では、libraryA と libraryB という参照名がそれぞれ指定された URL にマッピングされます。

SystemJSを用いたモジュールのロード

SystemJS を使用して libraryA というエイリアスがhttps://example.com/npm/library1.js にマッピングされている場合
SystemJS はこの URL からモジュールを動的にロードします。

System.import('libraryA').then(module => {
    // モジュールの使用
    const moduleInstance = module.moduleA();
});

この方法では、libraryA への URL が変更された場合にも、import ステートメントを変更する必要がありません。Import Map がこの変更を透過的に処理します。

SystemJS を活用した single-spa の依存解決

それでは実際に、SystemJS を single-spa に組み込んでいきます。

single-spaアプリケーションへの設定

single-spaで起動したいアプリケーションにSystemJS用の設定をします。
私たちは、viteをモジュールバンドラとして使用しているため、vite.config.tsに以下のような設定を入れました。

// vite.config.ts
export default defineConfig({
  resolve: {
    alias: {
      vue: "vue3",
    },
  },
// SystemJSで読み込むライブラリを外部依存関係として設定
  build: {
    rollupOptions: {
      external: ["vue3"],
    },
  },
  build: {
    rollupOptions: {
      output: {
// SystemJSモジュールとしてビルドできるように設定
        format: "system",
      },
    },
  },
});

Root Config への SystemJS の導入

single-spa における Root Config は、アプリケーションをロードするための中核的な役割を担います。
これは主に、single-spaアプリケーションや、その依存関係を import するルート HTML と、アプリケーションを登録する JavaScript ファイルから構成されています。

ルート HTML

ルート HTML では、single-spaアプリケーションやライブラリのモジュールのインポートを行います。下記の例では、SystemJS と Import Map を用いて、app1、app2、vue2、vue3 の各モジュールを外部からインポートしています。

<!doctype html>
<html lang="ja">
    <head>
        <!-- ヘッダー情報 -->
    </head>
    <body>
        <!-- SystemJSを読み込む -->
        <script src="https://example.com/npm/systemjs@6.8.3/dist/system.min.js"></script>
        <!-- SystemJSimportマップを定義 -->
        <script type="systemjs-importmap">
            {
                "imports": {
                    "app1": "https://example.com/application/app1.js",
                    "app2": "https://example.com/application/app2.js",
                    "vue2": "https://example.com/npm/vue@2.6.14/dist/vue.js",
                    "vue3": "https://example.com/npm/vue@3/dist/vue.system.js"
                }
            }
        </script>
        <!-- アプリケーションを登録するJavaScriptファイルを読み込む -->
        <script type="module" src="/src/index.ts"></script>
    </body>
</html>

JavaScriptファイル

JavaScript ファイルでは、single-spaアプリケーションの登録とロードが行われます。アプリケーションのロードは SystemJS を用いて、動的に行います。

import { registerApplication, start } from 'single-spa';

// app1の登録とロード
registerApplication({
  name: 'app1',
  app: () => System.import('app1'),
  activeWhen: location => location.pathname.startsWith('/app1'),
});

// app2の登録とロード
registerApplication({
  name: 'app2',
  app: () => System.import('app2'),
  activeWhen: location => location.pathname.startsWith('/app2'),
});

// アプリケーションのマウント
start({
  urlRerouteOnly: true,
});

Vue2とVue3のライブラリは、SystemJS形式でビルドされた app1、app2 の中でSystem.registerによってロードされます。

System.register(['vue3'], function (Hn, Wn) { ... }

この方法により、single-spa で構築されたフロントエンドアプリケーションは、必要に応じて動的にライブラリをロードできます。
例えば、app1 で使用されるライブラリは、app1 の実行時にのみロードされます。

最終的な構成

以下に示すのは、SystemJS を使用した signle-spa システムの構成図です。

  1.  (初回のみ) SystemJS、Vue3、Vue2 などのライブラリモジュールをサーバにアップロードします。ライブラリのバージョンを上げたい場合のみ、再度モジュールをアップロードします。

  2. CI/CD でコードの変更時に、Vue3 や Vue2 で作成されたアプリケーションのモジュールをサーバにアップロードします。

  3. アプリケーションは SystemJS と Import Map によって必要な時のみ、動的にロードされます。

  4. アプリケーションの依存関係は、アプリケーションのロード時に動的にクライアント (Root Config) から受け渡されます。

SystemJS 導入によるメリット

SystemJS を導入することで以下のメリットを得ることができました。

  1. 実行時の依存解決 : SystemJS は、Import Map を利用して実行時に必要なモジュールを動的にロードします。この機能により、アプリケーションの初回ロード時のパフォーマンスが向上しました。

  2. バージョンの衝突を回避 : Vue2 と Vue3 のような異なるバージョンのアプリケーションをロードする場合、SystemJS を使用すると、それらを同時にロードし、それぞれを独立したスコープ内で実行することができます。

  3. 動的なモジュールのインポート: SystemJS を利用することで、必要に応じてモジュールを動的にインポートできます。これにより、アプリケーションのフットプリントを小さく保ちつつ、必要なリソースだけをロードすることが可能となります。

  4. 独立したビルドとデプロイ : 各マイクロフロントエンドは独立してビルドおよびデプロイできるため、チーム間の作業の衝突を避け、効率的な開発プロセスを実現することができました。加えて、アプリケーションのバージョンアップ時には Import Map に記述されているモジュールの URL のみを更新することで、シームレスなバージョンアップデートとモジュールの管理が可能となりました。

特に、4 . に関しては、独立した複数のアプリケーションを同時に使用するマイクロフロントエンドの開発において有用だと感じています。

おわりに

今回、SystemJS を中心としたマイクロフロントエンドでのモジュール依存関係の解決法について解説しました。

日常の開発では、モジュールやライブラリの依存関係の裏側を深く掘り下げる機会は少ないかもしれません。しかし、この取り組みを通じて、モジュール管理のメカニズムや動的なモジュールインポートの仕組みを深く理解することができました。

今後は Vue2, Vue3 だけではなく、ユーザが使いたい UI フレームワークを自由に選択し、作成したコンポーネントを手軽にVisual Mosaic上で組み合わせられるようなことが実現できたらなと思います。

本記事が、マイクロフロントエンドの実装や SystemJS の活用を考えている皆様の一助となれば幸いです。
最後までご覧いただき、ありがとうございました。