Closed37

[キャッチアップ] Svelte

shingo.sasakishingo.sasaki

概要

  • 普段は Vue を主に扱ってる Webエンジニアが Svelte を今更キャッチアップするスクラップ
  • Svelte ?? なんか凄くシンプルに Vue っぽく書ける新顔フレームワークでしょ?ぐらいの前提知識からスタート
  • 基本的に学習ノート用途
shingo.sasakishingo.sasaki

リポジトリ確認

https://github.com/sveltejs/svelte

Svelte is a new way to build web applications. It's a compiler that takes your declarative components and converts them into efficient JavaScript that surgically updates the DOM.

らしいけど、ここだけ見ると Vue と違いはなさそう。

あとは OSS 周りの話だけなので、公式ドキュメントの方に移って良さそう

shingo.sasakishingo.sasaki

公式ドキュメント確認 (トップページ)

https://svelte.dev/

Write less code

専門知識で、HTML/JS/CSS の知識だけでコンポーネントを書けるらしい。
React における JSX とか、Vue における SFC みたいなのが不要ってことかな。

No virtual DOM

Svelte はあくまでコンパイラであって、ランタイムコードで Svelte が実行されることはないらしい。本当かぁ??

Truly reactive

真のリアクティブがなにかはわからんが、とにかくすごいらしい。

Svelte is a radical new approach to building user interfaces. Whereas traditional frameworks like React and Vue do the bulk of their work in the browser, Svelte shifts that work into a compile step that happens when you build your app.

Instead of using techniques like virtual DOM diffing, Svelte writes code that surgically updates the DOM when the state of your app changes.

なるほど、それでどうやってリアクティブを実現するのかはまだイメージがわかないけど、React や Vue とはまったく異なるアプローチを取ってるようでワクワクしてきたな。

shingo.sasakishingo.sasaki

チュートリアル

Web上で動かせるチュートリアルが用意されてる。環境構築とかせずにすぐに試せるのは良い。
Vue の SFC と同様に、 .svelte 拡張子のファイル一つでテンプレートとスクリプト、スタイルを書けるみたい。

ここからはチュートリアルを進めながら、気になったポイントとかを抜き出してコメントしてく。

shingo.sasakishingo.sasaki

Introduction / Basics

https://svelte.dev/tutorial/basics

Svelte converts your app into ideal JavaScript at build time, rather than interpreting your application code at run time. This means you don't pay the performance cost of the framework's abstractions, and you don't incur a penalty when your app first loads.

ここを何度も推してるね。ランタイムでフレームワークが動作することがパフォーマンスに影響すること自体懐疑的だけどどうなんだろ。結局 Svelte コンパイラが生成した JavaScript が実行コスト高いなんてこともありそうだけど。

You can also ship components as standalone packages that work anywhere, without the overhead of a dependency on a conventional framework.

Svelte で作ったコンポーネントはフレームワーク問わずどこでも動く。ほんと〜?? WebComponent として使えるとかって話かなぁ。

In Svelte, an application is composed from one or more components. A component is a reusable self-contained block of code that encapsulates HTML, CSS and JavaScript that belong together, written into a .svelte file.

Svelte コンポーネントは HTML/CSS/JavaScript をカプセル化したやつ。 Vue の SFC と同じようなものと考えて良さそう。

<h1>Hello world!</h1>

↑がもう Svelte コンポーネントになる。

shingo.sasakishingo.sasaki

Adding data

https://svelte.dev/tutorial/adding-data

<script>
	const name = 'world';
</script>

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

<script> ブロックで宣言された変数はテンプレートに自動でバインドされるみたい。
Vue の <script setup> に近い。 Vue が Svelte を真似たのかもしれないけど。

shingo.sasakishingo.sasaki

dynamic-attributes

https://svelte.dev/tutorial/dynamic-attributes

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

<img {src} alt="{name} dances.">
  • {} を使うことで HTML 要素の属性にバインドすることができる
  • src={src} のような同名のバインドはショートハンドが提供されてる (これイケてる!!)
