Svelteに入門する
Svelteとは
Webアプリケーションを高速にビルドするツールである。
ReactやVueといったJavaScriptのフレームワークと似ており、洗練されたインタラクティブなユーザーインターフェースを簡単に構築することを目的としている。
コードを実行時に解釈するのではなく、ビルド時にVanillaJavaScriptに変換することで、アプリケーションの高速化とパフォーマンスの向上を実現している。
参考:
公式チュートリアルを試す
Dynamic attributes
<script>
let src = 'hoge.png';
const name = 'A man';
</script>
<img {src} alt="{name} dances."> // {src} => `src={src} `
{ }
を使って、その中でJavaScriptを扱える。
HTML属性と変数名が一致する場合は{src}
のようにショートハンドが使える。
便利すぎ。。。
Styling
Vue.jsなどのように<style>
タグを使ってスタイリングをすることができる。
<p>This is a paragraph.</p>
<style>
/* Write your CSS here */
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
ここで重要なことは、この<p>
要素に対して当てたスタイルのスコープはこのコンポーネント内である。
つまり、他の場所(スコープ外)に影響を与えない。
Nested components
<script>
import Nested from './Nested.svelte';
</script>
<p>This is a paragraph.</p>
<Nested /> // This is another paragraph.
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
<p>This is another paragraph.</p>
import <component name> from <path>
で別のファイルにあるコンポーネントを持ってくる。
ここで気をつけたい点が、持ってきたコンポーネントにはimportしているファイルのスタイルは当たらないことである。
このサンプルでいえば、This is another paragraph.
にはパープルのカラーなどが当たらない。
HTML Tags
Svelteでは、通常文字列をプレーンなものとして認識するため、この文字列の中にHTMLタグを埋め込んでも認識してくれない。
<script>
let string = `this string contains some <strong>HTML!!!</strong>`;
</script>
<p>{string}</p>
// => this string contains some <strong>HTML!!!</strong>
HTMLタグとして、認識させたいときは、{@html ~}
を使用すればいける。
<script>
let string = `this string contains some <strong>HTML!!!</strong>`;
</script>
<p>{@html string}</p>
// <strong>タグとして認識される。
Assignments
Svelteは、イベントに対する応答のようなDOMを同期するための強力なリアクティブシステムである。
これはVueと似ている。
<script>
let count = 0;
function incrementCount() {
// event handler code goes here
count += 1;
}
</script>
<button on:click={incrementCount}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
Declarations
Svelteは、上記のリアクティブと、リアクティブ宣言を使って変数同士を同期させることもできる。
$: doubled = count * 2;
はJavaScriptのlabeled statementと同じように考える。
参照している値が変わるたびにコードを再実行している。
<script>
let count = 0;
$: doubled = count * 2;
function handleClick() {
count += 1;
}
</script>
<p>{count} doubled is {doubled}</p>
// count: 2 doubled: 4
// count: 3 doubled: 6
これがどんな時に役立つのか?
- 複数回参照する必要がある場合
- 他のリアクティブ値に依存しているリアクティブ値を持っている場合
Statements
$:
は、リアクティブ値を宣言するだけでなく、任意のコードもリアクティブにすることができる。
<script>
let count = 0;
function handleClick() {
count += 1;
}
$: console.log('the count is ' + count); // countが変更されるたびにコンソールに出力される。
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
さらにブロック{ }
を渡すことで、まとめて実行もできる。
<script>
[...]
$: {
console.log('the count is ' + count);
console.log('I SAID THE COUNT IS ' + count);
}
</script>
[...]
さらにif() { }
ブロックも置くことができる。
$: if(count > 10) { ~~~ }
すごい。
Updating arrays and objects
配列やオブジェクトを変更するメソッドはそれらの更新をトリガーしない。
下のサンプルで言うと、テンプレートのnumbers
は初期値の1, 2, 3, 4
のまま。
<script>
let numbers = [1, 2, 3, 4];
function addNumber() {
numbers.push(numbers.length + 1);
console.log(numbers); // output: 1, 2, 3, 4, 5
}
</script>
<p>{numbers}</p>
<button on:click={addNumber}>
Add a number
</button>
これをできるようにするには以下のようにする。
// 方法1
numbers.push(numbers.length + 1);
numbers = numbers;
// 方法2
numbers = [...numbers, numbers.length + 1];
// 方法3
numbers[numbers.length] = numbers.length + 1;
ただし、間接的に参照するとうまく動かない。
const hoge = obj.hoge;
hoge.fizz = 'baz';
つまり、変更する変数は直接左辺にいないと動かない。
Declaring props
あるコンポーネントから、子コンポーネントへデータを渡すためには、props
(プロパティ)を宣言する必要がある。
そこで登場するのがexport
というキーワードだ。
<script>
export let answer; // propsを宣言
</script>
<p>The answer is {answer}</p>
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42}/> // 子コンポーネントへデータを渡す
Default values
props
のデフォルト値を設定するには、通常の変数宣言と同じようにすれば良い。
<script>
export let answer = 'secret'; // propsのデフォルト値を設定
</script>
<p>The answer is {answer}</p>
<script>
import Nested from './Nested.svelte';
</script>
<Nested /> // データを渡さない
// => The answer is secret
Spread props
オブジェクトの場合、{...variable}
みたいな感じで、まとめて渡すことができる。
めっちゃ便利。
<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>
<script>
import Info from './Info.svelte';
const pkg = {
name: 'svelte',
version: 3,
speed: 'blazing',
website: 'https://svelte.dev'
};
</script>
<Info {...pkg} />
// 個別で指定するとこんなに面倒
// Info name={pkg.name} version={pkg.version} speed={pkg.speed} website={pkg.website}
if blocks
Svelteでは、条件付けやループ処理も扱える。
条件付けの場合は、{#if ~~~} { /if }
<script>
let user = { loggedIn: false };
function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{/if}
{#if !user.loggedIn}
<button on:click={toggle}>
Log in
</button>
{/if}
else
ブロックを使うとこうなる。
if
のには#
をつけて、else
の前には:
をつける。else if
も:
をつける。
これは少しややこしい。
Start | Continuation | End |
---|---|---|
# |
: |
/ |
[...]
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{:else}
<button on:click={toggle}>
Log in
</button>
{/if}
Each blocks
繰り返し処理は、if
ブロックと同じように組む。
#each vals as val, int
のように第二引数に変数を置くと、indexを格納できる。
<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" rel="noreferrer" href="https://www.youtube.com/watch?v={cat.id}">
{cat.name}
</a>
</li>
{/each}
</ul>
オブジェクトのキーをそれぞれ指定して、繰り返すこともできる。
ここでいうと、#each cats as { id, name }
<ul>
{#each cats as {id, name}}
<li>
<a target="_blank" rel="noreferrer" href="https://www.youtube.com/watch?v={id}">
{name}
</a>
</li>
{/each}
</ul>
each
ブロックの値に変更を加えたとき、デフォルトでは子コンポーネントのデータブロックの最後の要素に対して新しく追加したり、その要素を削除したりして、変更された値を更新する。
それを防ぐためにはユニークキーをeach
ブロックに対して明示することで、どのDOM要素を変更するかSvelteに教えることができる。
<script>
const ages = {
Bob: 10,
Alice: 20,
Mark: 30,
}
export let name;
const age = ages[name];
</script>
<p>
<span>{ name } is { age }</span>
</p>
<script>
import Child from './Child.svelte';
let persons = [
{ id: 1, name: 'Bob' },
{ id: 2, name: 'Alice' },
{ id: 3, name: 'Mark' },
];
function handleClick() {
persons = persons.slice(1);
}
</script>
<button on:click={handleClick}>
Remove first thing
</button>
{#each persons as person (person.id) }
<Child name={person.name}/>
{/each}
オブジェクトをそのまま渡すこともできるみたいだが、公式ではあまりお勧めされていない。
DOM events
on:
ディレクティブを使うことでイベントをリッスンできる。
<button on:click={handleClick}>Click!</button>
ワンラインで書くこともできる。クオートで括ったほうが、シンタックスハイライトで役に立つ。
他のフレームワークではパフォーマンスの面で避けた方が良いそうだが、Svelteの場合は例外らしい。
コンパイラーがよしなにやってくれるみたいだ。
<button on:click="{e => val = e.target}">Click!</button>
<p>{val}</p>
on:
ディレクティブに対して修飾子をつけることができる。
<button on:click|once={handleClick}>
Click me
</button>
その他にも、preventDefault
やpassive
などがあるみたいだ。
詳しい修飾子はここから確認できる。
この修飾子はチェーンすることができる。
<button on:click|once|capture={~~~}>
Click me
</button>
event dispatcherを作成することで、コンポーネントはイベントを送ることができる。
その時に使うのがcreateEventDispatcher
である。これはコンポーネントがインスタンス化された時に呼ばれている必要がある。だから、setTimeout
などのコールバックの中では使えない。
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function something() {
dispatch('eventName', {
text: 'hoge',
}
}
</script>
<script>
import Child from './Child.svelte';
function fuga(event) {
console.log(event.detail.text)
}
</script>
<Child on:eventName={fuga} />
子コンポーネントのdispatch()
で付けたイベント名を親コンポーネントのon:
ディレクティブの修飾子につけて、イベントを送っている。この修飾子を消すと、イベント自体は送られるが、反応はしない。
Event forwarding
DOMのイベントとは違って、コンポーネントのイベントはバブリングしない。
深くネストされたコンポーネントに対してイベントをリッスンさせたい場合は、中間のコンポーネントはそのイベントを転送しないといけない。
# イメージ
A component > intermediate component > nested component
// Aコンポーネントでnested componentのイベントを使いたい場合は、intermediateコンポーネントで転送しないといけない
それを解決する方法の一つとして、中間コンポーネントでもcreateEventDispatch
を定義する。
function forward(event) {
dispatch('eventName' , event.detail);
}
// <Component on:eventName={forward} />
ただ、これだとコードが冗長になる。
そこでSvelteでは、ショートハンドが用意されている。
シンプルにon:eventName
のみを記述する。なんて分かりやすい!
<script>
import Component from './Component.svelte'
</script>
<Component on:eventName />
DOM event forwarding
DOMのイベントも送ることができる。
送る方法は上記の方法と同じ。これは子コンポーネントに対して送っている。
<button on:click>Click!!</button>
Text inputs
Svelteでは、基本的なルールとして、データフローはトップダウンである。
つまり、親コンポーネントは子コンポーネントに対して、propsをセットしたり、HTML要素に属性をセットしたりできるが、その逆をすることはできない。
on:input
を定義して、val = event.target.value
で更新もできるが、冗長。
そこで使えるのがbind:
ディレクティブである。
<script>
let name = 'world';
</script>
// ここで双方向バイディングを実現している
<input bind:value={name}>
<h1>Hello {name}!</h1>
Numeric inputs
DOMの中では、すべての要素が文字列である。
input
要素のtype="number"
などは役に立たないのである。
しかし、bind:
ディレクティブはそれを考慮して、扱うことができる。
Checkbox inputs
checkboxに対してもバインディングできる。
checked
属性に対して、bind:
ディレクティブをつけてあげるだけ。
じゃあ、複数管理したいときはどうするの?
そういう時は、bind:group={~~~}
を使用する。
// const hoge = 1;
<input type="checkbox" bind:group={hoge} name="hoge" value="Bob and Alice" >
これを利用すると、繰り返し処理で簡単に記述できる。
each
で回す場合のbind:group
の値はデフォルトでチェックされるものになる。
<script>
let defaultPlayer = 'kuwahara'
let players = [
'kuwahara',
'sano',
'maki',
];
</script>
{#each players as player}
<label for="players">
<input type="checkbox" bind:group={defaultPlayer} name="players" value={player}>
{player}
</label>
{/each}
Textarea inputs
Text inputsと同じように扱うことができる。
HTML属性と同じ変数名を当てる場合は、省略してbind:value
のみで動く。
<script>
let value = 'Hello world';
</script>
<textarea name="text" bind:value></textarea>
<p>You write {value}</p>
select
要素に対しても、使うことができる。
この場合、初期値は設定していない。Svelteではデフォルトで、リストの最初の要素を初期値にしている。
<select bind:value={selected} on:change="{( ) => {hogehoge}}">
複数選択できるようにするためには、select
要素にmultiple
をつけてあげる。
<select multiple bind:value={selected}>
contenteditable="true"
の要素に対しては、textContent
とinnerHTML
をサポートしている。
Media elements
Dimentions
Component bindings
DOMのプロパティをバイディングできるようにComponentのプロパティもバイディングできる。
TODO:要復習
onMount
すべてのコンポーネントはlifecycle
を持っている。コンポーネントが作成された時に始まり、なくなった時に終了する。これは、ライフサイクルの中で、重要な瞬間に処理を実行したい場合に、実行できる機能があることを意味する。
そのうちの一つでよく使われるであろう機能がonMount
である。これはコンポーネントがDOMに対して、レンダリングされた後に走る処理を提供してくれる。
ライフサイクル関数はコンポーネントが作成されている間に呼ばれるため、その関数はコンポーネントに紐づいていないといけない。(setTimeout
ではない。)
onMount
コールバックが関数を返す場合、コンポーネントが破棄されたときにその関数が呼ばれる。
それ以外は、他の時間で復習。
Writable stores
すべてのアプリケーションの状態がアプリケーションのコンポーネント階層の中にあるわけではない。コンポーネント階層?は、コンポーネントの中のDOMツリーを指している?
異なるコンポーネントや通常のJavaScriptモジュールからアクセスする必要がある値を持っているときがある。
Svelteでは、stores
を使って、アクセスできるようにする。store
は単なるオブジェクト。store
はsubscribe
メソッドを持っていて、これは値が変更されるといつでも関係するものは知ることができる。
writable
メソッドは、subscribe
メソッドに加えてset
メソッドとupdate
メソッドを持っている。
TODO:この違いは何?
// stores.js
import { writable } from 'svelte/store';
export const count = writable(0);
// increment.svelte
const increment = () => {
count.update(n => n + 1);
}
// reset.svelte
const reset = () => {
count.set(0);
}
上記のやり方だと、subscribe
はするが、unsubscribe
はしない。つまり、コンポーネントが作成されたり、破棄されたりを繰り返すと、メモリリークを引き起こしてしまう。
なので、unsubscribed
を定義する必要がある。どうするかは、subscribe
メソッドの返り値がunsubscribe
メソッドであることを利用する。
const unsubscribed = count.subscribe(value => {
countValue = value;
}
これだけだと、不十分でこのunsubscribed
メソッドを呼び出す必要がある。
import { onDestroy } from 'svelte';
[...]
onDestroy(unsubscribed);
Auto-subscription
ただ、これを複数のストアに対して行なっていたら、冗長になってくる。
なので、Svelteでは、自動サブスクリプションが提供されている。どうするかというと、ストア変数の前に$
をつけるだけで良いらしい。
さらに、テンプレート内だけでなくイベントハンドラやリアクティブ宣言などの<script>
内でも使える。
<h1>The count is {$countValue}</h1>
Readable stores
すべてのストア変数が書き込みをしたいわけではない。つまり、読み取りしかしないストア変数もある。
そんな時は、readable
メソッドが用意されている。readable
メソッドの返り値は、end
関数。
start
関数はストアが通知を受け取った時に実行される。end
関数はストアがunsubscribe
されるときに実行される。
import { readable } from 'svelte/store'
// redable(<initial value>, <start function>)
export const time = readable(null, function() {
hogehoge
return function stop() {}
});
Derived stores
他のストア変数の値に基づいてストア変数を作成したいときもある。(派生するイメージ)
そんなときは、derived
メソッドを使う。
import { derived } from 'svelte/store';
export const val = derived(<initial value>, <start function>)
Custom stores
ドメイン固有のロジック?
多分、複数関数が散らばっていたものを一つまとめることができるっていうことか?
<script>
import { writable } from 'svelte/store';
const createCount = () => {
const { subscribe, set , update } = writable(0);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n + 1),
reset: () => set(0),
};
}
export const count = createCount();
</script>
<script>
import { count } from './store.js'
</script>
<h1>The count is {$count}</h1>
// countは`$`をつけていない点に注意!!
<button on:click={count.increment}>Increment</button>
<button on:click={count.decrement}>Decrement</button>
<button on:click={count.reset}>Reset</button>
Store binding
writable
なストアをバイディングすることもできる。
例えば、name
から派生したnickname
のストアがあったとする。
input
の値を変更すると、それに紐づくすべてのストア(ここではnickname
)も変更される。
<input bind:value={$name}>
// name: 'Bob' - nickname: 'Bob'
直接ストア変数を割り当てることができる。
<button on:click="{() => $name += '!'}"> // name.set($name + '!')と同じ
Add exclamation mark!
</button>
Motion
/* TODO: learning later */
Transition
/* TODO: learning later */
Animation
/* TODO: learning later */
Actions
/* TODO: learning later */
Advansed styling
The class directive(The style directive)
他のHTML属性と同じように、
JavaScriptを使ってクラス名を指定することができる。
<button
class="{klass === 'hoge' ? 'fizz' : ''}"
on:click="{() => klass = 'hoge'}"
>
Click Me!
</button>
これをもっと簡単な方法で行える。
クラス属性に対してfizz
クラスをくっつけて、値はBoolian
で制御すれば良い。
<button
class:fizz="{klass === 'hoge'}"
on:click="{() => klass = 'hoge'}"
>
Click Me!
</button>
クラス名とその変数名が同じであれば、値の部分を省略できる。
// Before
<button class:fizz={fizz}>
Click Me!
</button>
// After
<button class:fizz>
Click Me!
</button>
Component composition
Slots
HTML要素が子要素を持てるように、コンポーネントも子要素を持つことができる。
slot
を使うことで、親コンポーネントからコンテンツを子コンポーネントに渡して、指定の場所に入れることができる。
<script>
import Hoge from './Hoge.svelte'
</script>
<Hoge>
<div>
<p>Slot is nice</p>
</div>
</Hoge>
<div class="child">
<slot></slot>
</div>
Named slots
slot
には名前をつけて、それぞれにコンテンツを渡せる。
<div>
<slot name="hoge">
<p>Default display</p>
</slot>
</div>
[...]
<Child>
<p slot="hoge">FizzBuzz</p>
</Child>
Checking for slot content
slot
が空の場合にレンダリングしたくなかったり、slot
がある場合にクラスを当てたいなどのケースが出てくる。そういうときは、$$slots
変数を使ってプロパティをチェックできる。
$$slots
はオブジェクトで、そのキーは親コンポーネントから渡されたスロットの名前である。
Slot props
子コンポーネントで持っているデータを親コンポーネントに返したいときは、let:
ディレクティブを使う。
デフォルトのslot
の場合は、コンポーネント名の要素に定義する。
<script>
let counter = 0;
function increment() {
counter += 1;
}
</script>
<div on:click={increment}>
<slot {counter}></slot> // counter={counter}と同じ
</div>
<script>
import Child from "./Child.svelte";
</script>
<Child let:counter={counter}> // 子コンポーネントから親コンポーネントへデータを渡せるようにする。
<div> // ここからslotに移植されるコンテンツ
{#if counter >= 5} // 渡ってきたデータを使う
<p>Counter is more than 5.</p>
{:else}
<p>Counter is less than 4.</p>
{/if}
<p>Current counter is {counter}</p>
</div>
</Child>
名前付きスロットに対しても、let:
ディレクティブを使える。
名前付きスロットの場合は、slot="..."
の要素に定義する。
[...]
<div on:click={increment}>
<slot name="number" {counter}></slot>
</div>
[...]
<Child let:counter={counter}>
<div slot="number">
{#if counter >= 5}
<p>Counter is more than 5.</p>
{:else}
<p>Counter is less than 4.</p>
{/if}
<p>Current counter is {counter}</p>
</div>
</Child>
Context API
/* TODO: learning later */
Special elements
Svelteは、いろんな組み込み要素を提供している。
<svelte:self>
通常、モジュールは自分自身をインポートできないため、コンポーネントの中にコンポーネントを呼び出すことはできない。
<svelte:self>
要素を使うと、コンポーネントの中に、そのコンポーネント自体を入れることができる。
<script>
export let count = 0;
</script>
{#if count > 0}
<p>counting down...{count}</p>
<svelte:self count="count - 1" />
{:else}
<p>lift-off!</p>
{/if}
<svelte:component>
コンポーネントを動的に変更・破棄して、再生成できる。
これを使うと、if
ブロックの代わりに短く記述することができる。これはすごい。。。
<script>
import Child from "./Child.svelte";
import GrandChild from "./GrandChild.svelte";
const family = [
{
type: "GrandChild",
component: GrandChild
},
{
type: "Child",
component: Child
}
];
let selected = family[0];
</script>
<select bind:value={selected}>
{#each family as f}
<option value={f}>
{f.type}
</option>
{/each}
</select>
// Before
{#if selected.type === 'Child'}
<Child />
{:else}
<GrandChild />
{/if}
// After
<svelte:component this={selected.component} />
<p>Child Component</p>
<p>GrandChild Component</p>
<svelte:element>
<svelte:component>
要素のHTML要素バージョン。
ただし、このスペシャルエレメントでは、bind:this
のみサポートされている。
<svelte:window>
window
オブジェクトに対して、イベントリスナーを追加できる。
<svelte:window on:keydown={handleKeydown} />
また、バインディングを当てることもできる。
<svelte:window bind:scrollY={y} />
似ているスペシャル要素として<svelte:body>
要素がある。
window
オブジェクトに対して、イベントハンドラを追加できない時に、役立つ。
<svelte:body on:mouseenter={handleEvent} />
<svelte:head>
<svelte:head>
要素は<head>
タグの中に要素を挿入できる。
Railsでいうところの、content_for(:head)
みたいなノリか。
<svelte:head>
<link rel="stylesheet" href="./hoge.css">
</svelte:head>
<svelte:options>
<svelte:options>
要素はコンポーネントに対して、コンパイラオプションを指定できる。
例えば、<svelte:options tag="my-custom-component"/>
とすると、HTML内で<my-custom-component></my-custom-component>
でコンポーネントを作成できる。
<svelte:fragment>
名前付きスロットの中にDOM要素のコンテナで包むことなく、コンテンツを置くことができる。
つまり、<svelte:fragment>
の中に入れたコンテンツは、要素をつけることなくスロットに渡すことができる。