25
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Riot.jsAdvent Calendar 2019

Day 24

Svelte と Riot.js v4 のシンタックスの比較

Last updated at Posted at 2019-12-29

本記事は Riot.js Advent Calendar 2019 の第24日目の記事になります!(遅刻組です…)

Riot.js(以下、riot)は非常にシンプルかつ軽量で入門の敷居も低く、とても書きやすいコンポーネント指向のUIライブラリです。今回は, Svelte というライブラリの 公式チュートリアル を参考に, riot との書き方の比較を見ていきたいと思います.

結構長いので, 適当に流しながら見ていくと良いかもしれません.

※1 数が多いので, チュートリアルの全てを書いているわけではありません
※2 随時更新するかもしれません :bow:

前提

本記事に対する riot に関する前提を書いていきます.

1点目: html の設定

ベースとなる index.html は以下のように

  • app タグを設定していること(必要な子コンポーネントも同様とする)
  • mount メソッドで初期パラメータは渡さないこと

とします.

index.html
  <body>
    <app></app>

    <script data-src="src/app.riot" type="riot"></script>
    <script>
      riot
        .compile()
        .then(() => {
          riot.mount("app")
        })
        .catch(e => {
          console.error(e)
        })
    </script>
  </body>

2点目: カスタムコンポーネント

カスタムコンポーネントの書き方は基本的に

  • export default{} に集約する
  • 開始と終了の app タグは省略

するものとします.
一応以下のような書き方もできますが, 本記事では割愛します.

appタグ
<h1>Hello { title }!</h1>

<script>
  const title = 'Riot.js'
</script>

3点目: 書き方

svelte はバンドラを使った書き方をしているようですので, 基本的に riot もそれに則って行こうと思いますが, 一部インブラウザコンパイルの書き方をしています.(1点目の書き方がまさにそれ)

基本文法

ではここから実際に書いていきます. 基本的には svelte 側の書き方をベースに, それを riot4 で書いていきますのでご注意ください.

拡張子

  • svelte → .svelte
  • riot4 → .riot or .riot.html

Hello world!

svelte

svelte
<script>
	let name = 'world';
</script>

<h1>Hello {name.toUpperCase()}!</h1>

riot

riot
<h1>Hello { title }!</h1>

<script>
  export default {
    title: 'Riot.js'
  }
</script>

Dynamic attributes

svelte

svelte
<script>
  let src = 'tutorial/image.gif';
  let name = 'Rick Astley';
</script>

<img {src} alt="{name} dances.">

riot

riot
<img {src} alt="{title} dances.">

<script>
  // name は予約語のため title に変更
  export default {
     src: 'tutorial/image.gif',
     title: 'Rick Astley'
  }
</script>

Styling

共通
<p>This is a paragraph.</p>

<style>
  p {
    color: purple;
    font-family: 'Comic Sans MS', cursive;
    font-size: 2em;
  }
</style>

もし riot4 で Scoped CSS を指定したい場合は :host {} を使ってください.

参考: Scoped CSS

Nested components

svelte

svelte
<script>
  // 読み込むだけで良い
  import Nested from './Nested.svelte';
</script>

<style>
  p {
    color: purple;
    font-family: 'Comic Sans MS', cursive;
    font-size: 2em;
  }
</style>

<p>This is a paragraph.</p>
<Nested/>

riot

riot
<Nested/>

<script>
  import Nested from './nested.riot'
  export default {
    // components オブジェクトとして設定
    components: {
      Nested
    }
  }
</script>

<style>
  p {
    color: purple;
    font-family: 'Comic Sans MS', cursive;
    font-size: 2em;
  }
</style>
Nested(共通子コンポーネント)
<p>This is another paragraph.</p>

HTML tags

svelte

svelte
<script>
  let string = `this string contains some <strong>HTML!!!</strong>`;
</script>

<p>{@html string}</p>

riot

riot
<p>{string}</p>

