Vue CLIのWebComponentビルドをVue3でも使いたい
Vue CLIには ビルドターゲットとして Web Componentを選択できる機能があり、これを使うとVueのSFC(単一ファイルコンポーネント)をWeb Componentとして書き出してくれます。
このビルドモードには以下の特徴があります。
- 指定したディレクトリ内のSFCをWebComponentとして使えるようにビルドする(ビルドの過程でSFCのコンポーネントを define するソースコードが入るようになる)
- SFCごとのjsファイルと、一つのロードスクリプトを出力する( Async Web Componentモード の場合)
- ロードスクリプトがページ内に必要なWebComponentを探し出し、必要な分のjsファイルを非同期で取得する(1つのファイルにバンドルしないのでJavaScriptファイルの容量が抑えられる)
ところが、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にもサポートの要望が上がっています。
ところで、Vue3からはVueコンポーネントをWeb Componentとして取り扱えるようにするための各種環境が公式にサポートされるようになりました。
以下のように記述することで、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化することができそうだということがわかってひとまず安心したりしていました。
今回はお試しということで、細かい調整などはしていませんがなんとなく雰囲気が分かったので記事にしてみたという感じでした。ではでは~