🔌

Svelte と Riot v3 の文法比較

2022/01/31に公開

はじめに

Svelte という JavaScript フレームワークについて, 今更ながら本気でキャッチアップして色々と良さに気づいたので久々にエントリーを書いてみました

今まで spat(自作の Riot 用フレームワーク. Vue でいう Nuxt 的なもの)でプロダクトを作ってきたんですが, 久々に Svelte をキャッチアップしていたらかなり機能が改善, 追加されて実用的になっていたので実際に一部のプロダクトで採用してみてたりしています.

そんな中で、学んだ Svelte と Riot とのシンタックス的な違いをまとめてみました.
Riot から Svelte に最短で移行したいって人はそこそこキャッチアップの参考になるかと思います!(弊社のメンバーぐらい?w)

※以下 Riot と書いているのはすべて v3 についてになります.

ベース

コンポーネントのベースとなる書き方になります.

Riot

my-component.riot
<my-component>
  <!-- view -->
  <h1>{title}</h1>

  <!-- style -->
  <style>
    :scope h1 {
      font-size: 26px;
    }
  </style>

  <!-- script -->
  <script>
    this.title = 'Hello, Riot!';
  </script>
</my-component>

Svelte

Svelte の場合は, 基本 import して使うのでファイル内にコンポーネント名を指定せずにいきなり HTML を書いていく形になります.
script, style, view の順番についても順不同です.

Svelte ではそのコンポーネント内のローカル変数を View に埋め込むことができます.
style については, Riot で必要だった :scope:root といった疑似要素は不要です.

MyComponent.svelte
<!-- script -->
<script>
  let title = 'Hello, Svelte!';
</script>

<!-- style -->
<style>
  h1 {
    font-size: 26px;
  }
</style>

<!-- view -->
<h1>{title}</h1>

テンプレート変数(式の埋め込み)

ほぼ書き方は同じで { /* 式を書く */ } という形で HTML 内に変数を埋め込んだり処理の実行結果を表示したりといったことができます.

Riot

<div>{2 + 4}</div>
<div>{Math.round(5.5)}</div>

Svelte

<div>{5 + 10}</div>
<div>{Math.round(4.5)}</div>

イベントリスナー

Riot

onclick 属性にメンバ関数を渡すことでイベントリスナーを登録することができます.

<button onclick='{hello}'>Hello</button>
<script>
  this.hello = () => {
    alert('Hello, Riot!');
  };
</script>

Svelte

on:click 属性に(ローカル)関数を渡すことでイベントリスナーを登録することができます.

<script>
  let hello = () => {
    alert('Hello, Riot!');
  };
</script>

<button on:click={hello}>Hello</button>

クラス属性

Riot

オブジェクトを渡すと value の結果が true の key のみをクラスとして出力してくれます

<div class="{bold: true, red: red}">Hello</div>
<script>
  this.red = false;
</script>

Svelte

class:クラス名={条件} という形でクラスの出し分けができます.
またクラス名とフラグの名前が一致している場合は value を省略できます.

<script>
  let red = false;
</script>
<div class:bold={true} class:red>Hello</div>

条件分岐(if 文)

Riot

if='{条件式}' で出し分けすることができます.

<div if='{flag}'>
  ...
</div>

Svelte

