Vue CLIのWebComponentビルドをVue3でも使いたい

Vue CLIには ビルドターゲットとして Web Componentを選択できる機能があり、これを使うとVueのSFC(単一ファイルコンポーネント)をWeb Componentとして書き出してくれます。

cli.vuejs.org

このビルドモードには以下の特徴があります。

ところが、Vue CLIでVue3を使い、Web Componentビルドを試すと、以下のようになります。

>>> % npx vue-cli-service build --target wc-async --name test 'src/components/*.vue'

⠋  Building for production as web component (async)...
✔  Building for production as web component (async)...
 ERROR  Vue 3 support of the web component target is still under development.

このコマンドはVue3ではまだ使えません。Issueにもサポートの要望が上がっています。

github.com

ところで、Vue3からはVueコンポーネントをWeb Componentとして取り扱えるようにするための各種環境が公式にサポートされるようになりました。

v3.ja.vuejs.org

以下のように記述することで、VueコンポーネントをWebComponentとして利用することができるようになります。

import { defineCustomElement } from 'vue'
import TestComponent from './TestComponent.ce.vue'

const el= defineCustomElement(TestComponent )

customElements.define('test-component', el)

また、上記ドキュメントでも言及されていますが、Viteには「Glob Import」という機能があり、これを使うと特定のディレクトリ配下にあるコンポーネントをImportする処理を簡単に書くことができます。

/**
 * const modules = {
 *  './components/hoge.vue' : () => import('./components/hoge.vue')
 *  './components/fuga.vue' : () => import('./components/fuga.vue')
 * }
 * 以下の命令は上記のソースコードに等しい
 */
const modules = import.meta.glob('./components/*.vue')

(webpackでも同様のことができそうな雰囲気ですが、今回は試していません。)

modules に格納されるモジュールはPromiseを解決してロードさせることからも示される通り、非同期で読み込まれます。

Viteを使えばVue CLIのWeb Componentビルドと同等の要件を満たせそうなビルド結果が得られそうなので、Viteをセットアップしておきます。( 最初の Vite プロジェクトを生成する

プロジェクトのディレクトリ内は概ね以下のような構成になると思います。

.
|-- README.md
|-- index.html
|-- package-lock.json
|-- package.json
|-- src
|   |-- components
|   `-- main.js
`-- vite.config.js

ここで Vue CLIのWeb Componentビルドの要件を思い出し、「SFCごとのjsファイルと、一つのロードスクリプトを出力する」「ページ内で必要なWebComponentがリクエストされる」というビルド結果が得られるようにエントリポイントとなる src/main.js を書き下してみます。 (ざっくりしたソースコードですがお許しください)

import { defineCustomElement } from 'vue'

// comopnents 配下のすべてのVueファイルを読み込む
const modules = import.meta.glob('./components/*.vue')

for (const [path] of Object.entries(modules)) {
  const el = document.getElementsByTagName(convertPathToElementName(path))

  // タグがHTMLの中から見つかったらモジュールをFetchする
  if (el.length > 0) {
    modules[path]().then((mod) => {
      const el = defineCustomElement(mod.default)

      customElements.define(convertPathToElementName(path), el)
    })
  }
}

/**
 * パスからCustom Elementのタグ名を決定する
 * @param {string} path パス ex) './components/HogeFuga.vue'
 * @return {string} タグ名 ex) 'hoge-fuga'
 */
function convertPathToElementName(path) {
  // パスから必要な部分を取り出してsnake-caseになるようにしている
  return path.split('/')
    .pop().
    replaceAll(/([A-Z])/g, '-$1').
    toLowerCase().
    substr(1).
    replace(/\.vue/, '').
    replace(' ', '')
}

vite.config.js は以下のようになります。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue({
      // プロパティを指定しない場合は `*.ce.vue` というファイルがCustom Elementとして扱われる
      // WebComponentとして扱いたいSFCのファイル名を *.ce.vue にするならば指定は不要
      // Custom Elementとして扱われる場合、StyleがShadow DOMの中に挿入される
      // ref: https://github.com/vitejs/vite/tree/main/packages/plugin-vue#readme
      customElement: true, 
    })
  ]
})

Viteプロジェクト内の src/components/ 内にいくつかのコンポーネントを定義してみます。

src
|-- components
|   |-- TestButton.ce.vue
|   `-- TestComponent.ce.vue
`-- main.js

Viteプロジェクト内のルートディレクトリに存在するHTMLファイルにWebComponentを埋め込んでみます。

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" href="/favicon.ico" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Vite App</title>
      </head>
      <body>
        <test-button>ほげほげふがふが</test-button>
        <test-component />
        <script type="module" src="/src/main.js"></script>
      </body>
    </html>

これで npm run build してみると、SFCとして定義されたVueコンポーネントがWebComponentとして配置されている様子を確認できます。また、一部のWebComponentをHTMLから除外しつつブラウザの開発者ツールで確認すると、必要なぶんのJavaScriptファイルだけが取得されていることも確認できると思います。

Vue CLIのWeb ComponentビルドのVue3サポートに不安がありましたが、Vue CLIのWeb Componentビルドを使って開発していたプロジェクトも Vueの移行ビルド を使いつつ、上記のように製品のビルドをViteに任せることでVue3化することができそうだということがわかってひとまず安心したりしていました。

今回はお試しということで、細かい調整などはしていませんがなんとなく雰囲気が分かったので記事にしてみたという感じでした。ではでは~