Vue2, Vue3 による Web components の出力について深ぼる
2021/11/12
Tech
- 例によって UIT INSIDE に向けて、 Vue の Web components ビルドについて色々文書化しておく
- これを書いた時点の Vue3 は v3.2.21
defineCustomElement の概要
Vue v3.2 で追加された defineCustomElement
公式ドキュメント: Vue and Web Components - defineCustomElement
defineCustomElement
=== VueComponent を HTMLElement で wrap するための関数- 正確には…
- 引数は
defineComponent
が受け取れる引数と同等のもの - 戻り値は HTMLElement を継承した
VueElement
- 引数は
- 正確には…
- wrap した
HTMLElement
をcustomElements.define
に渡して Web components を定義する
Vue v2 では @vue/web-component-wrapper
があった
Repo: github.com/vuejs/vue-web-component-wrapper
- 使い方は
defineCustomElement
とほぼ同じ- VueComponent と Vue Object を渡して、返ってきた
HTMLElement
をcustomElements.define
する。
- VueComponent と Vue Object を渡して、返ってきた
Web components の作り方
Vue2 & Vue CLI
- Vue CLI - Build Targets - Web component
$ vue-cli-service build --target wc --name my-element [entry]
Vue3 & Vue CLI
- Vue CLI は v5.0.0-rc 時点で Vue3 の Web components ビルドは非サポート なので、Loader の設定をカスタマイズする必要がある
-
Vue Component から HTMLElement を作って、登録する (公式ドキュメントのママ)
import { defineCustomElement } from "vue"; import Example from "./Example.ce.vue"; const ExampleElement = defineCustomElement(Example); customElements.define("my-example", ExampleElement);
-
@vitejs/plugin-vue 、vue-loader の設定
- vue-plugin, webpack-loader は通常、 style 文字列を
<head>
内に挿入しますが、Web components の場合は shadow root 内に挿入して欲しいです。 - そのためのおまじない機能が
defineCustomElement
のリリースに合わせて追加されています。
// vite.config.ts export default defineConfig({ plugins: [vue({ customElement: true })], });
- ちなみに、上記
import Example from "./Example.ce.vue";
の用に ファイル名から対象を選択することも出来ます。
- vue-plugin, webpack-loader は通常、 style 文字列を
defineCustomElement
の実装を見てみる
- VueElement class
- Shadow root に VNode を描画 したり
- Web components 側の attributes を監視 して再描画したり
- Vue 側の emit を CustomElement として dispatch したり
constructor
でmode: 'open'
で Element#attachShadow
connectedCallback
で- MutationObserver で attributes を監視
- Vue component の props を取得
- Number なら Number として扱う
- Vue component の styles を取得
- style element を作って、ShadowRoot に appendChild
@vue/runtime-core
の renderer に渡して、ShadowRoot に VNode を描画する- emit は
dispatchEvent(new CustomEvent())
に変換
- 感想
- 「Vue の Core API(外部ライブラリじゃない)」らしい実装になっている
- Vue2 では、Vue インスタンスを作って 、 shadowRoot に appendChild していた。
考慮しないといけないポイント
Vue の props と Web components の attribute の扱い
- Vue は js で props の定義を行うので、この props を読み取って、いい感じに Web components の attribute に変換してくれると思った
- => 違った 😭
- Number で扱えそうなら(
parseFloat
)、Number に 変換 - Number で扱えなさそうなら、そのまま
- Number で扱えそうなら(
- 困ったこと
- 構文解析に使っている
parseFloat()
は厳密な解析ではないので…出来る範囲で Number 化してしまう。- 先頭文字が数字であれば、とりあえず数字の部分を抜き出して Number
- 1000-0000-0000 みたいな文字列のクーポンコードも Number として解釈される
- 構文解析に使っている
- PR で修正したいが…。
- 対象の vue 内共通モジュールの参照先が多くて、変更が厳しそう…。
- fix(shared): Parse string value including a leading number as string value #4946
style の扱い
- 子要素の SFC 内の style もまとめて ShadowRoot に挿入してくれると思った。
- => 違った 😭
defineCustomElement
は wrap する SFC 自身の style のみを読み込んで、ShadowRoot に挿入する- wrap する SFC の子コンポーネントの style は挿入されない
- 困ったこと
- 入れ子構造になっている SFC で構成された VueComponent をそのままでは Web components 化出来ない。
- Evan さん的には、子コンポーネントも Web components 化して使って欲しいらしい
- ref: #4309
- Vue から Web components を出力するケースって、対象のコンポーネントが比較的大きい(複数の SFC に分けたい)ものだと思うけれど…。
- PR で修正したいが…。
- 現状の vue と loader だと、style や依存コンポーネントを取得できないケースがあるので、
defineCustomElement
内の機能に出来ない…。 - feat(runtime-dom): Apply nested component styles when using defineCustomElements #4309
- 現状の vue と loader だと、style や依存コンポーネントを取得できないケースがあるので、