<script>
  export default {
    string: 'this string contains some <strong>HTML!!!</strong>',
    onMounted() {
      this.$('p').innerHTML = this.string
    }
  }
</script>

riot4 には HTML のバインディング機能はありません…

参考: Render unescaped HTML

Reactivity

ここからは少しアプリケーションっぽいものを見ていきます.

Assignments

svelte

svelte
<script>
  let count = 0;

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

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

riot

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

<script>
  export default {
    count: 0,
    handleClick() {
      this.count += 1
      this.update()
    }
  }
</script>

やっぱり riot は明示的に update() メソッドを実行しないといけないのが面倒に見えるかもしれませんね :sweat_smile:

Declarations

svelte

svelte
<script>
  let count = 0;
  $: doubled = count * 2;

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

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

<p>{count} doubled is {doubled}</p>

riot

riot
<button onclick={handleClick}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>{count} doubled is {doubled}</p>

<script>
  export default {
    count: 0,
    onBeforeMount() {
      this.doubled = this.count * 2
    },
    handleClick() {
      this.count += 1
      this.doubled = this.count * 2
      this.update()
    }
  }
</script>

$: hoge で自動で監視する変数を増やせるのは便利ですねー. 実は riot ですと, もう一つ doubled という変数を用意しなくても, this.doubled と書けば内部的に doubled という変数を持ってくれます.

また svelte のチュートリアルにも記載があるように, {count * 2} と書いても同じことができますね.

Statements

svelte

svelte
<script>
  let count = 0;

  $: if (count >= 10) {
    alert(`count is dangerously high!`);
    count = 9;
  }

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

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

riot

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

<script>
  export default {
    count: 0,
    handleClick() {
      this.count += 1
      if (this.count >= 10) {
        alert('count is dangerously high!');
      }
      this.update()
    }
  }
</script>

$: *** と書いても, *** の中で count 変数をコントロールできるというのが本題ですが, 今回の題材ですと, if 文を handleclick() メソッドの中で書いても動作すれば良いのでちょっと例が微妙でしたね.

Updated arrays and objects

svelte

svelte
<script>
  let numbers = [1, 2, 3, 4];

  function addNumber() {
    numbers = [...numbers, numbers.length + 1];
  }

  $: sum = numbers.reduce((t, n) => t + n, 0);
</script>

<p>{numbers.join(' + ')} = {sum}</p>

<button on:click={addNumber}>
  Add a number
</button>

riot

riot
<button onclick={addNumber}>
  Add a number
</button>
<p>{numbers.join(' + ')} = {sum}</p>

<script>
  export default {
    numbers: [1, 2, 3, 4],
    onBeforeMount() {
      this.sum = this.numbers.reduce((t, n) => t + n, 0)
    },
    addNumber() {
      this.numbers = [...this.numbers, this.numbers.length + 1]
      this.sum = this.numbers.reduce((t, n) => t + n, 0)
      this.update()
    }
  }
</script>

何度か使ってきていますが, riot ですと初期化時点で何かしらの処理をしたい場合, onBeforeMount() メソッド内で明示的に処理を書かないといけません. 本当はこのメソッド内の処理を省略したいですね💦重複して書いていますし.

Props

ここからはコンポーネントへの値の渡し方, いわゆる props について見ていきます.

Declaring props

svelte

svelte(main)
<script>
  import Nested from './Nested.svelte';
</script>

<Nested answer={42}/>
svelte(Nested)
<script>
  export let answer;
</script>

<p>The answer is {answer}</p>

riot

riot(main)
<Nested answer={42}/>
<script>
  import Nested from './nested.riot'
  export default {
    // components オブジェクトとして設定
    components: {
      Nested
    }
  }
</script>
riot(Nested)
<p>The answer is {props.answer}</p>

<script>
  // インブラウザコンパイルでは, この script が丸っと不要
  import Nested from './Nested.riot'
  export default {
    components: {
      Nested
    }
  }
