SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

Vueユーザーが感じたSvelteのおもしろい機能を紹介する

スマートキャンプでエンジニアをしている瀧川です!

2月に育休を取得し、3月に復帰したと思ったらコロナでリモートワーク、そしてチーム異動となかなか落ち着かない今日このごろ。 みなさんいかがお過ごしでしょうか?

今回家にいる時間が多くなり、せっかくだから新しいことしたいよなーということで、以前から気になっていた Svelteを触ることにしました!

Svelteの紹介記事では、「Vue.jsと構文が似ているため習熟が簡単」「Vue.jsの50倍早い」みたいなところにフォーカスされることが多いかなと思いますが、本記事ではSvelteのTutorialをやるなかで、フレームワーク(ライブラリ)の機能として普段Vue.jsを利用している私がおもしろいなーと思ったものをご紹介したいと思います。

Svelteとは

Svelteとは、ReactやVue.jsのようなフレームワークと同様にSPAに代表されるインタラクティブなUI/UXを簡単に構築するために、コンポーネント指向やリアクティブシステムなどを備えたツールです。

大きな違いは、ReactやVue.jsがフレームワークとしてブラウザで動作する際に機能するのに対し、Svelteはコンパイラであり、ビルド時に理想的な素のJavaScriptに変換します

そのためフレームワークのオーバーヘッドがほぼなくなり、何十倍という高速化に成功しているとのことです。

後発であるため、機能自体にも特徴があり本記事ではそちらにフォーカスして紹介していきます。

※ Svelteの公式サイトはコンテンツが多く、インタラクティブなチュートリアルやサンプルが整備されていてとてもわかり易いので、時間のある方はぜひ見てみてください!

svelte.dev

基本文法

Vue.jsを使われてる方であれば、以下のサンプルである程度Svelteの文法や基礎機能は把握できるのではないでしょうか。

App.svelte

<script>
   import SampleComponent from './SampleComponent.svelte';

   let count = 1; // data相当

   $: doubled = count * 2; // computed相当
   $: quadrupled = doubled * 2;

   function handleClick() { // methods相当
       count += 1;
   }
</script>

<button on:click={handleClick}>
    <SampleComponent count={count} />
</button>

<p>{count} * 2 = {doubled}</p>

