hacomono TECH BLOG

フィットネスクラブ・スクールなど施設・店舗のための会員管理・予約・決済システム「hacomono」 開発チームの技術ブログ

Vue コンポーネントを動的に外部から読み込む

こんにちは、開発グループの和田(ニックネームはなつです)です。

普段は大阪からリモートで、hacomono のエンタープライズ領域( = アドオン開発基盤周辺)を主に担当しております。

大手のお客様にシステム導入する際、Saas であっても個別の要件をいただくことは往々にしてあるかと思います。

hacomono ではプロダクトとしては対応しない場合でも、アドオンとして積極的に個別開発を行っており、その中でもよくある任意のページを組み込みたい、という要望を実現するためにとったアプローチを紹介できればと思います。

アプローチ

技術的な要件・前提条件

  • Vue2 / Nuxt2
  • ES6
  • クライアントサイドレンダリング
  • 読み込み対象となるコンポーネントはビルド/デプロイ時点では不明であり、任意の URL が指定される

考えられるアプローチ

CTO や Google 先生に助言をもらいつつ、マイクロフロントエンド周辺などを色々と調べては見ましたが、前述の要件を押さえたうえでプロダクションレベルに適用するのは中々に難しいテーマでした。

パッと思いつく方法としては iframe で埋め込む方法ですが、これは フレーム内外のやり取りの手間やセキュリティ観点からもネガティブで、それであれば別サイトを立てた方がましということから、候補から除外されました。

こうなるとあとは動的にコンポーネントをロード、レンダリングするというのが思いつくアプローチになります。

動的にコンポーネントをレンダリングする、というのは Vue 標準の機能で問題なさそうですが、ES6の import では http 経由でコンポーネントを読み取ることができません。

import mod from 'https://xyz.hacomono.jp/comp.js' // => これや、
const mod = import('https://xyz.hacomono.jp/comp.js') // => これはできない 

そのため外部からソースコードを読み込むとなると script タグを埋め込む形になるかと思いますが、Vue コンポーネントは UMD としてビルドすることが可能なため、以下の作戦を取ることにしました。

  1. UMD としてビルド
  2. script タグによる読み込み
  3. グローバルオブジェクトとして取得してコンポーネントを描画

ES module を利用したい気持ちもありましたが、以下を理由に一旦見送りました。

実装

以下では参考レベルで、Vue コンポーネントのビルドとコンポーネントの読み込みの実装コードを紹介します。

Vue コンポーネントを単体で実行可能なモジュールとしてビルドする

  • Vue ファイルを指定して、Webpack にて UMD ライブラリとしてビルドします。

webpack.config.js

module.exports = {
  ...

  entry: path.resolve(__dirname + '/src/components/') + fileName,
  output: {
    filename: fileName.replace('.vue', '.js'),
    libraryTarget: 'umd', // => umd として出力
    library: fileName.replace('.vue', ''),
    umdNamedDefine: true
  },
};

ここで生成された UMD 形式の js を script タグで読み込むとグローバル変数( window[{libraryに指定した値}]) に Vue コンポーネントが格納されます。

CommonJS、AMD 双方が利用できない環境で UMD が読み込まれた場合の動作を活用する形です。

名前空間が衝突しないよう、なにかしら工夫してもよいかもしれません。

動的にモジュールを読み込み、レンダリングする

  • script タグを埋め込み、load を補足して Promise として返却します。(ブラウザで umd を読み込むと、window (グローバル) オブジェクトに当該ライブラリが格納されます。)
  • Promise をcomponent の is 属性に指定して、動的 & 非同期コンポーネントを読み込ませます。
<template>
    <component :is="dynamicComponent" />
</template>

<script>
export default {
  data() {
    return { dynamicComponent: {} }
  },
  
  mounted() {
    this.dynamicComponent = this.loadComponent(this.libName(), this.libUrl())
  },

  methods: {
    loadComponent(name, url) {
      return new Promise((resolve, reject) => {
        const script = document.createElement('script')
        script.async = true
        script.addEventListener('load', () => {
          // ロード後 window オブジェクトにモジュールが格納される
          resolve(window[name])
        })
        script.src = url
        document.head.appendChild(script)
      })
    }
  }
}
</script>

あらためてみると原始的な実装ではありますが、これにより任意URLに配置されたコンポーネントを動的に描画する、という要件を満たす事ができます。

まとめ

いかがでしたでしょうか。

今回はアドオン開発基盤におけるフロントエンド周辺の取り組みの一つを紹介させていただきました。

エンタープライズ領域の開発ではバックエンド / インフラのアーキテクチャ整備や、API や SSO といった機能開発など幅広いテーマを取り扱っていますので、機会があればその他の取り組みもご紹介できればと思います。