shingo.sasakishingo.sasaki

styling

https://svelte.dev/tutorial/styling

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

<style>
	p {
		color: purple;
		font-family: 'Comic Sans MS', cursive;
		font-size: 2em;
	}
</style>
  • <style> タグ内にスタイルを記述する
  • スタイルはデフォルトでコンポーネントスコープに閉じてくれる

p.svelte-urs9w7{color:purple;font-family:'Comic Sans MS', cursive;font-size:2em} みたいに、要素に対してランダムなクラスが付与されて、そこにだけスタイルが適用される模様。

shingo.sasakishingo.sasaki

HTML tags

https://svelte.dev/tutorial/html-tags

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

<p>{@html string}</p>
  • バインドした文字列は通常エスケープされるが、あえて HTML タグとして出力したい場合は、 @html 修飾子を使用する
  • 当然 XSS のリスクがあるのでご利用は慎重に
shingo.sasakishingo.sasaki

Making an app

https://svelte.dev/tutorial/making-an-app

Svelte を使ったアプリ開発のためのツール連携の話

  • vite, rollup, webpack と連携するための公式プラグインがあるぞ
  • Web開発初心者のためのガイドもあるぞ
  • vscode などのエディタの公式プラグインもあるから、 .svelte ファイルをすぐにコンパイルできるぞ

チュートリアルからは脱線するけど、いずれ使うと思うので vscode で少し試してみることに。

https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode

シンタックスハイライトとインテリセンスが機能してることを確認。

shingo.sasakishingo.sasaki

reactive-assignments

https://svelte.dev/tutorial/reactive-assignments

<script>
	let count = 0;

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

<button on:click={incrementCount}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
  • 変数同様に、スクリプトブロックで宣言した関数はテンプレート側から呼び出すことができる
shingo.sasakishingo.sasaki

reactive-declarations

https://svelte.dev/tutorial/reactive-declarations

<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>
  • Vue の computed を reactive declartions って読んでるみたい。和訳するとなんだろ。
  • ここへ来て異質な構文が出てきた感じがするけど、関数を書かずに computed が定義できるのは面白い
shingo.sasakishingo.sasaki

reactive-statements

https://svelte.dev/tutorial/reactive-statements

<script>
	let count = 0;
	$: {
		console.log('count changed!!')
		console.log('the count is ' + count)
	}
	$: if (count > 10) {
		alert('count is over 10')
	}

	function handleClick() {
		count += 1;
	}
</script>
  • 文もリアクティブに書ける
  • 文の中身を見て依存関係をウォッチして再実行する感じか
  • ここで初めて Vue とは全然違うわって実感が生まれた
shingo.sasakishingo.sasaki

updating-arrays-and-objects

https://svelte.dev/tutorial/updating-arrays-and-objects

Because Svelte's reactivity is triggered by assignments, using array methods like push and splice won't automatically cause updates. For example, clicking the button doesn't do anything.

そういうところも Vue (v2) に似てるんだなぁ…。

Svelte は代入をリアクティブのトリガーとするので

numbers.push(numbers.length + 1);

とかは使わずに

numbers[numbers.length] = numbers.length + 1;

みたいにしようというお話。

急に使いづらいフレームワークになってきたぞ?

shingo.sasakishingo.sasaki

declaring-props

https://svelte.dev/tutorial/declaring-props

child.svelte

<script>
	export let answer;
</script>

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

app.svelte

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

<Child answer={43}/>

いわゆる props を扱う場合は、スクリプト内で宣言した変数を export すれば良い。親コンポーネント側からは、 export された変数にプロパティとして値をバインドすることができる。

なかなかヤバいなこの仕様。

shingo.sasakishingo.sasaki

spread-props

https://svelte.dev/tutorial/spread-props

<script>
	import Info from './Info.svelte';

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

<Info {...pkg} />

