[キャッチアップ] Svelte
記事化しました。
概要
- 普段は Vue を主に扱ってる Webエンジニアが Svelte を今更キャッチアップするスクラップ
- Svelte ?? なんか凄くシンプルに Vue っぽく書ける新顔フレームワークでしょ?ぐらいの前提知識からスタート
- 基本的に学習ノート用途
リポジトリ確認
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 周りの話だけなので、公式ドキュメントの方に移って良さそう
公式ドキュメント確認 (トップページ)
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 とはまったく異なるアプローチを取ってるようでワクワクしてきたな。
チュートリアル
Web上で動かせるチュートリアルが用意されてる。環境構築とかせずにすぐに試せるのは良い。
Vue の SFC と同様に、 .svelte
拡張子のファイル一つでテンプレートとスクリプト、スタイルを書けるみたい。
ここからはチュートリアルを進めながら、気になったポイントとかを抜き出してコメントしてく。
Introduction / 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 コンポーネントになる。
Adding data
<script>
const name = 'world';
</script>
<h1>Hello {name.toUpperCase()}!</h1>
<script> ブロックで宣言された変数はテンプレートに自動でバインドされるみたい。
Vue の <script setup> に近い。 Vue が Svelte を真似たのかもしれないけど。
dynamic-attributes
<script>
let src = '/tutorial/image.gif';
let name = 'Rick Astley';
</script>
<img {src} alt="{name} dances.">
-
{}
を使うことで HTML 要素の属性にバインドすることができる -
src={src}
のような同名のバインドはショートハンドが提供されてる (これイケてる!!)
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}
みたいに、要素に対してランダムなクラスが付与されて、そこにだけスタイルが適用される模様。
Nested components
<script>
import Nested from './Nested.svelte'
</script>
<p>This is a paragraph.</p>
<Nested />
- 当然他のコンポーネントも import して利用可能
HTML tags
<script>
let string = `this string contains some <strong>HTML!!!</strong>`;
</script>
<p>{@html string}</p>
- バインドした文字列は通常エスケープされるが、あえて HTML タグとして出力したい場合は、
@html
修飾子を使用する - 当然 XSS のリスクがあるのでご利用は慎重に
Making an app
Svelte を使ったアプリ開発のためのツール連携の話
- vite, rollup, webpack と連携するための公式プラグインがあるぞ
- Web開発初心者のためのガイドもあるぞ
- vscode などのエディタの公式プラグインもあるから、
.svelte
ファイルをすぐにコンパイルできるぞ
チュートリアルからは脱線するけど、いずれ使うと思うので vscode で少し試してみることに。
シンタックスハイライトとインテリセンスが機能してることを確認。
reactive-assignments
<script>
let count = 0;
function incrementCount() {
count += 1
}
</script>
<button on:click={incrementCount}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
- 変数同様に、スクリプトブロックで宣言した関数はテンプレート側から呼び出すことができる
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 が定義できるのは面白い
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 とは全然違うわって実感が生まれた
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;
みたいにしようというお話。
急に使いづらいフレームワークになってきたぞ?
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 された変数にプロパティとして値をバインドすることができる。
なかなかヤバいなこの仕様。
default-values
<script>
export let answer = 'a mystery';
</script>
export する変数に初期値を代入しておけば、それはオプショナルなプロパティになる
spread-props
<script>
import Info from './Info.svelte';
const pkg = {
name: 'svelte',
version: 3,
speed: 'blazing',
website: 'https://svelte.dev'
};
</script>
<Info {...pkg} />
プロパティが複数ある時、プロパティ名と値のオブジェクトを展開して渡すこともできる。
プロパティ名と値が一致してるときに省略可能なのも含めて、本当にコードを短く書ける工夫が盛り沢山だ。
Logic
ここからは章ごとにまとめて流してく
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}
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>
Binding
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 つかってどうこうするよりも楽なのは確か。
Lifecycle
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();
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>
すごいな。グローバルストアをローカルストアかのように自然に扱えるのか。
Motion
Tweened
tweened
はなめらかな値の更新を行うためのオブジェクトで、 set
メソッドを戻す。
<script>
import { tweened } from 'svelte/motion';
const progress = tweened(0);
</script>
progress.set(1)
をした際に、0 から 1 への間の値を小刻みに連続更新し、ゆるやかなアニメーションなどをCSSナシで簡易的に提供する。
やば。
Spring
spring
は tweened
と似ているが、より更新が頻繁に行われる場合に用いられる。
let coords = spring({ x: 50, y: 50 }, {
stiffness: 0.1,
damping: 0.25
});
イマイチ使い分けはわかってないけど流しとく
Transitions
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}
animate
The animate directive
トランジションは、表示・非表示する要素に設定できたが、それ以外の要素が移動するのにアニメーションを設定するために animat
モジュールを使用する
import { flip } from 'svelte/animate';
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
animate:flip
>
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
するだけで簡単に要素に特性を設定できたりすると考えるとワクワクするな。
Classes
The class directive
クラスへのバインディングは Vue とほとんど同じ
<button
class="{current === 'foo' ? 'selected' : ''}"
on:click="{() => current = 'foo'}"
>foo</button>
Shorthand class directive
クラス名と、条件の変数名が同じ場合は省略可能
<div class:big>
<!-- ... -->
</div>
Component composition
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 と同じだけど微妙に差異があるのがややこしい
Context API
Context API は、コンポーネント間で Props を使わずにデータの共有をするための仕組み。
React の Context と基本的には一緒。
コンテキストのキーをグローバルユニークになるようにシンボルで生成する
const key = Symbol();
setContext
でコンテキストにオブジェクトをアサイン
setContext(key, {
getMap: () => map,
});
getContext
でコンテキストからデータ取り出し
const { getMap } = getContext(key);
コンポーネント間でのデータ共有という意味ではストアのほうが手軽だけど、 ストアと違って Context API はコンテキストを注入したコンポーネントとその子孫コンポーネントでしか参照できないということ、リアクティブでないことの違いがある。
Special elements
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>
名前付きスロットに複数要素を注入したい場合、通常はラッパー要素で包む必要があるが、それを回避することができる。
Module context
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 フィールドと考えればよいのかな。
Debugging
レンダリングされる際にログを出力させることができる
{@debug user}
地味に便利そう
Conguratulations
ここでチュートリアル終わり!
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 みたいなレイヤーのフレームワークが公式で用意されてるみたいだけど、今回は見送り。
開発環境構築
degit
言語仕様がそこそこわかったところで手元で手っ取り早く動かしたいのでこの記事を見ていく
環境をサクッと構築するコマンド
npx degit sveltejs/template my-svelte-project
一旦 TypeScript はナシで動かしてみる。
cd my-svelte-project
yarn install
スキャフォールドされたコードをざっとみると、rollup がベースの開発環境になってるっぽい。
yarn dev
localhost:8080 にアクセスするとアプリケーションが動いてる
vite
と、ここまでブログ記事を参考に動かしたけど、2017年の記事だし、今だったら Vite のほうがスマートな開発環境が作れるのでは。
yarn create vite my-vue-app --template svelte
cd my-vue-app/
yarn install
yarn dev
やっぱ早いしこっちを使おうかな。
チュートリアルで書いてきたコードを改めてローカルで動かしながら、アプリ開発のためのチートシートを用意しよう。