🍘

Svelte入門(1)

2024/03/02に公開

はじめに

遅ればせながらSvelteのチュートリアルをやってみたので内容をまとめます。

チュートリアルは以下の4部構成になっており、今回は1. Basic Svelteのうち、
Introduction、Reactivity、Props、Logicの項目を対象とします。(1つの記事が長くなりすぎてしまうので。。)

  1. Basic Svelte
  2. Advanced Svelte
  3. Basic SvelteKit
  4. Advanced SvelteKit

https://learn.svelte.jp/tutorial/welcome-to-svelte

Svelte とは?

JavaScritpのフレームワークの一つです。

HTML、CSS、JavaScriptでコンポーネントを書き、Svelteがそれをビルドプロセスで小さなスタンドアローンのJavaScriptモジュールにコンパイルします。
コンポーネントのテンプレートを静的に解析することで、ブラウザーの作業を可能な限り少なくなるようにすることができます。[1]

とのことです。

HTML、CSS、JavaScriptでコンポーネントを構築していくのはReactやVueと同じですね。

大きな違いとしては、Svelteはコンパイラであり、
差分レンダリングを実現するために仮想DOMを採用していない点です。
それによって仮想DOM分の計算が丸々無くなりますよということらしいです。

仮想DOMのオーバーヘッドについてはこちらの記事に少し記載されています。
https://svelte.jp/blog/virtual-dom-is-pure-overhead

Introduction

要素や属性から変数を参照

コンテンツ内から特定の変数を呼び出す場合は{}で囲うだけで参照できます。

Sample.svelte
<script>
  let name = 'Svelte';
</script>

<h1>Hello {name}!</h1>


実行結果
「Svelte」が代入されていることがわかります。
 
 
属性から変数を呼び出す場合も同じように{}で囲うだけで参照できます。

Sample.svelte
<script>
  let src = 'https://placehold.jp/ff3e00/ffffff/150x150.png?text=image'
  let extension = 'png';
</script>

<img src={src} alt="this is {extension}"/>

// 属性名と変数名が同様の場合は`属性名=`は省略可能です。
// <img {src} alt="this is {extension}"/>


実行結果
imgタグのsrcとalt属性がそれぞれscriptタグ内で宣言したsrcとextensionを参照していることがわかります。

コンポーネントを呼び出す

1つのコンポーネントが複雑になりすぎないようにコンポーネントを分けて書くことができ、
それらをインポートして使用することができます。

Parent.svelte
<script>
  // ネストするコンポーネントをインポート
  import Child from './Child.svelte';
</script>

<p>Parent Component</p>
<Child />  // 子コンポーネントを呼び出す
Child.svelte
<p>Child Component</p>

<style>
  p {
    color: orangered;
  }
</style>


実行結果
「Parent Componen」の下に「Child Component」が表示されています。
※ わかりやすいようにstyleで色を変えています。

プレーンテキストとしてのhtmlをレンダリング

htmlの構造を持つプレーンテキストをレンダリングしたい場合は@htmlタグを使いましょう。

Sample.svelte
<script>
  let stringHtml = 'This is Italic <i>HTML</i>!!';
</script>

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


実行結果
プレーンテキストであるstringHtmlが、iタグが意味を持ってマークアップされていることがわかります。
※ サニタイズはされないので注意が必要です。

Reactivity

stateを更新

任意のイベントに応じてstateを更新し、表示結果などに反映することができます。
下記の例ではon:clickを用いてクリックイベントを受け取り、increment関数を呼び出しています。
※ on:clickについては後述するEventsの項目で記載します。

Sample.svelte
<script>
  let count = 0;

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

<!-- on:clickについては後述するEventsを参照 -->
<button on:click={increment}>
  Click!
</button>

<p>Clicked {count} {count === 1 ? 'time' : 'times'}</p>


実行結果
クリックしてcountがインクリメントされる度に表示も変更されていることがわかります。

リアクティブ宣言/リアクティブステートメント

他の値に依存して再評価が必要な場合や何度も参照する値には、
リアクティブ宣言/リアクティブステートメントを用いましょう。
Javascriptのラベル構文を用いて、接頭詞$: を付与することで実現できます。

Sample.svelte
<script>
  let count = 0;
  // countに変更がある度に再評価される
  $: evenOrOdd = count % 2 == 0 ? 'even' : 'odd';

  // countに変更がある度に再評価される
  $: if (count > 5) {
    alert('over 5');
  }

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

<!-- on:clickについては後述するEventsを参照 -->
<button on:click={increment}>
  Clicked {count}
  {count === 1 ? 'time' : 'times'}
</button>

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


実行結果
クリックしてcountがインクリメントされる度に、countに依存しているevenOrOddとif文の再評価が走り、表示が変わる事がわかります。
 
 
以下のようにステートメントをグループ化することも可能です。

Sample.svelte
<script>
  $: {
    console.log('line1');
    console.log(`count is ${count}`)
    console.log('line2');
  }
</script>

※ リアクティビティのトリガーは代入のみ

注意したいのが、リアクティビティのトリガーは変数への代入のみで、
配列のメソッド(pushやpop等)による操作では更新されません。

Sample.svelte
<script>
  let numbers = [1, 2];

  function addNumber() {
    // pushで新規の値を追加する
    numbers.push(numbers.length + 1);
  }

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

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

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


実行結果
クリックしても表示される内容が変更されていません。

numbersへ新規の値を追加する部分をpushを使わずに書き換えてみましょう。

Sample.svelte(修正前)
numbers.push(numbers.length + 1);

pushの代わりにスプレッド構文を用いて最後部に値を追加した配列をnumbersに代入します。

Sample.svelte(修正後)
numbers = [...numbers, numbers.length + 1];