プロパティが複数ある時、プロパティ名と値のオブジェクトを展開して渡すこともできる。
プロパティ名と値が一致してるときに省略可能なのも含めて、本当にコードを短く書ける工夫が盛り沢山だ。

shingo.sasakishingo.sasaki

Logic

ここからは章ごとにまとめて流してく

https://svelte.dev/tutorial/if-blocks

if blocks

テンプレート内で {} を使用することでマークアップの分岐が可能。¥
この感じ、何かのテンプレートエンジンでも見たな。

<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 blocks

同様に else の使用も可能

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

### Else-if blocks

同様に `else if`

```html
{#if x > 10}
	<p>{x} is greater than 10</p>
{:else if 5 > x}
	<p>{x} is less than 5</p>
{:else}
	<p>{x} is between 5 and 10</p>
{/if}

Each blocks

iterable なオブジェクトを用いてループを表現することも可能

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

Keyed each blocks

vue の :key のように、ループごとのアイテムのユニーク属性を指定することで更新を効率的に行う。

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

Await blocks

Promise の状態で分岐することも可能。これは強力かもー

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

Events

https://svelte.dev/tutorial/dom-events

DOM events

DOM イベントの関数へのバインドは on ディレクティブを使用する

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

Inline handlers

DOMイベントはインラインハンドラも定義可能

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

他のフレームワークだと、特にループ内でのインラインハンドラはパフォーマンスの都合推奨されないが、Svelte の場合は事前のコンパイルで最適されるため、問題ないとのこと。

なるほど、コード最適化をコンパイラがガンガンやってくれるから書き味良いのか。

Event modifiers

DOM イベントへの modifiers (preventDefault,stopPropagation など) の指定は | で設定可能

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

on:click|once|capture={...} のようにチェインも可能

Component events

コンポーネントから発火するイベントは createEventDispatcher を使用してコンポーネント初期化のタイミングで宣言しておく必要がある。

<script>
	import { createEventDispatcher } from 'svelte'
	const dispatch = createEventDispatcher()

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

<button on:click={sayHello}>
	Click to say hello
</button>

Vue の emit の仕組みと同じなんだろうけど、Svelte にしてはここへきて急に複雑になった感じがする。 React みたいにイベントハンドラを props で渡すのに一本化しちゃったほうがシンプルになりそうな。

Event forwarding

イベントハンドラを設定せずに、単に on ディレクティブを定義した場合、子コンポーネントから受け取ったイベントを親コンポーネントにそのまま伝搬してくれる。これによってイベントのバケツリレーを緩和することができる。

<Inner on:message/>

便利と言えば便利だけど、 TypeScript 化するときにこのあたりの型で苦労する気配がする

DOM event forwarding

DOM イベントの場合も同様に、イベントハンドラを省略することで親コンポーネントへ伝搬可能

<button on:click>
	Click me
</button>
shingo.sasakishingo.sasaki

Binding

https://svelte.dev/tutorial/text-inputs

Text inputs

input コンポーネントとの双方向バインディングを bind:value で可能。
vue の v-model ね。

<input bind:value={name}>

Numeric inputs

本来の DOM イベントでは、数値も文字列として扱われてしまうが、 type="number" type="range" のような、数値として扱いたい DOM value がある場合、 Svelte は暗黙的に Number への変換を行ってくれる。

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

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

Checkbox inputs

Checkbox の状態をバインドする場合は bind:checked

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

Group inputs

複数のラジオボタンをグループ化し、どれが選択状態になっているかを管理するために bind:group を使用する。

<label>
	<input type=radio bind:group={scoops} name="scoops" value={1}>
	One scoop
</label>

<label>
	<input type=radio bind:group={scoops} name="scoops" value={2}>
	Two scoops
</label>

<label>
	<input type=radio bind:group={scoops} name="scoops" value={3}>
	Three scoops
</label>

同様にチェックボックスの場合、複数のチェックボックスの状態を配列でバインドできる

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

Textarea inputs

Textarea も同様に bind:value で OK

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

Select bindings

Select

<select bind:value={selected} on:change="{() => answer = ''}">

Select multiple

<select multiple bind:value={flavours}>

Contenteditable bindings

滅多に使わないけど、 editable な要素に対しては innnerHTML をバインド可能

<div
	contenteditable="true"
	bind:innerHTML={html}
></div>

Each block bindings

each ブロック内でも当然バインド可能

{#each todos as todo}
	<div class:done={todo.done}>
		<input
			type=checkbox
			bind:checked={todo.done}
		>

		<input
			placeholder="What needs to be done?"
			bind:value={todo.text}
		>
	</div>
{/each}

class のバインドがしれっと初登場してるな。

Media elements

<audio> や <video> はいろんな属性を持ってるけど、もちろんどれもバインド可能

	<video
		poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
		src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
		on:mousemove={handleMove}
		on:touchmove|preventDefault={handleMove}
		on:mousedown={handleMousedown}
		on:mouseup={handleMouseup}
		bind:currentTime={time}
		bind:duration
		bind:paused>
		<track kind="captions">
	</video>

Dimensions

clientWidth clientHeight offsetWidth offsetHeight のような、ブロック要素が持つ領域プロパティには、 ReadOnly でバインド可能。 DOM の変更によって変数が更新されるが、変数を更新しても DOM のプロパティは変わらないので注意。

<div bind:clientWidth={w} bind:clientHeight={h}>
	<span style="font-size: {size}px">{text}</span>
</div>

あんまり使わないパターンだと思うけど、こういうとこにハマることはあるかも?

This

DOM に対する this コンテキストをバインドすることが可能。これも ReadOnly なので注意。

<canvas
	bind:this={canvas}
	width={32}
	height={32}
></canvas>

これやりだすと大変なことになりそうだ。

Component bindings

コンポーネントのプロパティに対してバインドする

<Keypad bind:value={pin} on:submit={handleSubmit}/>

これがかなり基本的なコンポーネントの使い方かな

Binding to component instances

コンポーネントインスタンスに対する参照をバインドする。
小難しいけど、要はコンポーネントそのものを親コンポーネントから扱えるようにするのか。

<script>
	let field;
</script>

<InputField bind:this={field} />

基本的にアンチパターンな気がするなコレは。
でも子コンポーネントの関数を直接呼び出したい場合、 Vue で ref つかってどうこうするよりも楽なのは確か。

shingo.sasakishingo.sasaki

Lifecycle

https://svelte.dev/tutorial/onmount

onMount

onMount: コンポーネントマウント時(最初にDOMがレンダリングされた時) に実行される

<script>
	import { onMount } from 'svelte'
	let photos = [];
	
	onMount(async () => {
		const res = await fetch(`https://jsonplaceholder.typicode.com/photos?_limit=20`)
		photos = await res.json()
	})
