t28.dev

Vue3.2のdefineCustomElementで子要素のSFCのstyleも含めてWebComponents化する

2021/8/12
Tech

何この記事?

  • Vue3.2 で Vue Component を Web Components にする defineCustomElement が提供された。
  • <style>を持つ SFC を子要素以下にもつ SFC を Web Components 化する場合、現行(v3.2.1)の機能では追加で作業が必要。

defineCustomElement

ref. Vue and Web Components - Building Custom Elements with Vue

Vue v3.2.0 の新機能として defineCustomElement が提供されました。 これは Vue component から Web Components (正確には customElements#define するための CustomElement) を作成するためのメソッドです。

const MyVueElement = defineCustomElement({
  template: `...`,
});
customElements.define("my-vue-element", MyVueElement);

defineCustomElement は Vue Component 内の lifecycle や props を Web Components の lifecycle や attribute にいい感じで変換した CustomElement を返却してくれます。

SFC 内の style も Web Components に閉じ込める

ref. Vue and Web Components - SFC as Custom Element

<style> を持つ SFC に対して defineCustomElement を使う場合、少し設定が必要です。

通常、SFC を解釈するツール(vue-loader とか @vitejs/plugin-vue)を使うと、 <style> 内の CSS を抽出・結合して 1 つの CSS ファイルが作成されます。 SFC から Web Components を作成する場合、この CSS も shadow root に挿入することで style もカプセル化したいと思うのは自然です。

vue-loader@^16.5.0@vitejs/plugin-vue@^1.4.0 で “custom elements mode” という機能が追加されており、これを使用します。

import Example from "./Example.vue";
console.log(Example.styles); // ['/* css content */']

この機能によってインポートされた SFC は<style>内の CSS 文字列が styles property に代入されています。 defineCustomElement では、引数として渡した SFC が styles property を持っている場合 shadow root に style tag と CSS 文字列を挿入してくれるため、Web Components 化した SFC には style が適用されます。

SFC を custom element mode として解釈させるには、以下のように .ce.vue でファイルを作成するか

import Example from "./Example.ce.vue";

以下のように boolean, string, RegExp で対象とするコンポーネントを指定します。

export default defineConfig({
  plugins: [
    vue({ customElement: true }),
    // ...
  ],
  // ...
});

Web Components 化する SFC が子要素として SFC を持っている場合

defineCustomElement に渡す SFC(RootComponent)の子要素に SFC(ChildComponent)があり、その ChildComponent が style tag を持っている場合、更に対応が必要です。

現行(v3.2.1)のdefineCustomElementでは RootComponent の style のみshadow root に挿入する ため、ChildComponent の style は無視されてしまいます。 そのため、

  1. RootComponent 配下の SFC を全て取得して
  2. style property を持っているかを確認して
  3. RootComponent 配下の CSS 文字列を全て取得して
  4. 全ての CSS 文字列をdefineCustomElementに渡す

必要があります。

今のところやっていること

上記 1~3 を行うための関数を定義します。

const getStylesRecursively = (
  component: Component & {
    components?: Record<string, Component>;
    styles?: string[];
  },
): string[] => {
  // root の SFC から最下層の SFC までの style (CSS文字列) を入れる配列
  const customElementStyles: string[] = [];

  // custom elements mode で import された SFC は styles propety を持っている
  if (component.styles) {
    customElementStyles.push(...component.styles);
  }

  // 子要素として使用する SFC は components に登録されている
  const childComponents = component.components;
  if (childComponents) {
    Object.keys(childComponents).forEach((name) => {
      const childComponent = childComponents[name];
      // 子要素の style を取得するために再帰的に getStylesRecursively を呼ぶ
      const styles = this._getStylesRecursively(childComponent);
      customElementStyles.push(...styles);
    });
  }

  return customElementStyles;
};

上記 4 は、 「customElementStyles を styles property として持ち、直下に RootComponent のみを描画する VueComponent」を defineCustomElement に渡すことで実現します。

const myCustomElement = defineCustomElement({
  styles: getStylesRecursively(RootComponent),
  render: () => h(RootComponent),
});

customElements.define("my-element", myCustomElement);

SFC がより小さい SFC を配下に持たせるのはコンポーネントの設計としては自然だし、オフィシャルでサポートしてくると嬉しいですね…🤔