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
の拡張子を付けたファイルに記述します。
<h1>Hello world!</h1>
追加データ
先ほどの静的なマークアップにデータを追加してみます。
`name`という変数を追加します。
<script>
let name = 'world'
</script>
<h1>Hello {name}!</h1>
記述がとてもシンプルですね。
動的な属性
テキスト同様に、中括弧で要素の属性を制御することも可能です。
<script>
let src = 'tutorial/image.gif';
</script>
<img src={src}>
しかし、上記の記述だと A11y: <img> element should have an alt attribute (5:0)
と行ったエラーが発生します。
a11y
はaccessibility
の略ですが、alt属性がないという警告を出してくれています。
svelteはアクセシビリティに配慮した警告も出してくれるというので驚きです。
下記のようにalt属性を追加すればOKです。
<script>
let src = 'tutorial/image.gif';
</script>
<img src={src} alt="男のダンス">
ちなみに、属性名と値が同じ値であれば下記のようにショートハンドを使うことができます。
<img {src} alt="男のダンス">
スタイル
HTML同様<style>
タグが使用可能です。
styleは記述したコンポーネントにスコープされるので、他のコンポーネントのcssとの重複などを気にせず済みます。
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
<p>This is a paragraph.</p>
ネストされたコンポーネント
実際に実装する場合はコンポーネントをネストしていくことになるかと思います。
<p>This is another paragraph.</p>
上記ファイルコンポーネントを単純にimportすればそのまま使用可能です。
<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 ... }
という特殊な記法を使います。
<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
演算子でインスタンス化して使用可能です。
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
// 後ほど学習します
answer: 42
}
});
reactivity
svelteはアプリケーションの状態とDOMを同期するための reactivity(反応性?)のシステムが中心となります。
<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をアップデートしてくれますが、
コンポーネントの状態の中には他の状態に依存するものあります。(例えば fullname
がfirstname
とlastname
の状態から成り立つような時)
そのような場合は下記のようなリアクティブ宣言を使います。
let count = 0;
$: doubled = count * 2
見慣れない書き方ですが、この宣言によって参照している値が変わったら再計算させることができます。
<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は push
やsplice
といったオブジェクトや配列自体を変更しない更新を検知することができません。
なので、変更する際は必ず新しいオブジェクトや配列になるように配慮しなければいけません。
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を渡せるようになります。
<script>
export let answer;
</script>
<p>答えは {answer}</p>
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42}/>
答えは 42
デフォルト値
propにデフォルト値を設定することもできます。
<script>
export let answer = 100;
</script>
<p>答えは {answer}</p>
<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を使用します。
<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
修飾子を使えば、一度だけイベントが走るように制御できます。
<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することもできます。
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('message', {
text: 'Hello!'
});
}
</script>
<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コンポーネントにイベントハンドラーをつけてしまうことです。
<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しています。
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('message', {
text: 'Hello!'
});
}
</script>
<button on:click={sayHello}>
Click to say hello
</button>
<script>
import Outer from './Outer.svelte';
function handleMessage(event) {
alert(event.detail.text);
}
</script>
<Outer on:message={handleMessage}/>
しかしこれだとコード記述量が多くなるので、Svelteはショートハンドを準備しています。
Outerコンポーネントのようにフォワードを行うコンポーネントは、下記の記述だけでOKです。
<script>
import Inner from './Inner.svelte';
</script>
<Inner on:message/>
DOMイベントのフォワーディング
イベントのフォワーディングはDOMイベントでも同様に働きます。
<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も更新されるという
双方向のデータ更新を可能にします。
<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のオーバーヘッドもない良いフレームワークだと思いました。