実行結果
無事に表示内容が更新されています。

Props

コンポーネント間での値の受け渡し

値を受け取るコンポーネント(Child)で先頭にexportを付与して受け取る値を宣言します。

Child.svelte
<script>
  // プロパティでnumberを受け取ることを宣言します。
  export let number;
  // export let number = 100;(デフォルト値を設定しておくこともできます。)
</script>

<p>my number is {number}</p>

値を渡すコンポーネント(Parent)はChildコンポーネントのプロパティとして値をセットするだけです。

Parent.svelte
<script>
  import Child from './Child.svelte';
</script>

<Child number={1} />


実行結果
ParentがChildのプロパティにセットした「1」が渡されていることがわかります。

スプレット構文でオブジェクトを一括で受け渡し

オブジェクトの内容がChildコンポーネントが期待しているプロパティに対応している場合、
スプレット構文で値をセットすることができる。

Child.svelte
<script>
  export let year;
  export let month;
  export let day;
</script>

<p>Today is {month} {day}, {year}</p>
Parent.svelte
<script>
  import Child from './Child.svelte';

  const pkg = {
    year: 1969,
    month: 'July',
    day: 20,
  };
</script>

<!-- スプレット構文でオブジェクトの内容をすべて受け渡す -->
<Child {...pkg} />

実行結果
pkgオブジェクトの内容が全て反映されていることがわかります。

Logic

条件分岐

if, else if, elseブロックを使用することで、
条件に合わせてマークアップの表示・非表示や切り替えることができます。

Sample.svelte
<script>
  let count = 0;

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

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

{#if count > 5}
  <p>{count} is greater than 5</p>
{:else if count < 3}
  <p>{count} is less than 3</p>
{:else}
  <p>{count} is between 3 and 5</p>
{/if}


実行結果
countの範囲に合わせて表示が切り替わっていることがわかります。

ループ処理

eachブロックを使用することでループ処理を記述することができます。
前述したifブロック同様、#はeachブロックの開始、/は終了を示します。

Sample.svelte
<script>
  const colors = ['red', 'green', 'blue'];
  $: selected = colors[0];
</script>

<h1 style="color: {selected}">Color is {selected}</h1>

<div>
  {#each colors as color, i}
    <button
      aria-current={selected === color}
      aria-label={color}
      style="background: {color}"
      on:click={() => selected = color}
    >{i + 1}. {color}</button>
  {/each}
</div>

<style>
  button {
    color: #fff;
    width: 100px;
  }
</style>


実行結果
配列内の色をループで処理して各色のボタンを表示されていることがわかります。

keyを指定したループ処理

svelteはループ処理する際にデフォルトでインデックスをkeyに利用します。
ループ処理するデータに対して操作(特定のアイテムの追加や削除)をした際に差分を取る際に不整合が起きる場合があります。

例えば、下記のボタンをクリックする度に先頭のアイテムを1つずつ削除していくコードを実行してみましょう。

List.svelte
<script>
  import Item from './Item.svelte';

  let items = [
    { id: 1, word: 'alfa' },
    { id: 2, word: 'bravo' },
    { id: 3, word: 'charlie' }
  ];

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

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

{#each items as item}
  <Item word={item.word} />
{/each}
Item.svelte
<script>
  const firstLetters = {
    alfa: 'a',
    bravo: 'b',
    charlie: 'c'
  };

  export let word;

  const firstLetter = firstLetters[word];
</script>

<p>{firstLetter} = {word}</p>


実行結果
※ 削除した際に単語は変わっているのに頭文字(firstLetter)が取り残されて変化していません。
なぜこのような事象が起きるのかについてはこちらの記事でわかりやすく説明してくださっています。
https://zenn.dev/ettsu/articles/d31d0c671a14de

上記のような事象を防ぐためにはアイテムに紐づくユニークな値をkeyとして使う必要があります。
下記のようにeach文の最後にkeyとして使用する値を追記します。

List.svelte
...
{#each items as item (item.id)}  // ()で囲ってアイテムのidを指定します。
  <Item word={item.word} />
{/each}


実行結果
削除した際に頭文字(firstLetter)も変更されていることがわかります。

非同期処理

webアプリケーションを作成する際にはapiへのリクエスト等の非同期データを扱いたい場合がほとんどかと思います。
awaitブロックを使用すると、マークアップ内で直接promiseをawaitするように書くことができます。

Sample.svelte
<script>
  import { getStrangerName } from './fetcher.js';

  let getStrangerNamePromise = getStrangerName();

  function handleClick() {
    getStrangerNamePromise = getStrangerName();
  }
</script>

<button on:click={handleClick}>
  fetch stranger name
</button>

{#await getStrangerNamePromise}  // getStrangerNamePromiseオブジェクトをawait
  <p>...waiting</p>  // resolveされるまで表示したい値
{:then resp}  // resolve時の処理をthenブロックに記述
  <p>{resp.results[0].name.first}</p>
{:catch error}  // reject時の処理をcatchブロックに記述
  <p>{error.message}</p>
{/await}
fetcher.js
export async function fetchStrangerName() {
  const res = await fetch('https://randomuser.me/api/');

  if (res.ok) {
    return await res.json();
  } else {
    throw new Error('Request failed');
  }
}


実行結果
処理中に「...waiting」が表示されresolveしたら値が表示されていることがわかります。
 
 
thenブロックとcatchブロックは省略可能で、下記のように記述することも可能です。

Sample.svelte
<script>
...
{#await getStrangerNamePromise then resp}
  <p>{resp.results[0].name.first}</p>
{/await}
脚注
  1. https://svelte.jp/blog/frameworks-without-the-framework#introducing-svelte ↩︎

Discussion