Vue3.5からの改善と振り返るdefineCustomElementについて
Vue3.5の更新では defineCustomElement に関する改善が含まれています。今回の記事ではその defineCustomElement に関連する事項についてと、改善された内容を紹介します。
Custom Elementsとは?
defineCustomElement についてを紹介する前に、まずは根底の知識となる「Custom Elements」についてを解説します。
Custom ElementsはWeb Componentsを構成する一部で、独自のHTML要素を作成するためのJavaScript APIです。
独自の定義のため、通常のHTML要素とは異なりダッシュが使われている名前( kebab-case )である必要があります。
<!-- 純粋なHTML要素 -->
<progress></progress>
<!-- Custom Elements -->
<custom-progress></custom-progress>
Custom Elementsの利点としては、UIライブラリ・フレームワークを使用しているかに関わらずに利用できることが挙げられます。例えばフロントエンドの技術スタックが異なるアプリケーション間で共通のUIコンポーネントを使用する場合に有用です。
Web標準技術の相互運用向上プロジェクトであるInteropではWeb Componentsの改善が行われており、Custom Elementsはほぼテストケースを通過しているためクロスブラウザでもある程度安定して使用できると言えます。
Custom Elementsの具体的な使用例を上げると、GitHubのリポジトリ内での更新日付部分で使用されており、開発ツールで該当部分を見ると独自のHTML要素で定義されていることがわかります[2]。


VueコンポーネントをCustom Elementsとして配布する
Vue3.2から defineCustomElement というAPIが追加され、VueコンポーネントをCustom Elementsとして使用できるようになりました。
defineCustomElement の使い方
使い方自体はシンプルで、コンポーネントの拡張子を .vue から .ce.vue に変更して defineCustomElement で呼び出すことで、VueのコンポーネントをCustom Elementsとして使用できます。
import { defineCustomElement } from 'vue';
import Example from './Example.ce.vue';
// Custom Elementsのコンストラクタに変換
const ExampleElement = defineCustomElement(Example);
// Custom Elementsとして登録
customElements.define('vue-app-element', ExampleElement);
<!-- バンドルされたJSを読み込むと以下Custom Elementsが使用できます -->
<vue-app-element></vue-app-element>
公式のSFCツール(vue-loader@^16.5.0、@vitejs/plugin-vue@^1.4.0)では、"Custom Elements Mode" をサポートしており、ce.vue 拡張子でなくとも customElement オプションを追加することでCustom Elementsを呼び出せます。
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue({
customElement: true, // Custom Elements Mode
})],
})
defineCustomElement のサンプルコードとデモ
defineCustomElement のデモとして以下リポジトリを用意しています。
Viteを起動すると、描画されているVueコンポーネント部分がCustom Elementsとして表示されます。内部のボタンをクリックするとカウント数が増えることも確認できます。

実際の使用事例
次に defineCustomElement を実際にはどのように使われているかを紹介します。
VueやNuxtアプリケーション内でVueコンポーネントをわざわざCustom Elementsに変換して使用することはまずないと思います。VueやNuxtの世界外でVueコンポーネントを共通で使用する際に有用だと思います。
LINEヤフーさんの事例ではtoB向けとtoC向けでコンポーネントの共通化する際にCustom Elementsへ変換して使用しています。
GMOペパボさんの事例でもUIコンポーネントを提供する際に defineCustomElement を変換層として活用しています。
defineCustomElement はVueのランタイムに依存しているためビルドサイズも実装内容に応じて変動するため、単一のCustom Elementsのみを提供する場合はVueから作成するのは過剰となります。Custom Elementsの内容が複雑なロジックであったり、GMOペパボさんのように大規模なUIコンポーネント群として提供する際には向いていると言われています[3]。
v3.5から入った defineCustomElement の改善
そんな defineCustomElement ですがVue3.5のアップデートに伴い、様々な改善が入りました。以下はその改善された内容についてを紹介します。
custom-element: useShadowRoot() helper
Custom Elementsの shadowRoot を取得する useShadowRoot() ヘルパーが実装されました。
custom-element: useHost() helper
Custom Elementsの host を取得する useHost() ヘルパーが実装されました。
custom-element: expose this.$host in Options API
Options APIでCustom Elementsのホスト要素を公開できる this.$host 属性が実装されました。
custom-element: inject child components styles to custom element shadow root
子コンポーネントのスタイルをCustom Elementsの shadowRoot に挿入する機能が実装されました。
import { defineCustomElement } from 'vue';
import Root from './Root.ce.vue';
customElements.define('root-element', defineCustomElement(Root));
<!-- Root.ce.vue -->
<script setup>
import Child from './Child.ce.vue';
</script>
<!-- Child.ce.vue -->
<style>
div { color: red }; /* Root.ce.vue コンポーネントに挿入される style */
</style>
custom-element: support configurable app instance in defineCustomElement
defineCustomElement でCustom Elementsを登録する際に configureApp でVueアプリのインスタンスを設定できるようになりました。これによりdevtoolsでも defineCustomElement で作成されたコンポーネントを認識できるようになりました。
defineCustomElement({
// ...
}, {
configureApp(app) {
// ...
}
})
custom-element: support css :host selector by applying css vars on host element
:host セレクタをサポートするために、host 要素にCSS変数を適用する実装がされました。
custom-element: support emit with options
最初のイベント引数がオブジェクトである場合 CustomEvent のオプションオブジェクトとして使用されるようになりました。引数リスト全体は CustomEvent の detail プロパティを通して公開されます。
emit('event', { bubbles: true });
custom-element: support expose on customElement
defineCustomElementで expose をサポートされるようになり、Custom Elementsから参照できるようになりました。
const HelloExposeElement = defineCustomElement({
props: {
value: String,
},
setup(props, { expose }) {
expose({
value: 'hello',
})
return () => h('div', null, [props.value])
},
customElements.define('hello-expose-el', HelloExposeElement);
<!-- 以下のCustom Elementsにてvalueプロパティはすでに公開されている旨の警告が出る -->
<hello-expose-el value="world"></hello-expose-el>
custom-element: support nonce option for injected style tags
nonce オプションがサポートされて、Custom Elementsへ挿入されるstyleタグに nonce 属性を追加できるようになりました。
これはコンテンツ・セキュリティ・ポリシーを満たすために nonce 属性を含める必要があるため、追加されました。
custom-element: support passing custom-element-specific options via 2nd argument of defineCustomElement
defineCustomElement の第2引数に CustomElementOptions を渡せるようになりました。
custom-element: support shadowRoot: false in defineCustomElement()
defineCustomElement で shadowRoot: false の指定をサポートするようになりました。これによりShadow DOMを使わずにCustom Elementsを作成できるようになりました。
おわりに
この記事では defineCustomElement についてと、Vue3.5からの改善内容についてを紹介しました。今回の改善にでは2〜3年前から起票されていた問題を解消するものも含まれており、突然改善が含められた理由は明らかになっておりません(ご存知の方が居たら教えて下さい…)。
いずれにせよ、VueからのCustom Elementsの出力がより安定的になり、より多くの開発者で利用しやすくなることが期待されます。
Discussion