</script>

onDestroy

onDestroy: コンポーネントが削除されるときに実行される

<script>
	import { onDestroy } from 'svelte';

	let counter = 0;
	const interval = setInterval(() => counter += 1, 1000);

	onDestroy(() => clearInterval(interval));
</script>

onMounted onDestroy ともに、この関数を呼び出すファイル自体がどこにあっても構わないらしいけど、スコープがわかりづらくなりそうではある。

beforeUpdate and afterUpdate

  • beforeUpdate: DOM が更新される直前に実行
  • afeterUpdate: DOM が更新された直後に実行

tick

Vue でいう nextTick

await tick();
shingo.sasakishingo.sasaki

Stores

https://svelte.dev/tutorial/writable-stores

Writable stores

Svelte には、関連性のないコンポーネントないしモジュール間でステートを共有するための仕組みがビルトインで提供されている模様。

writable は、書込み可能なストアを提供し、 count は set update メソッドを通じて更新し、 subscribe へ変更を監視することができる。

import { writable } from 'svelte/store';

export const count = writable(0);

update は更新後の値を戻す関数を、 set は更新後の値を渡す違いがある。

	function increment() {
		count.update(n => n + 1)
	}

	function reset() {
		count.set(0)
	}

subscribe 関数のコールバックで、変更後の値を受け取ることができる

	count.subscribe(value => {
		countValue = value;
	});

