LoginSignup
12
8

More than 3 years have passed since last update.

Svelteチュートリアルやってみた

Last updated at Posted at 2020-11-23

svelteの公式サイトで学んでみる

Introduction

基本

https://svelte.dev/tutorial/basics
公式svelteでは実際にコードを書きながら学べる設計となっています。
環境構築なしですぐに書いて試せるのはありがたいですね。

ちなみに...
APIドキュメントはこちら
公式のexample集はこちら
60秒で始めるquick startはこちら

ReactとVueに似ていると言われるSvelteですが、
Svelteは実行時に走るのではなく、ビルド時にJSを吐き出す仕組みとなっています。
なのでフレームワークによってパフォーマンスを落とす心配がありません。

SvelteではReactなどと同じようにcomponentをコードの単位として扱います。
コンポーネントはHTMLやCSS,JSをカプセル化した再利用可能な自己完結したコードブロックになります。
コンポーネントは.svelteの拡張子を付けたファイルに記述します。

App.svelte
<h1>Hello world!</h1>

追加データ

先ほどの静的なマークアップにデータを追加してみます。
`name`という変数を追加します。

App.svelte
<script>
    let name = 'world'
</script>

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

スクリーンショット 2020-08-30 14.34.35.png

記述がとてもシンプルですね。

動的な属性

テキスト同様に、中括弧で要素の属性を制御することも可能です。

App.svelte
<script>
    let src = 'tutorial/image.gif';
</script>

<img src={src}>

しかし、上記の記述だと A11y: <img> element should have an alt attribute (5:0) と行ったエラーが発生します。
a11yaccessibilityの略ですが、alt属性がないという警告を出してくれています。
svelteはアクセシビリティに配慮した警告も出してくれるというので驚きです。
下記のようにalt属性を追加すればOKです。

App.svelte
<script>
    let src = 'tutorial/image.gif';
</script>

<img src={src} alt="男のダンス">

スクリーンショット 2020-08-30 14.45.31.png
ダンスしているgifが表示されました🕺

ちなみに、属性名と値が同じ値であれば下記のようにショートハンドを使うことができます。

<img {src} alt="男のダンス">

スタイル

HTML同様<style>タグが使用可能です。
styleは記述したコンポーネントにスコープされるので、他のコンポーネントのcssとの重複などを気にせず済みます。

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

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

ネストされたコンポーネント

実際に実装する場合はコンポーネントをネストしていくことになるかと思います。

Nested.svelte
<p>This is another paragraph.</p>

上記ファイルコンポーネントを単純にimportすればそのまま使用可能です。

App.svelte
<script>
  import Nested from './Nested.svelte'
</script>

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

呼び出した側(ここではApp)の<style>は、呼び出された側(ここではNested)のスタイルには影響を与えません。
また、コンポーネントの名前は他のHTMLタグと区別するため、必ず最初の文字が大文字である必要があります。

Reactのようにexport文も書かないでOKなので、コード量が減りますね。

HTMLタグ

通常は文字はプレーンテキストとして解釈されるので、 <strong>のようなタグはHTMLとして解釈されません。
HTMLとして解釈させたい場合は {@html ... }という特殊な記法を使います。

App.svelte
<script>
    let string = `この文章は <strong>HTMLを含む!!!</strong>`;
</script>

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

Svelte側ではセキュリティ対策を行なっていないので、 外部データを流し込む際はXSSなどに要注意です。

アプリを作る

実際に自分のエディタでsvelteを使うには、環境構築が必要です。
rollup用にはrollup-plugin-svelteが、
webpack用にはsvelte-loaderがそれぞれ準備されています。
環境構築についての公式の解説は https://svelte.dev/blog/svelte-for-new-developers に乗っています。
こちらの記事では割愛しますが、非常に簡単に構築可能のようです。
エディタのセッティング方法の公式解説はこちらになります。

設定が終われば、svelteのコンポーネントをimportし、new演算子でインスタンス化して使用可能です。

App.svelte
import App from './App.svelte';

const app = new App({
    target: document.body,
    props: {
        // 後ほど学習します
        answer: 42
    }
});

reactivity

