🧊

レガシーなVue2をマイクロフロントエンド化しながらVue3へ漸進的にアップデートするハンズオン

2022/06/26に公開

Vue2からVue3のアップデート、大変ですよね...

マイクロフロントエンドも考慮にいれてみると、Vue2+Vue3のアプリケーションとして漸進的にアップデートできるんじゃなかろうか?と思いました。

今回は、icestarkというマイクロフロントエンドフレームワークを利用して、Vue2 + Vue3のマイクロフロントエンドを作成してみます。実際に、某スタートアップのプロダクトに実践投入したうえでの解説記事となります。

更新履歴

Icestarkとは

公式: https://micro-frontends.ice.work/

Taobao, AliExpressも使っているマイクロフロントエンドフレームワークです。

簡単にいうと、マイクロフロントエンドフレームワークで、親アプリケーションと子アプリケーションを作成するために利用します。

親アプリケーションは、マイクロフロントのレイアウト部分に相当し、子アプリケーションは/hoge, /fooなどそれぞれのルーティングで表示するアプリケーションレポジトリに相当します。

例えば、/hogeの場合にVue2にルーティングしたり、/fooの場合にVue3にルーティングしたりします。

詳細については、先日記事にまとめてみたのでこちらをどうぞ。

https://zenn.dev/mikana0918/articles/344861f49f7190

ハンズオン

さて、それではVue2 + Vue3のマイクロフロントエンドをつくってみましょう。

メインレイアウトアプリケーション(Vue3)のセットアップ

サンプルレポジトリ:
https://github.com/mikana0918/icestark-layout-sample

READMEにも掲載がありますが、メインレイアウトアプリケーションのpackage構成は下記になります。

package:

Key Value
Vue.js 3.2.16
Vite.js 2.6.0
Vue Router 4.0.11 for Vue 3.x
@ice/stark 2.6.1 for Microfrontend integration
element-plus 1.2.0-beta.6 for UI

どんな感じなのか、ひとまずインストールして手元で動かしてみます。

インストール:

$ git clone https://github.com/mikana0918/icestark-layout-sample.git
$ cd icestark-layout-sample
$ yarn install
$ yarn run dev

  vite v2.9.12 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose

  ready in 737ms.

http://localhost:3000/にアクセスすると、レイアウトアプリケーションが確認できました。

サブアプリケーション(Vue2)のセットアップ

同様に、今度はVue2アプリケーションをセットアップしてみましょう。

サンプルレポジトリ:
https://github.com/mikana0918/icestark-vue2-child-sample

package:

Key Value
Vue.js 2.6.14
Vite.js 2.9.12
Vue Router 3.5.3 for Vue 2.x
@ice/stark-app 1.5 for Microfrontend integration

さて、このサンプルVue2アプリケーションですが、いろいろな記法での参考例を書いてみています。後ほど、マイクロフロントエンドとして動かして確認してみましょう。

  • Vue2 Options API
  • Vue2 Class Component
  • Vue2 Property Decorator

このあたりのレガシーVue2シンタックスを対応したいというニーズが主だと思うので、ひとまずこれら3つに絞ってサンプルを用意しました。(もし、vue2 compositionAPIが必要などあればissueを投げてください。対応します。)

インストール:

$ git clone https://github.com/mikana0918/icestark-vue2-child-sample.git
$ cd icestark-vue2-child-sample
$ nvm use # make sure you have Node.js 14.18.1
$ yarn Install
$ yarn run dev

  vite v2.9.12 dev server running at:

  > Local: http://localhost:3322/
  > Network: use `--host` to expose

  ready in 1555ms.

http://localhost:3322/にアクセスすると、Vue2子アプリケーションが確認できました。

マイクロフロントエンドとして動かす

Vue3親レイアウトアプリケーションとVue2子アプリケーションそれぞれのViteプロセスが走っていることを確認したら、レイアウトアプリケーションのhttp://localhost:3000/にアクセスしてみます。

左のサイドバーから、Vue2 sample > vue2 options をクリックしてみると、vue devtoolからVue3とVue2のアプリケーションが起動していることが確認できました。また、vue2子アプリケーションが親アプリケーションからアクセスできていることも確認できました。