</script>

<!-- or -->

<p>The answer is {answer}</p>

<script>
  import Nested from './nested.riot'
  export default {
    components: {
      Nested
    },
    // 本当は answer: props.answer と書きたい
    answer: '',
    onBeforeMount(props) {
      this.answer = props.answer
    }
  }
</script>

riot の子コンポーネントでは, props.answer でも扱えますが, 上記のコメントにあるように export default の変数定義では使えないのが残念です.

Default values

親(main)コンポーネントは先程の Declaring props と同じなので省略します.

svelte

svelte(Nested)
<script>
  // 初期値を設定
  export let answer = 'a mystery';
</script>

<p>The answer is {answer}</p>

riot

riot(Nested)
<p>The answer is {props.answer || answer}</p>

<script>
  export default {
    answer: 'a mystery'
  }
</script>

<!-- or -->

<p>The answer is {answer}</p>

<script>
  export default {
    answer: 'a mystery',
    onBeforeMount(props) {
      this.answer = props.answer || this.answer
    }
  }
</script>

こちらも riot は明示的に props の値かコンポーネント内の変数かを提示しないとダメなようです(ここは svelte 楽ですね)

Spread props

svelte

svelte(main)
<script>
  import Info from './Info.svelte';

  const pkg = {
    name: 'svelte',
    version: 3,
    speed: 'blazing',
    website: 'https://svelte.dev'
  };
</script>

<Info {...pkg}/>
svelte(Info)
<script>
  export let name;
  export let version;
  export let speed;
  export let website;
</script>

<p>
  The <code>{name}</code> package is {speed} fast.
  Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
  and <a href={website}>learn more here</a>
</p>

riot

riot(main)
<Info {...pkg} />

<script>
  import Info from './info.riot';

  export default {
    components: {
      Info
    },
    pkg: {
      name: 'riot',
      version: 4,
      speed: 'blazing',
      website: 'https://riot.js.org'
    }
  }
</script>
riot(Info)
<p>
  The <code>{props.name}</code> package is {props.speed} fast.
  Download version {props.version} from <a href="https://www.npmjs.com/package/{props.name}">npm</a>
  and <a href={props.website}>learn more here</a>
</p>

<!-- or -->

<p>
  The <code>{title}</code> package is {speed} fast.
  Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
  and <a href={website}>learn more here</a>
</p>

<script>
  export default {
    onBeforeMount(props) {
      this.title = props.name
      this.version = props.version
      this.speed = props.speed
      this.website = props.website
    }
  }
</script>

何度か書いて思いましたが, 直接 {props.name} と HTML に書いたほうが楽ですね.

Logic

ここからは条件分岐などのロジックについて書いていきます.

IF-Else blocks

svelte

svelte
<script>
  let user = { loggedIn: false };

  function toggle() {
    user.loggedIn = !user.loggedIn;
  }
</script>