svelteはアプリケーションの状態とDOMを同期するための reactivity(反応性?)のシステムが中心となります。

App.svelte
<script>
    let count = 0;

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

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

ボタンを押すと handleClickが発動し、countが+1され、それが描画にも同期されています。

宣言

コンポーネントの状態が変わるとSvelteは自動でDOMをアップデートしてくれますが、
コンポーネントの状態の中には他の状態に依存するものあります。(例えば fullnamefirstnamelastnameの状態から成り立つような時)
そのような場合は下記のようなリアクティブ宣言を使います。

let count = 0;
$: doubled = count * 2

見慣れない書き方ですが、この宣言によって参照している値が変わったら再計算させることができます。

App.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>

上記のコード実行結果では、ボタンを押してcountを追加させると、しっかりdoubledも変更できているのが確認できます。

ステートメント

リアクティブ宣言は値以外でも使用可能です。

$: console.log(`the count is ${count}`)

上記を<script>タグ内に記述すると、countが変更される度にconsole.logが走ります。

ステートメントをグループ化することも可能です。

$: {
    console.log(`the count is ${count}`);
    alert(`I SAID THE COUNT IS ${count}`);
}

コードブロックの前に置くことも可能です。

$: if (count >= 10) {
  alert(`カウントしすぎ!`);
  count = 9;
}

配列やオブジェクトの変更

変数の変更を検知する都合上、svelteは pushspliceといったオブジェクトや配列自体を変更しない更新を検知することができません。
なので、変更する際は必ず新しいオブジェクトや配列になるように配慮しなければいけません。

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

プロパティへの代入は、値そのものへの代入と同様に変更を検知できます。
ただし、変更を検知したい変数名はまとまって使われていなければなりません。

const foo = obj.foo
foo.bar = 'baz'

$: test = obj.foo.bar

この場合、 obj.foo.barは変更されていますが、
testの再計算は行われません。
この制約はハマりそうなので要注意です。

props

今まではcomponent内のデータのみを扱ってきましたが、実際には他のコンポーネントから
データを受け取ったり、データを子のコンポーネントに渡す必要もあります。
このようなデータをpropと呼びます。(propertiesの略です)
Svelteではexportというキーワードを使用してpropsを渡せるようになります。

Nested.svelte
<script>
    export let answer;
</script>

<p>答えは {answer}</p>
App.svelte
<script>
    import Nested from './Nested.svelte';
</script>

<Nested answer={42}/>
描写結果
答えは 42

デフォルト値

propにデフォルト値を設定することもできます。

Nested.svelte
<script>
    export let answer = 100;
</script>

<p>答えは {answer}</p>
App.svelte
<script>
    import Nested from './Nested.svelte';
</script>

<Nested />
描写結果
答えは 100

スプレッドprops

オブジェクトをpropsとして持っているのであれば、スプレッド構文を使ってまとめて子に値を渡すことも可能です。

const pkg = {
  name: 'svelte',
  version: 3,
  speed: 'めちゃ早'
}

<Info {...pkg} />

逆に子コンポーネント側で、
親から与えられたprops全てを $$propsで取得することが可能です。
これはexport演算子を使用していないものでも取得できます。
svelte側での最適化が難しいので非推奨となっていますが、場合によっては便利でしょう、と説明に書かれていました。

ロジック

HTMLにはifやloopといった制御をする機能はありませんが、svelteにはあります。

ifブロック