これでマイクロフロントエンドの叩き台が完成しました。🎉

Vue.jsに追加したicestarkのコードをざっくり解説

メインレイアウトアプリケーション(Vue3)

https://github.com/mikana0918/icestark-layout-sample

icestarkのメインレイアウトアプリケーションのボイラープレートから作成されています。
script setup lang=tsで書かれており、非常にモダンでいいですね。

参考: https://micro-frontends.ice.work/docs/guide/use-layout/vue

メインアプリケーションは基本的にはレイアウトコンポーネントの構築と、マイクロフロントエンドのルーティング管理が責務になってきます。また、横断的関心事を載せることもあると思います。(認証・認可など)

icestarkを使う場合は、icestarkのライフサイクルフックをonMountedの中で実行する必要があり、icestarkの設定~icestarkアプリケーションのスタートまでもonMountedの中に記述してしまいます。

src/App.vue
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
  import Layout from './layouts/BasicLayout.vue'
  import { onMounted, ref } from 'vue';
  import start from '@ice/stark/lib/start';
  import { registerMicroApps } from '@ice/stark/lib/apps';
  import { useRouter } from 'vue-router'

  const router = useRouter();
  let loading = ref(false);
  let microAppsActive = ref(false);

  // Vueライフサイクルフック
  onMounted(() => {
    // App.vueのtemplateにあるcontainerのHTMLElementへの参照
    const container = document.getElementById('container') as HTMLElement;
    // icestarkにマイクロアプリケーションルーテインングを登録する
    registerMicroApps([
      // icestark公式提供のReactマイクロアプリケーションへのルーティング
      {
        name: 'seller',
        activePath: '/seller',
        title: '商家平台',
        loadScriptMode: 'import',
        // React app demo: https://github.com/ice-lab/react-materials/tree/master/scaffolds/icestark-child
        entry: 'http://iceworks.oss-cn-hangzhou.aliyuncs.com/icestark/child-seller-ice-vite/index.html',
        container, // containerのHTMLElementへマウント
      }, 
      // icestark公式提供のVue3マイクロアプリケーションへのルーティング
      {
        name: 'waiter',
        activePath: '/waiter',
        title: '小二平台',
        loadScriptMode: 'import',
        entry: 'http://iceworks.oss-cn-hangzhou.aliyuncs.com/icestark/child-vue3-vite/index.html',
        container, // containerのHTMLElementへマウント
      },
      // ハンズオンで実際に見たvue2子アプリケーションのルーティング定義
      {
        name: 'icestark-vue2-child-sample',
        activePath: '/vue2',
        title: 'icestark-vue2-child-sample',
        loadScriptMode: 'import',
        entry: 'http://localhost:3322/index.html',
        container // containerのHTMLElementへマウント
      }
    ]);
    // icestarkアプリケーションをスタートする
    start({
      // マイクロアプリケーションがロードを開始した場合のフック
      onLoadingApp: () => {
        loading.value = true;
      },
      // マイクロアプリケーションがロードを終了した場合のフック
      onFinishLoading: () => {
        loading.value = false;
      },
      // マイクロフロントエンドアプリケーションのルーティングが変更された場合のフック
      onRouteChange: (_, pathname) => {
        // マイクロフロントエンドのpathname("/hoge"のような形式)を
        // vue routerにpush()することで同期させます
        // 处理微应用间跳转无法触发 Vue Router 响应
        router
          .push(pathname)
          .catch(() => {})
      },
      // マイクロアプリケーションがアクティブになった場合のフック
      onActiveApps: (activeApps) => {
        microAppsActive.value = !!(activeApps || []).length;
      }
    });
  })
</script>

<template>
  <div id="app">
    <div>
      <layout />
    </div>
    <div class="content" v-loading="loading">
      <div id="container"></div>
      <router-view v-if="!microAppsActive" />
    </div>
  </div>

</template>

Vue2サブアプリケーション

https://github.com/mikana0918/icestark-vue2-child-sample

Vue2でicestarkサブアプリケーションを作る場合、若干設定が面倒なのですが、下記がほとんど決定版になっていまして、下記の通りに設定すれば基本的にVue2子アプリケーションを動作させることができるはずです。