{#if user.loggedIn}
  <button on:click={toggle}>
    Log out
  </button>
{:else}
  <button on:click={toggle}>
    Log in
  </button>
{/if}

riot

riot
<button if={user.loggedIn} onclick={toggle}>
  Log out
</button>
<button if={!user.loggedIn} onclick={toggle}>
  Log in
</button>

<script>
  export default {
    // ここは必須
    user: {},
    onBeforeMopunt() {
      this.user = { loggedIn: false }
    },
    toggle() {
      this.user.loggedIn = !this.user.loggedIn
      this.update()
    }
  }
</script>

2点あります.

  • else の機能がない(else-if も)
  • user: {} が必要

特に二つ目が意外で, object ではなくプリミティブな変数であればこれが不要なんですよね…

Each blocks

svelte

svelte
<script>
  let cats = [
    { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
    { id: 'z_AbfPXTKms', name: 'Maru' },
    { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
  ];
</script>

<h1>The Famous Cats of YouTube</h1>

<ul>
  {#each cats as { id, name }, i}
    <li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
      {i + 1}: {name}
    </a></li>
  {/each}
</ul>

riot

riot
<h1>The Famous Cats of YouTube</h1>
<ul each={(cat, i) in cats}>
  <li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
    {i + 1}: {cat.name}
  </a></li>
</ul>

<script>
  export default {
    cats: [
      { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
      { id: 'z_AbfPXTKms', name: 'Maru' },
      { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
    ]
  }
</script>

svelte は {#each cats as { id, name }, i} のように, each ループ内で object のそれぞれの key 毎に指定して取得できるのが良いですね :thumbsup:

Keyed each blocks

svelte

svelte(main)
<script>
  import Thing from './Thing.svelte';

  let things = [
    { id: 1, color: '#0d0887' },
    { id: 2, color: '#6a00a8' },
    { id: 3, color: '#b12a90' },
    { id: 4, color: '#e16462' },
    { id: 5, color: '#fca636' }
   ];

  function handleClick() {
    things = things.slice(1);
  }
</script>

<button on:click={handleClick}>
  Remove first thing
</button>

{#each things as thing}
  <Thing current={thing.color}/>
{/each}
svelte(Thing)
<script>
  // `current` is updated whenever the prop value changes...
  export let current;

  // ...but `initial` is fixed upon initialisation
  const initial = current;
</script>

<p>
  <span style="background-color: {initial}">initial</span>
  <span style="background-color: {current}">current</span>
</p>

<style>
  span {
    display: inline-block;
    padding: 0.2em 0.5em;
    margin: 0 0.2em 0.2em 0;
    width: 4em;
    text-align: center;
    border-radius: 0.2em;
    color: white;
  }
</style>

riot

riot(main)
<button onclick={handleClick}>
  Remove first thing
</button>

<Thing each={thing in things} current={thing.color} />

<script>
  import Thing from './thing.riot'

  export default {
    components: {
      Thing
    },
    things: [
      { id: 1, color: '#0d0887' },
      { id: 2, color: '#6a00a8' },
      { id: 3, color: '#b12a90' },
      { id: 4, color: '#e16462' },
      { id: 5, color: '#fca636' }
    ],
    handleClick() {
      this.things = this.things.slice(1);
      this.update()
    }
  }
</script>
riot(Thing)
<p>
  <span style="background-color: {initial}">initial</span>
  <span style="background-color: {current}">current</span>
</p>

<script>
  
  export default {
    onBeforeMount(props) {
      // `current` is updated whenever the prop value changes...
      this.current = props.current
      // ...but `initial` is fixed upon initialisation
      this.initial = this.current
    }
  }
</script>

<style>
  span {
    display: inline-block;
    padding: 0.2em 0.5em;
    margin: 0 0.2em 0.2em 0;
    width: 4em;
    text-align: center;
    border-radius: 0.2em;
    color: white;
  }
</style>

riot でもカスタムコンポーネントに直接 each ディレクティブを使って複数の子コンポーネントをレンダリングすることができます.

参考: Looping custom components

Await blocks

svelte

svelte
<script>
  let promise = getRandomNumber();

  async function getRandomNumber() {
    const res = await fetch(`tutorial/random-number`);
    const text = await res.text();

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

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

<button on:click={handleClick}>
  generate random number
</button>

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

riot

riot には現在 async/await に対応する機能が存在しないため, 自前で Promise を使って実装するしかないです…これも今後実装されてほしいですね :sweat:

おわりに

さすがに全部は書いていないですが, svelte のチュートリアル見た感じ, かなりライトなライブラリでとても書きやすい印象を受けました. riot の新しいライバル感が…w npm でのダウンロード数も伸びてきており, 世界的にも注目され始めているようです.

軽量な分, やはり小規模向けでサクッと作るときの選択肢としてはかなり良さげなのではないかなと思います!もしご興味ある方は触ってみてください!もちろん riot も👍

ではでは(=゚ω゚)ノ

25
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?