if文は {#if 条件式} ~ {/if}
で実装可能です。

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

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

なんとなくpugなどのHTMLテンプレートエンジンっぽい書き方ですね。

else

もちろんelseもあります。

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

#文字はblockの始まりを、
/文字はブロックの終わりを、
:はブロックが続けられることを表しています。

else-if

else-if文は下記のように記述します。

{#if x > 10}
    <p>{x} は 10より大きい</p>
{:else if 5 > x}
    <p>{x} は 5より小さい</p>
{:else}
    <p>{x} は 5 と 10の間</p>
{/if}

each

各アイテムに対して同じ処理を行いたい時はeachを使用します。

App.svelte
<script>
let cats = [
  { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
  { id: 'z_AbfPXTKms', name: 'Maru' },
  { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
];
</script>
<ul>
  {#each cats as cat}
    <li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}"> 
      {cat.name}
    </a></li>
  {/each}
</ul>

配列だけでなく,配列ライクなオブジェクト(lengthプロパティを持ったもの)も同様にeachが使えます。
さらにさらに、indexも取得可能です。

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

分割代入も可能です。

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

keyed each blocks

デフォルトではeachの値を変更すると、ブロックの最後を追加/削除して調整されます。
例えば配列の先頭を削除した場合、一番最後の要素が消えてしまう挙動となります。
これを改善するために、各要素にユニークなkeyをつけます。

{#each things as thing (thing.id)}
    <Thing current={thing.color}/>
{/each}

Await

非同期処理を同期的に行う際にはawaitを使います。
下記では、 あるpromiseの処理を同期的に実行する文です。

{#await promise}
  <p>...waiting</p>
{:then number}
  <p>ナンバーは {number}</P>
{:catch error}
  <p style="color: red">{error.message}</p>

構文はPromiseと同じなのでわかりやすいかと思います。
Promise解決時に何も表示を行いたいくない場合は、下記のように最初のブロックを省略可能です。

{#await promise then value}
  <p>値は {value} </p>
{/await}

イベント

Domイベント

すでに紹介したように、 on:ディレクティブを使用してイベントの登録が可能です。

<div on:mousemove={handleMousemove}>
    The mouse position is {m.x} x {m.y}
</div>

インラインハンドラ

インラインでハンドラを設定することももちろん可能です。

<div on:mousemove="{e => m = { x: e.clientX, y: e.clientY }}">
    The mouse position is {m.x} x {m.y}
</div>

"は無くてもいいのですが、これをつけることで環境によってはハイライトがついて見やすい、とのことです。

reactなどでは実行時負担になるためインラインハンドラは避けるべき、
と言われていますが、svelteの場合コンパイラがいい感じに調整してくれるので、インラインでもOKです。

イベント修飾子

イベントの振る舞いを変更するためにイベント修飾子を使用することができます。
例えば、 once修飾子を使えば、一度だけイベントが走るように制御できます。

App.svelte
<script>
  function handleClick() {
    alert('clicked')
  }
</script>

<button on:click|once={handleClick}>
  Click me
</button>

他にも色々な修飾子があります。

名前 作用
preventDefault ハンドラーが走る前にevent.preventDefault()を走らせる
stopPropagation event.stopPropagationを走らせる(イベントの伝播を止める)
passive スクロール系のイベントのパフォーマンスをあげる(svelteが安全とみなすとと自動で入れてくれる)
capture バブリングフェーズでは無くキャプチャフェーズでイベントが発火する
once 一度だけイベントが走るようにする
self event.targetが自身のDOMだった場合のみ発火する

修飾子をチェインすることも可能です。

on:click|once|capture={...}

コンポーネントイベント

コンポーネントはeventをdispatchすることもできます。

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

  const dispatch = createEventDispatcher();

  function sayHello() {
    dispatch('message', {
      text: 'Hello!'
    });
  }
</script>
App.svelte
<script>
  import Inner from './Inner.svelte';

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

<Inner on:message={handleMessage}/>

createEventDispatcherで作成したdispatchを使用します。
dispatchの第一引数に指定した文字列のイベントが発火し、第二引数で与えた値がevent.detailに与えられます。

createEventDispatcherはコンポーネントが最初にインスタンスされた時にしか呼び出せない点に注意です。つまり、setTimeoutのコールバックで呼び出したりすることはできません。

イベントフォワーディング

DOMイベントと異なり、コンポーネントイベントはバブリングしません。
なので、深くネストしているコンポーネントのイベントを走らせたい場合、中間のコンポーネントはイベントを転送する必要があります。

例として、 App > Outer > Innter とコンポーネント入れ子になっている場合を考えます。
Innerのコンポーネントには、先ほどのようにmessageのイベントがdispatchされています。

これを解決するには、Outerコンポーネントにイベントハンドラーをつけてしまうことです。

Outer.svelte
<script>
  import Inner from './Inner.svelte';
  import { createEventDispatcher } from 'svelte';

  const dispatch = createEventDispatcher();

  function sayHello() {
    dispatch('message', event.detail);
  }
</script>

<Inner on:message={forward}/>

event.detailには、 Innerから渡されてきた{ text."Hello!"}が格納されてきますので、
それをそのままAppにdispatchしています。

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

  const dispatch = createEventDispatcher();

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

<button on:click={sayHello}>
  Click to say hello
</button>
App.svelte
<script>
  import Outer from './Outer.svelte';

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

<Outer on:message={handleMessage}/>

しかしこれだとコード記述量が多くなるので、Svelteはショートハンドを準備しています。
Outerコンポーネントのようにフォワードを行うコンポーネントは、下記の記述だけでOKです。

Outer.svelte
<script>
  import Inner from './Inner.svelte';
</script>

<Inner on:message/>

DOMイベントのフォワーディング

イベントのフォワーディングはDOMイベントでも同様に働きます。

CustomButton.svelte
<button on:click>
 Click me
</button>

バインディング

Textインプット

svelteはデータフローが上から下へなるように設計されています。
しかし、便利である時はこのフロールールを破ることがあるようです。
例えば<input>で上から下のルールを守る場合、on:inputハンドラーをつけ、event.target.valueを渡し...という煩雑さがあります。

愚直な例
<script>
  let name = 'world';
  function test(event) {
    name =  event.target.value
  }

</script>

<input on:input={test}>

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

記述をシンプルにするために、inputの場合は bind:valueというディレクティブが使用可能です。

<input bind:value={name}>

これは、nameを更新すると、入力値が更新されるのと同時に、入力値を変更するとnameも更新されるという
双方向のデータ更新を可能にします。

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

<input bind:value={name}>

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

数字のinput

DOMでは全てがstring型で扱われます。
これはtype="number”type="range" などといった数字を扱いたいときに不便です。

Svelteでは、これをいい感じにしてくれていて、数字をDOMに入れてもうまく働くようにしてくれます。

<input type=number bind:value={a} min=0 max=10>
<input type=range bind:value={a} min=0 max=10>

checkbox

チェックボックスでは input.valueではなく input.checkedディレクティブを使用します。

<input type=checkbox bind:checked={yes}>

グループinput

ラジオボタンのように互いの値に関係性があるようなinputの場合、bind:groupディレクティブを使います。

<input type=radio bind:group={scoops} value={1}>

ラジオボタンなんかはEach文がよく使えるケースの1つですね。

<script>
let scoops = 1;
let menu = [
  'Cookies and cream',
  'Mint choc chip',
  'Raspberry ripple'
]

</script>

<h2>Flavours</h2>

{#each menu as flavour}
  <label>
    <input type=checkbox bind:group={flavours} value={flavour}>
    {flavour}
  </label>
{/each}

textarea input

textareaはtext inputと同様にbind:valueを使用します。

<textarea bind:value={value}></textarea>

textareaに限った話ではないですが、この場合は名前が被っているので略すことが可能です。

<textarea bind:value></textarea>

selectのバインディング

selectのbind:valueを使用します。

<script>
  let questions = [
    { id: 1, text: `Where did you go to school?` },
    { id: 2, text: `What is your mother's name?` },
    { id: 3, text: `What is another personal fact that an attacker could easily find with Google?` }
  ];
  let selected;
  let answer = '';
</script>

<select bind:value={selected} on:change="{() => answer = ''}">
  {#each questions as question}
    <option value={question}>
      {question.text}
    </option>
  {/each}
</select>

optionのvalueはオブジェクトでOKなのが変わっています。
selectedの初期値が設定されていないので、リスト最初の値が自動的にデフォルト値となります。
ただし、seletedが初期化されるまでundefinedとなるので、seletedの参照には注意が必要となります。

まとめ

長くなってきたので途中までにしましたが、公式ドキュメントはまだまだ記述がたっぷりです。
svelteは他のフレームワークと違い、状態管理(ReduxやVuexに当たるもの)がデフォルトで組み込まれているそうです。
現在他ライブラリに比べると普及度は低いですが、非常に使いやすく、仮想DOMのオーバーヘッドもない良いフレームワークだと思いました。

12
8
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
12
8