見出し画像

Vue2 と Vue3 を共存させるためのマイクロフロントエンドアプローチ ~single-spa の導入~

はじめまして。株式会社ラキールで DX 基盤開発を行う LaKeel DX Engine Group に所属する田村です。
普段はUI コンポーネントを一元管理し、それらを自由に組み合わせてアプリケーションを構築できる、フロントエンドの開発基盤 「LaKeel Visual Mosaic」という製品の開発を担当しています。

Visual Mosaic では、Vue2 と Vue3 のコンポーネントを同一アプリケーションで動作させるためのアプローチとして single-spa というマイクロフロントエンドを実現する OSS を採用しました。
本記事は、その single-spa についての技術紹介記事となっております。

背景

Visual Mosaic は、「ウィジェット」と呼ばれる Vue コンポーネント単位で、マイクロフロントエンドを実現する製品です。
これまで、ウィジェットは Vue2 での開発を前提としていました。これは、ウィジェットをレンダリングするためのクライアントが Vue2 に依存するためです。
しかし、Vue2 の EoL が2023年末に迫っており、これからは現在の Vue の最新バージョンである Vue3 で新たなウィジェットの開発を行う必要があります。
この際、Vue2 の既存のコードを Vue3 に単純に移行することは困難で、時間がかかってしまうことが懸念点としてありました。
そのため、Visual Mosaic では、Vue2 アプリケーションと Vue3 アプリケーションを共存させることができるアプローチを採用することにしました。
そこで利用したのが、 single-spa というマイクロフロントエンドを実現する OSS です。

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

マイクロフロントエンドとは、バックエンド開発におけるマイクロサービスの考え方を適用したフロントエンド開発手法です。
画面を複数の部品に分割し、これを組み立てる事で1つの画面を構成します。これにより、機能の変更と拡張が容易になります。

single-spa とは

single-spa は、フロントエンド JavaScript プロジェクトのためのルーティングフレームワークです。従来のシングルページアプリケーションが1つのフレームワーク(React、Angular、Vue など)の利用に限定されるのに対し、single-spa は複数のフレームワークで構成されるマイクロフロントエンドを構築できるように設計されています。

single-spa の機能

  • マイクロフロントエンドの実現
    single-spa によって、異なるフレームワークを用いた複数の独立したアプリケーションを1つの SPA に統合することができます。

  • ルーティング
    ユーザーがウェブサイト内でページ遷移を行うとき、single-spa はそれぞれのマイクロフロントエンドアプリケーションを適切にロードおよびアンロードします。

  • ライフサイクルの管理
    single-spa は各マイクロフロントエンドアプリケーションのライフサイクルを管理します。具体的には、各アプリケーションのロード、マウント、アンマウントです。

single-spa の利点

  • 特定のフレームワークに依存しない
    single-spa は、Vue, React, Angular などの主要なフロントエンドフレームワークに依存せず、それぞれのフレームワークを用いた画面を同時にレンダリングできます。

  • コードの分割
    マイクロフロントエンドの採用によって、大規模なコードベースを小さな、管理しやすいパーツに分割できます。

  • 段階的なアップグレード
    アプリケーションは細かいマイクロフロントエンドアプリケーションに分割できるので、段階的にアプリケーションを作り上げる事ができます。

以上を踏まえて、single-spa は以下のような場合に利用するケースで、特に効力を発揮するといえます。

  • 複数のフロントエンドフレームワークを利用する SPA アプリケーションを開発したい場合

  • 既存のアプリケーションを段階的に再構築したい場合

single-spa のアーキテクチャ

ここまで機能や利点について、紹介した single-spa ですが、実際にどのようなアーキテクチャで動作しているかについて、主要な要素と動作原理を説明します。

アプリケーションのレンダリング

single-spa では、マイクロフロントエンドアプリケーションは、それ自身のレンダリングに関するライフサイクルを持ちます。具体的には、以下の3つです。

  • Bootstrap
    アプリケーションの初期化を行います。このライフサイクルはアプリケーションが初めてロードされたときに一度だけ呼び出されます。

  • Mount
    アプリケーションのレンダリングを行います。ページ遷移などによって、アプリケーションをレンダリングする際に呼び出されます。

  • Unmount
    アプリケーションをアンマウントします。ページ遷移などによって、アプリケーションのレンダリングが不要になった際に呼び出されます。

これらのライフサイクルを関数としてエクスポートし、single-spa で利用します。

ルーティング

single-spa は、ユーザーがサイト内を遷移するたびに、どのアプリケーションを起動または停止するかを決定するルーティング機能が組み込まれています。これは single-spa によって、URLに対してどのアプリケーションを起動するかを設定できます。

実際に Vue2 と Vue3 を共存させてみる

実際に、single-spa を使って、Vue2 と Vue3 アプリケーションを共存させてみます。
single-spa の初期構成は公式ホームページのサンプルをベースに紹介します。

Vue2 アプリケーションの設定

Vue2 のマイクロフロントエンドアプリケーションの設定です。ディレクトリ構成やビルド設定は、single-spa のサンプルを参考にしてください。

まずは、描画する Vue コンポーネントを定義します。今回は、ボタンをクリックした回数を表示する簡単なコンポーネントを表示してみます。
以下は、コンポーネントの App.vue のサンプルです。