Vue でいうところの provite/inject パターンをものすごく簡単に書けるのはイケてる。よほど複雑な状態管理をしない限りは、 Vuex みたいな Flux ライクのパターンは必要ないかも?

Auto-subscriptions

subscribe メソッドは、返り値で unsubscribe する関数を戻してくれるので、コンポーネントが破棄されるタイミングなどで適切に unsubscribe することでメモリリークを避ける必要がある。

const unsubscribe = count.subscribe(value => {
	countValue = value;
});

しかし、ストアを使用するたびにこのパターンを書くのは冗長すぎるので、Svelte にはこれを簡略化する仕組みが用意されている。

なんとストアオブジェクトの頭に $ を付けるだけで、 subscribe/unsubscribe が自動で行われる

<script>
	import { count } from './stores.js';
</script>

<h1>The count is {$count}</h1>

とんでもない仕様に見えるけど、一々 subscribe することを考えると自然か…?
このあたりは Vue の常識が通じないな。

Readable stores

writable はストアを参照する側からも自由に変更が出来るので不適切な場合はある。そういう場合は参照専用の readable を使用する。

readable の第一引数は初期値で、第二引数には、初めて subscribe されたときに呼び出される関数を定義する。この関数は、 subscriber がゼロになったときの停止処理を戻すようにする。

export const time = readable(new Date(), function start(set) {
	const interval = setInterval(() => {
		set(new Date());
	}, 1000);

	return function stop() {
		clearInterval(interval);
	};
});

ここちょっと癖が強いな。 subscriber が 0 から 1 になったときの処理と、1 から 0 になったときの処理を書けるわけね。

Derived stores

Derved store は、他のストアから派生したストアのこと。

export const elapsed = derived(
	time,
	$time => Math.round(($time - start) / 1000)
);

ストア版の computed みたいなものなんだろうけど、これはなかなか直感的にわかりづらいな。

Custom stores

subscribe メソッドさえ正しく含んでいれば、それはいかなる実装であってもストアと呼べる。

そのため、値だけでなく、それを操作するロジックも含めて戻すことで、ビジネスロジックをストアにカプセル化することができる。

import { writable } from 'svelte/store';

function 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();

上記は writable を用いてストアを生成しているが、更新するための set update は返していないため、更新方法を隠蔽することができてる。

Vue だと composition API でどうにか実現してることがすんなり出来てヤバいな。

Store bindings

ストアが set メソッドを持つ時、これまで通りテンプレート内でバインドすることが可能

<input bind:value={$name}>

<button on:click="{() => $name += '!'}">
	Add exclamation mark!
</button>

すごいな。グローバルストアをローカルストアかのように自然に扱えるのか。

shingo.sasakishingo.sasaki

Motion

https://svelte.dev/tutorial/tweened

Tweened

tweened はなめらかな値の更新を行うためのオブジェクトで、 set メソッドを戻す。

<script>
	import { tweened } from 'svelte/motion';

	const progress = tweened(0);
</script>

progress.set(1) をした際に、0 から 1 への間の値を小刻みに連続更新し、ゆるやかなアニメーションなどをCSSナシで簡易的に提供する。

やば。

Spring

springtweened と似ているが、より更新が頻繁に行われる場合に用いられる。

let coords = spring({ x: 50, y: 50 }, {
	stiffness: 0.1,
	damping: 0.25
});

イマイチ使い分けはわかってないけど流しとく

shingo.sasakishingo.sasaki

Transitions

https://svelte.dev/tutorial/transition

The transition directive

Svelte では要素のトランジションはディレクティブを使用するみたい

<script>
	import { fade } from 'svelte/transition'
	let visible = true;
</script>

<label>
	<input type="checkbox" bind:checked={visible}>
	visible