{#if 条件式} ~ {/if} で囲うことで出し分けが可能です.
また間に {:else}{:else if 条件式} を挟むこともできます.

{#if flag}
  <div>
    flag が true のとき
  </div>
{:else}
  <div>
    flag が false のとき
  </div>
{/if}

svelte-preprocess 経由で pug で書いている場合は以下のように別の記法が用意されています.

https://github.com/sveltejs/svelte-preprocess/blob/a239e829295bde5f62383697266cefe7767dd0e2/src/transformers/pug.ts#L7

+if('flag')
  div flag が true のとき
  +else
    div flag が false のとき

ちょっとネストしていくのは違和感ありますが慣れましょう!

ループ処理(for, each)

Riot

each in でループ処理を実現できます.

<div each='{item,index in items}'>
  <h3>{index}. {item.title}</h3>
  <p>{item.description}</p>
</div>

Svelte

{#each 配列 as 要素,インデックス} ~ {/each} という形でループ処理を実現することができます.

{#each items as item,index}
  <div>
    <h3>{index}. {item.title}</h3>
    <p>{item.description}</p>
  </div>
{/each}

if 分同様 pug の場合は別の記法が用意されています.

+each('items as item,index')
  h3 {item.title}
  p {item.description}

データバインディング

Riot

ref 属性に名前をつけると this.refs[ref属性に付けた名前] でアクセスできます.

<input ref='name' />
<div>あなたの名前は: {refs.name.value} です!</div>

Svelte

bind:value に紐付けたい変数を指定することで要素に JavaScript からアクセスできるようになります.

<script>
  let name = '';
</script>
<input bind:value={name} />
<div>あなたの名前は: {name} です!</div>

子コンポーネントへの変数受け渡しと参照方法

Riot

親コンポーネント側
<app>
  <item-article item='{item}'></item-article>
  <script>
    this.item = {
      title: 'Hello, Riot!',
    };
  </script>
</app>
子コンポーネント側
<item-article>
  <h3>{opts.item.title}</h3>
</item-article>

Svelte

親コンポーネント側
<script>
  let item = {
    title: 'Hello, Svelte!',
  };
</script>
<Article item={item}></Article>
子コンポーネント側
<script>
  export let item;
</script>

<h3>{item.title}</h3>

yield, slot

Riot

配置側で設置した content は, 子コンポーネント側で yield タグを使うことで参照できます!
(v4 以降では slot)

親コンポーネント
<app>
  <article title='Hello, world!'>これは解説エントリーです!</article>
</app>
子コンポーネント
<article>
  <h3>{ opts.title }</h3>
  <yield />
</article>

Svelte

Svelte では slot タグで親側で配置した content を参照, 展開できます!

親コンポーネント
<script>
  import Box from './Article.svelte';
</script>

<Article>
  <h2>Hello!</h2>
  <p>This is a box. It can contain anything.</p>
</Article>
子コンポーネント
<div class="article">
  <slot></slot>
</div>

mount イベントリスナーの登録

Riot

<script>
  this.on('mount', () => {
    ...
  });
</script>

Svelte

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

  onMount(() => {
    ...
  });
</script>

イベントの発火

Riot

子コンポーネント
<inner>
  <button onclick='{hello}'></button>
  <script>
    this.hello = () => {
      this.trigger('action', {
        type: 'press',
      });
    };
  </script>
</inner>
親コンポーネント
<app>
  <inner ref='inner'><inner>
  <script>
    this.refs.inner.on('action', (e) => {
      alert(e.type);
    });
  </script>
</app>

Svelte

createEventDispatcher を生成してそれを呼ぶことでイベント発火することができます.

子コンポーネント
<script>
  import {createEventDispatcher} from 'svelte';

  const dispatch = createEventDispatcher();

  function hello() {
    dispatch('action', {
      type: 'press',
    });
  }
</script>
親コンポーネント
<script>
  import Inner from './Inner.svelte';

  function handleMessage(event) {
    alert(event.detail.type);
  }
</script>

<Inner on:action={handleMessage}/>

HTML をそのまま表示する

そういった場面も出てくると思います.
ただサニタイズされなくなるのでユーザーの入力した値等では使わないよう気をつけてください.(XSS攻撃)

Riot

riot では html をそのまま展開するシンタックスは用意されていないので無理やり innerHTML に代入するしかありません.

<raw>
  <span></span>

  this.root.innerHTML = opts.content
</raw>

Svelte

Svelte では {@html ...} という記法で簡単に HTML をそのまま展開することができます!

<script>
  let message = "Hello, <b>Svelte</b>!";
</script>

<div>{message}</div>
<div>{@html message}</div>

https://svelte.jp/tutorial/html-tags

mount するコンポーネントを変数化

タブバーに対応したコンテンツを表示する際等に応用できます.

Riot

Riot では data-is 属性に文字列を渡すとその名前のコンポーネントを展開してくれます.
変数も使えるのでそこに

<!-- item-book タグが展開される -->
<div data-is='item-{component}'></div>
<script>
  this.component = 'book';
</script>

Svelte

Svelte では <svelte:component this={コンポーネント}> で動的にコンポーネントを展開することができます.

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

  let component = Book;
</script>

<!-- Book コンポーネントが展開される -->
<svelte:component this={component}/>

動的 mount

独自コンポーネントライブラリを作ったりする際には JavaScript 完結でコンポーネントを生成したり
コントロールしたりといったことが必須になってきます.

以下はその方法になります.

Riot

riot.mount() 関数にマウント対象となる DOM 要素, タグ名, オプションを渡すことでマウントすることができます.

let app = riot.mount(elm, 'app', {
  item: item,
})[0];

Svelte

Svelte では import した svelte コンポーネントそのものが mount するために関数になっています.
その関数に対象となる DOM要素(target), オプション(data) を渡すことで JavaScript 完結でマウントすることができます.

import App from './App.svelte';

let app = new App({
  target: elm,
  data: {
    item: item,
  }
});

Svelte の便利機能

Riot にはなくて Svelte にだけある便利機能をまとめました

グローバルな関数登録

以下のようにコンポーネント内でのみ有効にしたい window や document に登録する関数を指定することができます.

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

こう書くことでこのコンポーネントが破棄(destroy)されたタイミングで removeEvnetListener が自動で呼ばれるようになり管理コストが減ります.

TODO: その他諸々追記してく予定...

おわりに

個人的にはずっと Riot, 特に v3 の思想やシンタックスが大好きで色々と足りないところは fork して拡張しながら実際のプロダクトで作ってきていたんですが, 色々と模索するなかで課題もあり, Svelte といった後発のイケてるライブラリもキャッチアップしていかないとなと改めて感じました.

Svelte/SvelteKit の良いところを学んで, また Riot に戻ってきてそれまでに学んだ知見を活かしていくなんて道もまだ諦めてなかったりします.
どういう結果になるにしろ, いろんな技術やライブラリを学んで開発者の思想に浸りながら開発していくのは楽しいものですね.

完全に社内メンバー向けエントリーで, かつマニアックなライブラリの対比にはなってしまったんですが参考になれば幸いです.

Discussion