コメントで解説をいれてみました。

src/main.ts
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { isInIcestark } from "@ice/stark-app/lib";
import setLibraryName from "@ice/stark-app/lib/setLibraryName";

// これが非常に重要で、ブラウザのwindowにこのsetLibraryName()した値を仕込んでおり、
// これを親アプリケーションから見つけることで、サブアプリケーションの下記メソッドmount()や
// unmount()を呼べるようになっています。
setLibraryName("icestark-vue2-child-sample");

Vue.config.productionTip = false;

const vue = new Vue({
  router,
  render: (h) => h(App)
});

// 即座にvue.$mount(el)を行わないために、関数としてラップ
const runApp = (container: Element | string) => {
  vue.$mount(container);
};

// icestarkがこのmount({container}: {container: Element})に対して
// メインアプリケーションで定義したHTMLElementへの参照を渡してくれます。
// その渡されたHTMLElementに対して、Vueインスタンスのマウントを行います。
export function mount({ container }: { container: Element }) {
  console.log(container) // どのElementが渡されたか見たいのでデバッグ
    
    // isInIcestark(): マイクロアプリケーションとして動いている場合、true
  // マイクロアプリケーションとして動作しているかをデバッグするためにconsole.logを置いています。
  if (isInIcestark()) {
    console.log("🧊 your icestark-vue2-child-sample is within icestark")
  } else {
    console.log("🧊 your icestark-vue2-child-sample is NOT witihin icestark")
  }

  vue.$mount();

  // for vue don't replace mountNode
  container.innerHTML = "";
  container.appendChild(vue.$el);
}

// icestarkがunmount()をする場合のフックを定義
// ここではvue.$destroy()してインスタンスを破棄します。
export function unmount() {
  vue && (vue as any).$destroy();
}

// マイクロフロントエンドどして動いていない場合、マウント先を#appにして
// スタンドアロンアプリケーションとして起動させます。
// if this child app is not working witihin microfrontend,
// just run as standalone app.
if (!isInIcestark()) {
  runApp("#app");
}

Vue routerの設定ですが、マイクロフロントエンドアプリケーションはブラウザのhistory APIに準拠したルーティングを行う必要があるため、historyモードが必須になっています。

また、親のマイクロフロントエンドのbaseURLが変更された場合に、子アプリケーションでも同期させておく必要があるので、router.push(getBasename())というおまじないを行なっています。

src/router/index.ts
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld.vue'
import Vue2Options from '@/components/Vue2Options.vue'
import Vue2Class from '@/components/Vue2Class.vue'
import Vue2PropertyDecorator from '@/components/Vue2PropertyDecorator.vue'
import { getBasename } from "@ice/stark-app/lib";
import { isInIcestark } from "@ice/stark-app/lib";

Vue.use(Router)

const router = new Router({
  mode: "history", // ブラウザのhistory APIに準拠してマイクロフロントエンドのルーティングを行うため、historyモードが必須
  base: "/",
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: '/vue2',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: '/vue2/options',
      name: 'vue2-home',
      component: Vue2Options
    },
    {
      path: '/vue2/class',
      name: 'vue2-class',
      component: Vue2Class
    },
    {
      path: '/vue2/class-decorator',
      name: 'vue2-class-decorator',
      component: Vue2PropertyDecorator
    }
  ]
})

// マイクロフロントエンドとして起動していてbaseURLが変更された場合、SPAルーターと同期するように
// router.pushしてしまいます。
// when microfrontend base url changed, always push as history
if (isInIcestark()) {
  router.push(getBasename());
}

export default router

以上、公式ドキュメントの焼き増しではありますが、コードのさらに細かい解説については、公式サイトを確認してみてください。

https://micro-frontends.ice.work/docs/guide/

さらにVue3子アプリケーションを追加したい場合

icestark公式サイトに、ボイラープレートが配布されているのでそれを使います。

https://micro-frontends.ice.work/docs/guide/use-child/vue

また、親アプリケーションへのmicrofrontendルーティングの登録もやってみましょう。

(準備中...)

Discussion