</label>

{#if visible}
	<p transition:fade>
		Fades in and out
	</p>
{/if}

使用するディレクティブを import しなきゃならないってのがちょっとしたポイントか

Adding parameters

トランジションはパラメータを受け取ることも出来る。

{#if visible}
	<p transition:fly="{{ y: 200, duration: 2000}}">
		Fades in and out
	</p>
{/if}

アニメーション作るのがめっちゃ簡単だ

In and out

インとアウトで異なるトランジションを使うことも

{#if visible}
	<p in:fly="{{ y: 200, duration: 2000 }}" out:fade>
		Flies in, fades out
	</p>
{/if}

対称でないアニメーションはちょっと違和感あるけど

Custom CSS transitions

組み込みのトランジションでなく、CSSを使ったカスタムトランジションも使用可能

	function spin(node, { duration }) {
		return {
			duration,
			css: t => {
				const eased = elasticOut(t);

				return `
					transform: scale(${eased}) rotate(${eased * 1080}deg);
					color: hsl(
						${Math.trunc(t * 360)},
						${Math.min(100, 1000 - 1000 * t)}%,
						${Math.min(50, 500 - 500 * t)}%
					);`
			}
		};
{#if visible}
	<div class="centered" in:spin="{{duration: 8000}}" out:fade>
		<span>transitions!</span>
	</div>
{/if}

Custom JS transitions

トランジションはCSSだけでなく、JavaScriptを使うことも可能。

function typewriter(node, { speed = 1 }) {
	const valid = (
		node.childNodes.length === 1 &&
		node.childNodes[0].nodeType === Node.TEXT_NODE
	);

	if (!valid) {
		throw new Error(`This transition only works on elements with a single text node child`);
	}

	const text = node.textContent;
	const duration = text.length / (speed * 0.01);

	return {
		duration,
		tick: t => {
			const i = Math.trunc(text.length * t);
			node.textContent = text.slice(0, i);
		}
	};
|

これちょっとおもしろいな。1文字ずつテキストを増やしていってタイプライター風にしてる。

Transition events

トランジションの各フェーズにコールバック関数を設定できる

<p
	transition:fly="{{ y: 200, duration: 2000 }}"
	on:introstart="{() => status = 'intro started'}"
	on:outrostart="{() => status = 'outro started'}"
	on:introend="{() => status = 'intro ended'}"
	on:outroend="{() => status = 'outro ended'}"
>
	Flies in and out
</p>

Local transitions

Each ブロック内の要素にトランジションを設定すると、ブロック全体にトランジションが適用されてしまうが、 |local を設定することで、ブロック内の個々の要素に対してトランジションを適用することができる。

{#if showItems}
	{#each items.slice(0, i) as item}
		<div transition:slide|local>
			{item}
		</div>
	{/each}
{/if}

最初よくわからなかったけど、リスト全体か、リスト内の個々の要素かの違いね。

Deferred transitions

別々のトランジションが設定された要素同士をクロスフェードするための仕組みとして、 crossfade が用意されている。

コードが長いのと、公式ドキュメント以上の説明ができないので割愛

Key blocks

{$key} ブロックで囲まれた要素は、指定された状態が更新された場合に、要素を一度削除し再生成する。一見無駄だが、状態が更新されるたびにアニメーションを再実行したいときに使える。

{#key value}
	<div transition:fade>{value}</div>
{/key}
shingo.sasakishingo.sasaki

animate

https://svelte.dev/tutorial/animate

The animate directive

トランジションは、表示・非表示する要素に設定できたが、それ以外の要素が移動するのにアニメーションを設定するために animat モジュールを使用する

import { flip } from 'svelte/animate';
<label
	in:receive="{{key: todo.id}}"
	out:send="{{key: todo.id}}"
	animate:flip
>
shingo.sasakishingo.sasaki

Actions

https://svelte.dev/tutorial/actions

The use directive

アクションは、要素単位のライフサイクルを定義するために使用できる。

export function clickOutside(node) {
	const handleClick = (event) => {
		if (!node.contains(event.target)) {
			node.dispatchEvent(new CustomEvent("outclick"));
		}
	};

	document.addEventListener("click", handleClick, true);

	return {
		destroy() {
			document.removeEventListener("click", handleClick, true);
		}
	};
}
{#if showModal}
	<div class="box" use:clickOutside on:outclick={() => (showModal = false)}>
		Click outside me!
	</div>
{/if}

use ディレクティブを用いてアクションの使用を宣言し、要素のマウント時、アンマウント時にライフサイクルイベントを発火させることができる。

Adding parameters

トランジションやアニメーション同様、アクションにもパラメータの付与が可能

export function longpress(node, duration) {
}
<button use:longpress={duration}>Push</button>

アクション面白いな。汎用的なアクションのライブラリとかあって use するだけで簡単に要素に特性を設定できたりすると考えるとワクワクするな。

shingo.sasakishingo.sasaki

Classes

https://svelte.dev/tutorial/classes

The class directive

クラスへのバインディングは Vue とほとんど同じ

<button
	class="{current === 'foo' ? 'selected' : ''}"
	on:click="{() => current = 'foo'}"
>foo</button>

Shorthand class directive

クラス名と、条件の変数名が同じ場合は省略可能

<div class:big>
	<!-- ... -->
</div>
shingo.sasakishingo.sasaki

Component composition

https://svelte.dev/tutorial/slots

Slots

スロットは Vue というか WebComponents とだいたい一緒

<div class="box">
	<slot></slot>
</div>

Slot fallbacks

Slot に何も注入されなかった場合のフォールバックの埋め込みが可能

<div class="box">
	<slot>
		<em>no content was provided</em>
	</slot>
</div>

Named slots

名前付きスロットもだいたいいつもの書き方

<article class="contact-card">
	<h2>
		<slot name="name">
			<span class="missing">Unknown name</span>
		</slot>
	</h2>

	<div class="address">
		<slot name="address">
			<span class="missing">Unknown address</span>
		</slot>
	</div>

	<div class="email">
		<slot name="email">
			<span class="missing">Unknown email</span>
		</slot>
	</div>
</article>
<ContactCard>
	<span slot="name">
		P. Sherman
	</span>

	<span slot="address">
		42 Wallaby Way<br>
		Sydney
	</span>
</ContactCard>

Checkingfor slot content

$$slots を用いて、注入されたスロットを参照できる。

{#if $$slots.comments}
	<div class="discussion">
		<h3>Comments</h3>
		<slot name="comments"></slot>
	</div>
{/if}

Slot props

注入された Slot に対してプロパティを注入することも可能

<script>
	let hovering;

	function enter() {
		hovering = true;
	}

	function leave() {
		hovering = false;
	}
</script>

<div on:mouseenter={enter} on:mouseleave={leave}>
	<slot hovering={hovering}></slot>
</div>
<Hoverable let:hovering>
	<div class:active={hovering}>
		{#if hovering}
			<p>I am being hovered upon.</p>
		{:else}
			<p>Hover over me!</p>
		{/if}
	</div>
</Hoverable>

だいたい Vue と同じだけど微妙に差異があるのがややこしい

shingo.sasakishingo.sasaki

Context API

https://svelte.dev/tutorial/context-api

Context API は、コンポーネント間で Props を使わずにデータの共有をするための仕組み。
React の Context と基本的には一緒。

コンテキストのキーをグローバルユニークになるようにシンボルで生成する

const key = Symbol();

setContext でコンテキストにオブジェクトをアサイン

	setContext(key, {
		getMap: () => map,
	});

getContext でコンテキストからデータ取り出し

const { getMap } = getContext(key);

コンポーネント間でのデータ共有という意味ではストアのほうが手軽だけど、 ストアと違って Context API はコンテキストを注入したコンポーネントとその子孫コンポーネントでしか参照できないということ、リアクティブでないことの違いがある。

shingo.sasakishingo.sasaki

Special elements

https://svelte.dev/tutorial/svelte-self

svelte:self

コンポーネント内に、自身を再帰的に埋め込むことができる

{#if file.files}
	<svelte:self {...file}/>
{:else}
	<File {...file}/>
{/if}

svelte:component

どのコンポーネントを描画するかを動的に決定できる

<svelte:component this={selected.component}/>

svelte:window

Window オブジェクトのイベントリスナーをバインド出来る抽象コンポーネント?

<svelte:window on:keydown={handleKeydown}/>

window オブジェクトのデータバインディングも可能

<svelte:window bind:scrollY={y}/>

なにこれおもしろ

svelte:body

同様に body も

<svelte:body
	on:mouseenter={handleMouseenter}
	on:mouseleave={handleMouseleave}
/>

svelte:head

header への要素の注入が可能。

<svelte:head>
	<link rel="stylesheet" href="/tutorial/dark-theme.css">
</svelte:head>

Nuxt でこんなん見た気がする。

svelte:options

Svelte コンパイラのオプションを設定するための要素

<svelte:options immutable={true}/>

あんまり使うこと無さそうだけど

svelte:fragment

複数要素を束ねる抽象要素

<Box>
	<svelte:fragment slot="footer">
		<p>All rights reserved.</p>
		<p>Copyright (c) 2019 Svelte Industries</p>
	</svelte:fragment>
</Box>

名前付きスロットに複数要素を注入したい場合、通常はラッパー要素で包む必要があるが、それを回避することができる。

shingo.sasakishingo.sasaki

Module context

https://svelte.dev/tutorial/sharing-code

Sharing code

<script context="module"> をコンポーネントの先頭で宣言しておくと、同一のコンポーネントすべてで状態を共有できる。

<script context="module">
	let current;
</script>

なかなか使い勝手難しいと思うし、これを使いたいなら親コンポーネントで制御するほうが良いと思う。

Exports

モジュールコンテキスト内で export されたコードは、同一コンポーネント内から利用可能

<script context="module">
	const elements = new Set();

	export function stopAll() {
		elements.forEach(element => {
			element.pause();
		});
	}
</script>

うーん、オブジェクト指向における private static フィールドと考えればよいのかな。

shingo.sasakishingo.sasaki

Conguratulations

https://svelte.dev/tutorial/congratulations

ここでチュートリアル終わり!

To get set up in your local development environment, check out the quickstart guide.

次はこれで実際にアプリを作ってみるのが良さそう

If you're looking for a more expansive framework that includes routing, server-side rendering and everything else, take a look at SvelteKit.

Next/Nuxt みたいなレイヤーのフレームワークが公式で用意されてるみたいだけど、今回は見送り。

shingo.sasakishingo.sasaki

開発環境構築

degit

言語仕様がそこそこわかったところで手元で手っ取り早く動かしたいのでこの記事を見ていく

https://svelte.dev/blog/the-easiest-way-to-get-started

環境をサクッと構築するコマンド

npx degit sveltejs/template my-svelte-project

一旦 TypeScript はナシで動かしてみる。

cd my-svelte-project
yarn install

スキャフォールドされたコードをざっとみると、rollup がベースの開発環境になってるっぽい。

yarn dev

localhost:8080 にアクセスするとアプリケーションが動いてる

vite

と、ここまでブログ記事を参考に動かしたけど、2017年の記事だし、今だったら Vite のほうがスマートな開発環境が作れるのでは。

https://ja.vitejs.dev/guide/#vite-をオンラインで試す

yarn create vite my-vue-app --template svelte
cd my-vue-app/
yarn install
yarn dev

やっぱ早いしこっちを使おうかな。

shingo.sasakishingo.sasaki

チュートリアルで書いてきたコードを改めてローカルで動かしながら、アプリ開発のためのチートシートを用意しよう。

このスクラップは2022/04/13にクローズされました