🤔

Svelteでweb componentsを開発するときのTIPS

2023/09/24に公開

はじめに

Chrome拡張機能を開発している時のこと。
Vueで開発を進めていましたが、Tree ViewのUIが必要になったため要件がカバーできる公開ライプラリを探していました。

探し回りましたが、自分が思っていたものと操作性や機能が若干違ったり、または要件カバーしていないものしかなかったため(もっと探せばありそうですが...)、「じゃあ作るか」、と思ったのがきっかけです。

モチベーション

Vue使ってるんだからVueで開発すれば良いかと思いましたが、Vueで作って、仮にnpmで公開した際にVueでしか使えないライブラリとなってしまう、という思いから

  1. Vueのライブラリとして公開するとVue限定のライブラリになってしまう。
  2. それならweb componentsとして開発すればその他のJS FWでも再利用できるのでは?

と考えました。

もちろんVueでもweb componentsを作ることができますがどうせなら新しいFWということでSvelteを利用しています。
SFCでVueの記法と似ているという点もSvelteを選択した理由の一つです。

開発を進めていく際に得たナレッジをTipsとしてまとめようと思い、備忘録がわりに紹介していきます。

ちなみにSvelte 3.55.1でのお話です。

web componentsとは

ウェブコンポーネントは、一連のさまざまな技術です。これにより、再利用可能なカスタム要素を作成し、その機能を他のコードから分離してウェブアプリケーションで利用できるようにします。

引用: mdn web docs - ウェブコンポーネント

ざっくりいうとJSのAPIを利用しカスタム要素を定義、シャドウ DOMを利用してカプセル化[1]し、ルートとなるカスタム要素にアタッチします。

主要FWであるVue、Reactでのweb components利用方法

セットアップ

まずはSvelteの新規PJを作成します。
今回はコンポーネント開発がメインのため、webアプリケーションに必要な機能を備えているSvelteKitの利用は見送りました。(ほぼ使用しない)

yarn create vite myapp --template svelte-ts

web componentsの構成

今回は以下のようなコンポーネント構成となります。
以下のdirに格納されています。

myapp/src/lib
  • Tree.svelte
    • tree view本体コンポーネント
  • Treeitems.svelte
    • tree viewの各アイテムを再起的に描画するコンポーネント
  • Treeitem.svelte
    • tree view個別の要素

開発の進めていく流れでTIPSを紹介していきます

各ファイルをカスタム要素としてコンパイルする

各ファイルのsvalteタグにoptionsでカスタム要素名を指定します。

Tree.svelt
<svelte:options tag="tree" />

// インポートは通常通り(パスエイリアスを設定しています)
import Treeitem from '$lib/Treeitems.svelte';
Treeitems.svelte
<svelte:options tag="tree-items" />

// インポートは通常通り
import Treeitem from '$lib/Treeitem.svelte';
Treeitem.svelte
<svelte:options tag="tree-item" />

参考: Custom elements API

下位コンポーネントもカスタム要素として指定しています。
Sveltをweb componentsとしてビルドしたい場合は全ての下層コンポーネントもカスタム要素として登録する必要があるみたいです。

カスタム要素としてビルドしますが、Sveltコンポーネントとしての記述が可能です。

参考: Svelte で Web Components を開発するときの Tips (2021年7月時点)

カスタム要素としてビルドする設定の有効化

コンパイラオプションの変更を行います。

svelte.config.js
export default {
  // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
  // for more information about preprocessors
  preprocess: vitePreprocess(),
+ compilerOptions: {
+   customElement: true
+ }
};

ViteのHMRを有効にしたい

更新されたファイルの内容が自動で描画されるHMRは便利でweb componentsの開発でも利用したいと思っていましたが、デフォルトの設定だとHMRがうまく動作してくれません。

ブラウザの再読み込みとは違い、更新がかかったモジュールのみを再描画する関係上、以下のエラーが発生して再描画が失敗してしまいます。

Failed to execute 'define' on 'CustomElementRegistry': the name "component name" has already exsit...

すでに登録済みの同名カスタム要素を再度登録する時に起こるエラーです。
モジュールの再描画だと再度カスタム要素の登録が走ってしまいます。

参考: CustomElementRegistry.define()

そこでViteのHMRの設定を都度full reloadするように変更すればHMRが使用できます。
(都度ブラウザのリロードが発生する形に変更)

ViteはHMR APIを提供しているので、こちらを活用して設定変更を進めていきます。

vite.config.ts
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import reloadSettings from './vite-plugins/reloadSettings';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    svelte({
+     include: ['./src/**/*.svelte'],
+     hot: true
    }),
    reloadSettings()
  ],
// importパスにエイリアスを設定したかったので追加
+ resolve: {
+   alias: {
+     $lib: `${__dirname}/src/lib`,
+   }
+ }
});
vite-plugins/reloadSettings.ts
+import { PluginOption } from 'vite';
+
+export default function reloadSettings(): PluginOption {
+ return {
+   name: 'sync-reload',
+   // プラグインの実行順序
+   enforce: 'pre',
+   // dev server only
+   apply: 'serve',
+
+   handleHotUpdate({ server }) {
+     server.ws.send({
+       type: 'full-reload',
+       path: '/src/lib/TreeItem.svelte'
+     });
+
+     return [];
+   }
+ };
+}

viteはRollupベースのプラグイン拡張を提供しています。
プラグインAPI[2]とHMR API[3]を使用してHMRの動作を変更しています。

イベントの設定

Svelteの組み込みイベントハンドラーでイベントを定義してもweb componentsとしてマウントする場合、DOMにイベントが送信されません。

その場合は別途JavaScript API[4]を使用してカスタムイベント[5]として登録させます。

Treeitem.svelte
<svelte:options tag="tree-item" />

<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  import { get_current_component } from 'svelte/internal';

  const component = get_current_component();
  const originalDispatch = createEventDispatcher();
  
  export let item: TreeItems = {
    id: 0,
    name: '',
    slot: '',
    isShow: true,
    class: '',
    dividerClassBefore: '',
    dividerClassAfter: '',
    node: []
  };
  
  const dispatch = (name, detail) => {
    originalDispatch(name, detail);
    component?.dispatchEvent(new CustomEvent(name, { detail }));
  };
  
  // dragが開始された時にidを送出
  const dragStart = (id: number) => {
    dispatch('dragging', { id });
  };
</script>

...
<ul>
...

<li
 on:dragstart={() => dragStart(item.id)}
...
</ul

参考: Svelte で Web Components を開発するときの Tips (2021年7月時点)

おわりに

まだ注意すべき点、ハマりそうな点はあるかと思いますが、その他コメントなどいただけると嬉しいです。
Svalte面白い。

脚注
  1. シャドウ DOMは通常のDOMツリーとは別に描画され要素が隠蔽、分離されます。 ↩︎

  2. https://vitejs.dev/guide/api-plugin.html ↩︎

  3. https://vitejs.dev/guide/api-hmr.html ↩︎

  4. https://developer.mozilla.org/ja/docs/Web/API/EventTarget/dispatchEvent ↩︎

  5. https://developer.mozilla.org/ja/docs/Web/API/CustomEvent ↩︎

Discussion