<template>
  <div id="vue2-app">
    <div>
      <img alt="Vue logo" class="logo" src="./assets/Vue2Logo.png" width="125" height="125" />
    </div>
    <main>
      <h1>Welcome Vue2 Component!</h1>
      <div>ClickCount: {{ clickCount }}</div>
      <button @click="clickCount++">increment</button>
    </main>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      clickCount: 0,
    };
  },
};
</script>

<style>
#vue2-app {
  background-color: aquamarine;
}

header {
  display: flex;
  justify-content: space-around;
}

main {
  display: flex;
  justify-content: space-around;
  flex-direction: column;
}

h1,h2 {
  text-align: center;
}
</style>

次は、このコンポーネントを single-spa で読み込んで表示するための設定を行います。
以下は、main.js の記述です。

import Vue from 'vue';
import App from './App.vue';
import singleSpaVue from 'single-spa-vue';

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render: h => h(App),
  },
});

export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;

Vue に関しては、single-spa-vue というプラグインを利用して、ライフサイクル関数をエクスポートします。
これらの関数を single-spa 側から呼び出し、描画が行われます。

Vue3 アプリケーションの設定

同様に、Vue3 コンポーネントも用意します。ディレクトリ構成やビルド設定は、single-spa のサンプルを参考にしてください。
こちらも、ボタンをクリックした回数を表示する簡単なコンポーネントを表示してみます。以下は、コンポーネントの App.vue のサンプルです。

<template>
  <div id="vue3-app">
    <header>
      <img alt="Vue logo" class="logo" src="./assets/Vue3Logo.png" width="125" height="125" />
    </header>
    <main>
      <h1>Welcome Vue3 Component!</h1>
      <div>ClickCount: {{ clickCount }}</div>
      <button @click="clickCount++">increment</button>
    </main>
  </div>
</template>

<script setup>
import { ref } from "vue";
const clickCount = ref(0);
</script>

<style>
#vue3-app {
  background-color: antiquewhite;
}

header {
  display: flex;
  justify-content: space-around;
}

main {
  display: flex;
  justify-content: space-around;
  flex-direction: column;
}

h1,h2 {
  text-align: center;
}
</style>

こちらも同様に、このコンポーネントを single-spa で読み込んで表示するための設定を行います。
以下は、main.js の記述です。

import { createApp, h } from "vue";
import App from "./App.vue";
import singleSpaVue from "single-spa-vue";

const vueLifecycles = singleSpaVue({
  createApp,
  appOptions: {
    render() {
      return h(App, {});
    },
  },
  handleInstance: (app) => {},
});
export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;

single-spa の設定(root-config.js)

各マイクロフロントエンドアプリケーションを描画する、single-spa クライアントの設定です。こちらを参考にしてみてください。

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

// Vue2 アプリケーションの登録
registerApplication({
  name: 'techblog-vue2-app',
  app: () => System.import('techblog-vue2-app'),
  activeWhen: ['/sample'],
});

// Vue3 アプリケーションの登録
registerApplication({
  name: 'techblog-vue3-app',
  app: () =>
    import(
      /* webpackIgnore: true */
      'http://localhost:3000/src/main.js'
    ),
  activeWhen: ['/sample'],
});

start();

各マイクロフロントエンドアプリケーションをインポートし、レンダリングを行う URL と紐づけて登録します。
今回は、同じページに作成したコンポーネントを配置するので、 activeWhen プロパティには同じ URL を指定します。

そして、作成したアプリケーションを全て起動して、実際にローカルに立ち上げたページにアクセスしてみると……

表示されました! Vue のバージョン差異は見た目だとわかりにくいので、画像と色を付けてみました。スマホのフレームはブラウザの開発者ツールの機能で出しています。
配置したボタンと、押したときのカウンターも表示されています。

また、この方法では Vue アプリケーションをマウントする DOM 要素をsingle-spa が処理の中で作成しますが、どの DOM 要素にマウントするかを明示的に指定する方法もあります。

mountRootParcel(System.import('techblog-vue2-app'), {
  domElement: document.getElementById('vue2-mount-point'),
});

mountRootParcel(
  import(
    /* webpackIgnore: true */
    'http://localhost:3000/src/main.js'
  ),
  {
    domElement: document.getElementById('vue3-mount-point'),
  }
);

start();

上記のように、single-spa の Parcel という機能を利用を利用して、アプリケーションをマウントする DOM 要素を明示的に指定する事も可能です。
対象の DOM 要素は、getElementById() 等の関数を利用して取得します。

おわりに

フロントエンド開発では、利用する UI フレームワークの選定から選択肢が多く、選択したフレームワークに束縛されてしまうことが多々あるかと思います。
single-spa をはじめとした、UI フレームワークに依存しないマイクロフロントエンドアプリケーションでの開発は、このような UI フレームワークの束縛から解放されるための手段として、非常に画期的なものだと感じました。

私自身、single-spa についても、フロントエンド開発についても知識が浅く、まだまだ学んでいく段階ですので、これからもこのテックブログを通じて様々な学んだ事を共有したいと思っております。

そして、今回の内容が少しでも誰かのお役に立てれば、これ以上の喜びはありません。
最後まで読んでくださり、ありがとうございました。

参考サイト

https://single-spa.js.org/