{#if doubled > 0} // v-if相当
    <p>{doubled} * 2 = {quadrupled}</p>
{/if}

SampleComponent.svelte

<style> // デフォルトでscopedになっている
    p {
        color: purple;
        font-family: 'Comic Sans MS', cursive;
        font-size: 2em;
    }
</style>

<script>
   export let count = 0; // props相当
</script>

<p>Count: {count}</p>

基本的にはscriptタグ、 styleタグ、その他(template相当)といった構成になっています。 (styleタグはデフォルトでscopedになっているようです)

Vue.jsでいうところのdataがscriptタグ内のletでの変数定義になっており、methodsは単純な関数定義(function)となっています。

他のコンポーネントを呼び出す際はインポートするだけで利用でき、 export let で定義された props 相当に値を受け渡すことができます。 (propsのtypeやvalidatorはまだないようでした)

少し見慣れない記法でいうと、computed$: computed名 = 式のように記述します。

またv-ifv-for相当の制御構文も{# if }{#each cats as { id, name }, i}のような記法になっています。

その他にも Lifecycle EventSlot といったコンポーネントの基本機能はサポートされています。

特徴的な機能

ここからVue.jsには存在しない機能や、使い勝手が向上している機能など紹介していきます!

propsやclassの省略記法

以下のように変数(data)名とprop名やclass名が同一だった場合、省略記法が用意されています。

地味に便利ですね!

<SampleComponent count={count} class:big={big} />
<SampleComponent {count} class:big />

Await Block

Vue.jsに限らず非同期処理によるローディングなどは、実装が煩雑になるイメージがあります。

SvelteではPromiseの状態によるUIの変更を直にテンプレート内に記述することができます。 これにより、シンプルにローディングやエラー時の表示変更などが実装できるようになっています。

<script>
   let promise = getWaitFunction();

   async function getWaitFunction() {
       const res = await fetch(`hoge`);
       const text = await res.text();

       if (res.ok) {
           return text;
       } else {
           throw new Error(text);
       }
   }

   function handleClick() {
       promise = getRandomNumber();
   }
</script>

<button on:click={handleClick}>
    Wait Button
</button>

{#await promise}
    <p>...waiting</p>
{:then result}
    <p>Result is {result}</p>
{:catch error}
    <p style="color: red">{error.message}</p>
{/await}

Reactive Statement (watch相当)

これはおそらくVue.jsのwatchに近い機能だと思います。

computedと似た記法で$: ブロックとすることで、そのブロック内で使われている変数(data)が変更された際に、そのブロックが再評価されるようです。

使い勝手良さそうな反面、個人的にはwatchと比べて宣言的でないため若干怖いなとも思っています。

<script>
   let count = 0;

   // countの変更で発火
   $: if (count >= 10) {
       alert(`count is dangerously high!`);
       count = 9;
   }

   // countの変更で発火
   $: {
       console.log(count)
   }

   function handleClick() {
       count += 1;
   }
</script>

<button on:click={handleClick}>
    Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

Event fowarding (emit相当)

Svelteでは子コンポーネントから親コンポーネントへのイベント波及は、createEventDispatcherを使います。

特徴的なのは、多段のコンポーネント呼び出しです。

Vue.jsでは末端コンポーネントで発生したイベントを最上位のコンポーネントで使いたい場合、間のコンポーネントでhandlingとemitを繰り返す必要があります。

Svelteでは以下のように中継コンポーネントでon:イベント名のように記述するだけで、親にイベントを受け流してくれるようになっています

またDOMイベントon:clickなども同様にできるようなので、Vue.jsのnativeのような役割もありそうです。

End.svelte

<script>
   import { createEventDispatcher } from 'svelte';

   const dispatch = createEventDispatcher();

   function sayHello() {
       dispatch('message', {
           text: 'Hello!'
       });
   }
</script>

Middle.svelte

<script>
   import End from './End.svelte';
</script>

<!-- 親コンポーネントにイベントを波及 -->
<End on:message/>

Lifecycle Event

Lifecycle EventはonMoundonDestroybeforeUpdateafterUpdateと特にVue.jsとも変わらないです。

しかし以下のようなコンポーネント外でもLifecycle Eventを使えるのは特徴的かなと思います。

これによりコンポーネント側の責務が減り、ライブラリ内で完結して終了処理まで実装することができます

util.js

import { onDestroy } from 'svelte';

export function onInterval(callback, milliseconds) {
    const interval = setInterval(callback, milliseconds);

    onDestroy(() => {
        clearInterval(interval);
    });
}

App.svelte

<script>
   import { onInterval } from './utils.js';

   let seconds = 0;
   onInterval(() => seconds += 1, 1000); // このコンポーネントが破棄されるとclearIntervalされる
</script>

Store

Vue.jsではコンポーネント間での状態管理はvuexがデファクトスタンダードだと思いますが、Svelteでは標準でStoreの実装が存在します。

Writable, Readable

Store定義自体はとてもシンプルで、以下のようになります。

stores.js

import { writable, readable } from 'svelte/store';

export const count = writable(0); // subscribe, set, update
export const immutableCount = readable(0, function start(set) { // 参照のみ
    // この中でのみ値を更新できる
    // e.g. 定期処理など
});

Storeの参照方法はSubscribeとAuto subscriptionがあります。

Subscribeは以下のようにStoreの値が変更されたことを検知する方法です。

こちらの場合、同時にcomputedのようなことや、Unsubscribe(検知されなくする)ができるのが特徴です。

<script>
   import { count } from './stores.js';
   let count_value;
   const unsubscribe = count.subscribe(value => {
       count_value = value;
   });
</script>

<p>{count_value}</p>

一方のAuto subscriptionは単純にStoreの値を取得したい場合に使える省略記法となります。

<script>
   import { count } from './stores.js';
</script>

<!-- $Storeの値名 -->
<p>{$count}</p>
Derived (Getters相当)

WritableかReadableの値を使って、別の値を生成する機能です。

stores.js

import { derived, writable } from 'svelte/store';

export const number = writable(0)

export const double = derived(
    number,
    $number => $number * 2
);
Custom (Actions相当)

Storeを更新する関数を定義することができます。

stores.js

import { writable } from 'svelte/store';

function createCount() {
    const { subscribe, set, update } = writable(0);

    return {
        subscribe,
        increment: () => update(n => n + 1),
        decrement: () => update(n => n - 1),
        reset: () => set(0)
    };
}

export const count = createCount();

Transition

Vue.jsでもTransitionはサポートされていますが、Svelteではすでに定義済みのTransitionが多く存在するためお手軽に実装することができます

最もシンプルな例は以下のようになります。

svelte/transitionには現在fade、blur、slide、draw、scale、fly、crossfadeが定義されており、タグにtransitionディレクティブを指定するだけで表示・非表示切替時にTransitionをつけることができます。

またin、outというディレクティブを設定することで、表示・非表示それぞれでTransitionを設定することもできます。

<script>
   import { fade, scale } from 'svelte/transition';
   let visible = true;
</script>

<label>
    <input type="checkbox" bind:checked={visible}>
    visible
</label>

{#if visible}
    <p transition:fade>
        Fades in and out
    </p>
    <p in:fade out:scale>
        Fades in and Scale out
    </p>
{/if}
パラメータ

Svelteで定義されたTransitionにはパラメータが渡せるようになっており、それによってDurationなどをカスタマイズすることになります。

例えばflyであれば、delay、duration、easing、x、y、opacityが指定できるようです。

<script>
   import { fly } from 'svelte/transition';
   let visible = true;
</script>

<label>
    <input type="checkbox" bind:checked={visible}>
    visible
</label>

{#if visible}
    <!-- 2秒かけてy軸に200px移動 -->
    <p transition:fly="{{ y: 200, duration: 2000 }}">
        Flies in and out
    </p>
{/if}
Custom Transition

Transitionを自由に定義する方法はCustom CSS transitionsやCustom JS transitionsの2つあります。

これに関してはVue.jsでもSvelteでも泥臭くはなってしまう印象ですが、Svelteではsvelte/easingにEasingの関数が定義してあったりとサポートが厚いかなと思います。

CSSTransition.svelte

<script>
   import { elasticOut } from 'svelte/easing';

   let visible = true;

   function spin(node, { duration }) {
       return {
           duration,
           css: t => {
               const eased = elasticOut(t);

               return `
                  transform: scale(${eased}) rotate(${eased * 1080}deg);
                  color: hsl(
                      ${~~(t * 360)},
                      ${Math.min(100, 1000 - 1000 * t)}%,
                      ${Math.min(50, 500 - 500 * t)}%
                  );`
           }
       };
   }
</script>

特殊なタグ

svelte:head, svelte:body, svelte:window

Nuxt.jsではheadプロパティをコンポーネントに設定することでヘッダーに要素を設定することができますが、Svelteでは以下のように特殊タグを利用することで埋め込むことができます。

またbodyやwindowに対してイベント設定や値のbindも設定できるのはおもしろいですね。

<svelte:head>
    <link rel="stylesheet" href="hoge.css">
</svelte:heaad>

<svelte:body
    on:mouseenter={handleMouseenter}
    on:mouseleave={handleMouseleave}
/>

<svelte:window on:keydown={handleKeydown}/>

<svelte:window bind:scrollY={y}/>
svelte:component

Vue.jsでは条件に応じて表示するコンポーネントを変える場合、テンプレート内でv-ifを使っているかなと思います。

Svelteでは{#if } で切り替える方法の他に、以下のように記述することができます。 こうすることで、コンポーネント切り替えのロジックをscriptに寄せられるため、より柔軟に条件など記述できるようになります。

<script>
   import Yes from './Yes.svelte';
   import No from './No.svelte';
   
   let isChecked;
   
   $: selected = isChecked ? Yes : No;
</script>

<input type="checkbox" bind:checked={isChecked}/>

<svelte:component this={selected}/>

Module Context

scriptとは別に<script context="module">を定義することで、最初に一回だけ評価される、すべての同一コンポーネントで共通のデータやメソッドを定義することができます。 (シングルトンや静的クラスのようなイメージでしょうか?)

あくまで限定的な用途を想定しており、moduleで宣言した変数はリアクティブではなく、scriptで変更しても再レンダリングはされないので注意が必要そうです。 (使いみちが難しいですね...)

App.svelte

<script>
   import Component1, {alertTotal} from './Component1.svelte'
</script>

<Component1/>

<button on:click={alertTotal}>alert</button>

Component1.svelte

<script context="module">
   let totalCount = 0;

   export function alertTotal() {
       alert(totalCount);
   }
</script>

<script>
   function handleClick(){
       totalCount += 1;
   }
</script>

<button on:click={handleClick}>countup</button>

<!-- 以下は 0 のまま変わらない... -->
<p>{totalComponents}</p>

Debugging

デバッグ用のブロックも組み込まれています。

{@debug 変数名}のように記述すると、指定した変数が変化した場合にconsoleに出力してくれるようです。

{@debug} とすると、debuggerが埋め込まれるみたいですね。

<script>
   let user = {
       firstname: 'Ada',
       lastname: 'Lovelace'
   };
</script>

<input bind:value={user.firstname}>
<input bind:value={user.lastname}>

{@debug user}
{@debug}

まとめ

今回Vue.jsユーザーである私が、Svelteの機能にフォーカスしておもしろさを紹介させてもらいました。

後発ということでVue.jsやReactでの煩わしさを解消する機能だったり、Nuxtやvuexといったエコシステムでサポートされている機能が標準で組み込まれており、シンプルに利用できるツールだなと思いました。

まだエコシステムが未成熟だったりTypeScript未対応だったりが要因で国内での採用事例は少ないですが、このあたりの整備が進むと爆発的に流行る可能性はあるなと感じています。

弊社では今、フロントエンドをより早くより高い質(UI/UX)で実現できる方法を模索しています。 Svelteもその一つかもしれないと考えています。

これを読んでいただいた方で、もしフロントエンドの改修や技術推進に興味がありましたら、ぜひご連絡いただければと思います!

ありがとうございました!