Closed45

Svelte学習帳

kawarimidollkawarimidoll

Introduction

  • とりあえずApp.svelteにhtmlを書いていけばコンポーネントになる
  • {var}でhtml内に変数を書き出せる
    • シングルマスタッシュなんだ まあJSXも同じか
    • この中はJSが解釈されるっぽい メソッドも書ける
App.svelte
<script>
  const name = "Svelte";
</script>

<h1>Hello {name.toUpperCase()}!</h1>
  • 属性の名前と変数名が同じ場合は省略可能
App.svelte
<script>
  const src = './image.gif';
  const alt = 'alt text';
</script>

<img {src} {alt} />
<!-- <img src={src} alt={alt} /> と書かなくて良い -->
  • 別ファイルのコンポーネントを使う場合はscriptタグでimportする
    • カスタムタグ名はパスカルケース
    • svelteファイル内に書いたスタイルはそのファイル内でのみ適用される
Sub.svelte
<p>This is a sub component.</p>
<style>
p {
  color: red;
}
</style>
App.svelte
<script>
	import Sub from './Sub.svelte';
</script>
<p>This is a main component.</p>
<Sub />
<style>
p {
  color: blue;
}
</style>

  • 変数内のhtmlを直接展開したいときは{@html var}にする
    • これは覚えてなくてもいいや
kawarimidollkawarimidoll

Reactivity 1

$state rune を使うことで変数をリアクティブにできる
以下のコードはボタンをクリックするとカウントアップするコード

App.svelte
<script>
	let count = $state(0);

	function increment() {
		count ++;
	}
</script>

<button onclick={increment}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

onclickに無名関数を突っ込んでも動いた

App.svelte
<script>
	let count = $state(0);
</script>

<button onclick={(()=>count++)}>
	Clicked {count}
	{count === 1 ? 'time' : 'times'}
</button>

配列の値の変更も可能

App.svelte
<script>
	let numbers = $state([1, 2, 3, 4]);
	function addNumber() {
		numbers.push(numbers.length + 1);
	}
</script>

<p>{numbers.join(' + ')} = {numbers.reduce((t, n) => t + n, 0)}</p>

<button onclick={addNumber}>
	Add a number
</button>

変数が別の$state変数を参照している場合、参照先の更新を監視するためには$derived rune を使う必要がある
以下ではnumbersの更新に伴ってtotalも更新されるが、$derived$stateにしたり外したりすると値が更新されない

App.svelte
 <script>
     let numbers = $state([1, 2, 3, 4]);
+    let total = $derived(numbers.reduce((t, n) => t + n, 0));
     function addNumber() {
         numbers.push(numbers.length + 1);
     }
 </script>

-<p>{numbers.join(' + ')} = {numbers.reduce((t, n) => t + n, 0)}</p>
+<p>{numbers.join(' + ')} = {total}</p>

 <button onclick={addNumber}>
     Add a number
 </button>
kawarimidollkawarimidoll

Reactivity 2

リアクティブな値をconsole.log(stateValue)しようとすると警告が出る
値がProxy https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy になっているため…らしいがよくわからない
$state.snapshot(stateValue)することで警告を消せる たぶんdeep copyしている
または$inspect(stateValue)というコードで値が更新されるたびにエコーすることができる これはproduction build時に消されるとのことなのでこっちを使うのが良さそう
$inspect(stateValue).with(console.trace)するとより詳細な情報を出力できる

変数の変更時にどのように処理を行うかの$effectを自作することができる
以下はコンポーネントマウントからの経過時間(elapsed)を表示するコード
effect関数はcleanup関数を返すことができ、これは状態の更新時およびコンポーネント破棄時に実行される

App.svelte
<script>
	let elapsed = $state(0);
	let interval = $state(1000);

	$effect(() => {
		const id = setInterval(() => {
			elapsed += 1;
		}, interval);

        // これがないと古いsetIntervalが動き続ける
		return () => {
			clearInterval(id);
		};
	});
</script>

<button onclick={() => interval /= 2}>speed up</button>
<button onclick={() => interval *= 2}>slow down</button>

<p>elapsed: {elapsed}</p>
<p>interval: {interval}</p>

なお上のようなコードはたぶんeffectを使わないと実現できないが、effectなしで実行できるのであれば可能な限りeffectを使わないべき
個人的にもあまり使いたくはない 副作用がめんどくさそうなので

なお、状態値もコンポーネントのようにexport / importして使うことができるが、拡張子は.svelteまたは.svelte.js / .svelte.tsである必要がある

kawarimidollkawarimidoll

Props

属性値として渡された値を引き出すには$props() rune を使用する

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

<Nested text={"number"} answer={42} />
<Nested text={"default"} />
Nested.svelte
<script>
	let { text, answer = "OK" } = $props();
</script>

<p>The {text} is {answer}</p>
output
The number is 42
The default is OK

通常のhtmlの属性値と同じく、spread operatorで省略が可能

App.svelte
<script>
	import Nested from './Nested.svelte';
	const attrs = { name: 'foo', anser: 'bar' };
</script>

<Nested {...attrs} />
kawarimidollkawarimidoll

Logic

論理構造を表現できる

  • {# ...}: 開始
  • {: ...}: 継続
  • {/ ...}: 終了

if

  • jsと異なり、ifの後の条件をカッコで囲む必要はない
App.svelte
<script>
	let count = $state(0);
</script>

<button onclick={count++}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

{#if count > 5}
	<p>{count} is greater than 5</p>
{:else if count < 2}
	<p>{count} is less than 2</p>
{:else}
	<p>{count} is between 2 and 5</p>
{/if}

each

  • array-likeオブジェクトならeachで回すことができる
  • インデックスを取ることも可能
App.svelte
<script>
	const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
</script>

{#each colors as color, i}
	<div style="background: {color}">{i+1} {color}</div>
{/each}
  • eachする要素を削除する場合はkeyed loopにするのが安全
  • 具体的な状況を理解できていないのでここは勉強した記録を残すに留める
{#each things as thing (thing.id)}
	<Thing name={thing.name} />
{/each}

await

  • {#await ...}でpromiseを表現可能
App.svelte
<script>
	import { roll } from './utils.js';
	let promise = $state(roll());
</script>

<button onclick={() => promise = roll()}>
	roll the dice
</button>

{#await promise}
	<p>...rolling</p>
{:then number}
	<p>you rolled a {number}!</p>
{:catch error}
	<p style="color: red">{error.message}</p>
{/await}
  • エラーが無いことがわかっている場合は{#await ...}{:then ...}をまとめて書ける
{#await promise then number}
	<p>you rolled a {number}!</p>
{/await}
kawarimidollkawarimidoll

Events

on<name>でイベントハンドリングが可能

App.svelte
<script>
	let b = $state(false);
	function onclick(event) {
		b = true;
	}
</script>

<button {onclick}>
	{b ? "clicked" : "click me"}
</button>

インラインも可

App.svelte
<script>
	let b = $state(false);
</script>

<button onclick={(event) => {
		b = true;
	}}>
	{b ? "clicked" : "click me"}
</button>

イベントハンドラがネストしている場合は内側から順に発火するっぽい

App.svelte
<div onclick={() => alert(`div clicked`)} role="presentation">
	<button onclick={() => alert(`button clicked`)}>
		click me
	</button>
</div>

on<event>captureを使うと逆順になる 混在している場合はcaptureを全て消化してから無captureになるみたい
これはあまり多用したくないな…

App.svelte
<div onclickcapture={() => alert(`div clicked`)} role="presentation">
	<button onclickcapture={() => alert(`button clicked`)}>
		click me
	</button>
</div>

propsにインラインで関数を渡しても良い

App.svelte
<script>
	import Stepper from './Stepper.svelte';
	let value = $state(0);
</script>

<p>The current value is {value}</p>

<Stepper increment={()=>value++} decrement={()=>value--} />
Stepper.svelte
<script>
	let {increment, decrement} = $props();
</script>

<button onclick={decrement}>-1</button>
<button onclick={increment}>+1</button>

propsを変数に出してそれをspreadすることも可能

App.svelte
<script>
	import MyButton from './MyButton.svelte';

	function hook() {
		alert("clicked");
	}
</script>

<MyButton onclick={hook} />
MyButton.svelte
<script>
	let props = $props();
</script>

<button {...props}>Push me</button>

ただし$関数を直接マークアップ内で使うことはできない

MyButton.svelte
<button {...$props()}>Push me</button>
<!-- `$props()` can only be used at the top level of components as a variable declaration initializer -->
kawarimidollkawarimidoll

Bindings

原則としてデータフローは親から子に流れる
すなわちコンポーネントは自身または子の値をセットできるがその他(親兄弟)の値を書き換えることはできない

ではこのようにinputがvalueを持つ場合はどうなるか

App.svelte
<script>
	let name = $state('world');
</script>

<input value={name} />

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

<input>から見てnameは親(App)の変数なので直接変更できない
したがって原則どおりなら親がoninputを定義して変更をハンドリングすることになる

App.svelte
 <script>
	let name = $state('world');
+	function oninput(event) {
+		name = event.target.value;
+	}
 </script>

-<input value={name} />
+<input value={name} {oninput} />

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

ただしこれは余りにも頻出なのでbindingの仕組みが整備されている
実際はbind:を前置するだけで良い

App.svelte
 <script>
	let name = $state('world');
 </script>

-<input value={name} />
+<input bind:value={name} />

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

変数名と同じ場合の短縮記法はマスタッシュなしで<input bind:value />

  • <input type="number" bind:value={value}><input type="range" bind:value={value}>では値を数値として扱う
  • <input type="checkbox" bind:checked={value}>では値を真偽値として扱う
  • <select bind:value={selected}>...</select>および<textarea bind:value={value}></textarea>も使える
  • 複数のラジオボタンに同じ変数で<input type="radio" value="hoge" bind:group={selected} />とすると、複数の値の中から選んだ1つがselectedに入る
  • 複数のチェックボックスに同じ変数で<input type="checkbox" value="hoge" bind:group={selected} />とすると、選んだ複数の値を要素とする配列がselectedに入る
    • これは<select multiple bind:group={selected}>...</select>も同様
kawarimidollkawarimidoll

Classes and Styles

flexという真偽値変数がtrueの場合にclass="flex"を付加したいとき、以下のような書き方ができる
3番目はこれまでも出てきた省略記法

App.svelte
<div class="{flex ? 'flex' : ''}">flex or not</div>
<div class:flex={flex}>flex or not</div>
<div class:flex>flex or not</div>

上記はclassでなくstyleでも同様

子コンポーネントにスタイルの値を渡したい場合、--varName記法を用いてCSS変数を定義できる

App.svelte
<script>
	import Box from './Box.svelte';
</script>

<Box --color="red" />
Box.svelte
<div class="box">box</div>

<style>
	.box {
		background-color: var(--color, #ddd);
	}
</style>

これでboxの背景がredになる

kawarimidollkawarimidoll

Actions

要素レベルのライフサイクル
関数を<div use:myFunc>のように書ける
関数の第一引数は配置されたnode

ちょっとサンプルがでかいのでこの自習ノートに書けない

https://svelte.dev/tutorial/svelte/actions

関数の第二引数以降はディレクティブ部分に引数として書くことができる
以下はTippy.jsを使った例

App.svelte
<script>
	import tippy from 'tippy.js';
	let content = $state('Hello!');
	function tooltip(node, fn) {
		$effect(() => {
			const tooltip = tippy(node, fn());
			return tooltip.destroy;
		});
	}
</script>

<input bind:value={content} />
<button use:tooltip={() => ({content})}>Hover me</button>

<style>
/* 略 */
</style>

$effectの返り値は前述の通りcleanup関数

kawarimidollkawarimidoll

Transition 1

描画をいい感じに見せるTransitionが提供されている

以下は'svelte/transition'からインポートできるfadeの例
チェックボックスのオンオフで表示がゆっくり切り替わる

App.svelte
<script>
	import { fade } from 'svelte/transition';
	let visible = $state(true);
</script>

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

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

その他

  • transition:fly スライドしつつ表示される
  • transition:で指定すると表示・非表示ともに適用されるが、in:またはout:でどちらかに限定できる

transition関数は普通のjs関数なので自作できる こんな具合である

返り値のcssはトランジション中のスタイルの調整を行う

import { elasticOut } from 'svelte/easing';
function spin(node, { delay = 0, duration = 5000 }) {
	return {
		delay,
		duration,
		css: (t, u) => {
			// t = 0 → 1, u = 1 - t
			const eased = elasticOut(t);
			return `
			  transform: scale(${eased}) rotate(${eased * 1080}deg);
			`
		}
	};
}

nodeの情報の変更をしたい場合はtickを用いる
以下を見ると、transition関数のtypewriterは最初に一度呼ばれてconst text = node.textContentが保存され、それ以降はtickによりtextContentが変化してもtextの値に影響しないことが見てとれる

// 簡易版
function typewriter(node, { speed = 1 }) {
	const text = node.textContent;

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

Transition 2

以下のディレクティブでトランジションの開始・終了のフックを実行できる

onintrostart={onIntroStartFn}
onoutrostart={onOutroStartFn}
onintroend={onIntroEndFn}
onoutroend={onOutroEndFn}

リスト全体にトランジションを付ける場合は|globalを付加する

{#each items.slice(0, len) as item}
	<div transition:slide|global>{item}</div>
{/each}
# svelte3では `|local` という記法だったので古いバージョンを触る場合は注意

#{key var} ... {/key}ブロックでは変数varが変化した場合にブロック内を再描画し、トランジションも再実行される
値だけでなくブロックを丸ごと更新したい場合に便利

kawarimidollkawarimidoll

Advanced Reactivity

  • 通常のリアクティビティが不要な場合は$state.raw()を使うことができる たとえばリアルタイムで更新される要素など
  • 通常の値だけでなくJSのクラスのフィールドもreactiveにできる
    • getter / setterを自動で認識する
    • setterを使うと値のバリデーションに便利
  • JS標準のデータ構造をSvelteのreactiveで使いやすくなるようラップしたsvelte/reactivityというパッケージがあるのでDateやURLを使う場合にはこちらを参照するのが良い
kawarimidollkawarimidoll

Reusing content

#{snippet name()} ... {/snippet}を使用することで独立コンポーネントを定義せずとも要素を再利用できる

App.svelte
{#snippet greet()}
<div>Hello, world!</div>
{/snippet}

{@render greet()}
{@render greet()}

変数展開も可能

App.svelte
{#snippet greet(name)}
<div>Hello, {name}!</div>
{/snippet}

{@render greet("Alice")}
{@render greet("Bob")}

これまでの値のように別コンポーネントに渡すこともできる 以下の例だとあまり意味はないが…

App.svelte
<script>
    import { Sub } from "./Sub.svelte";
</script>
{#snippet greet(name)}
<div>Hello, {name}!</div>
{/snippet}
<Sub {greet} />
Sub.svelte
<script>
    const { greet } = $props();
</script>
{@render greet("Alice")}
{@render greet("Bob")}

また、コンポーネントのタグで囲むことで暗黙的にスニペットがpropsに渡される

App.svelte
<script>
    import { Sub } from "./Sub.svelte";
</script>
<Sub>
    {#snippet greet(name)}
    <div>Hello, {name}!</div>
    {/snippet}
</Sub>

さらに、#{snippet name()} ... {/snippet}を省略するとchildrenという名前の特別なスニペットとしてpropsに渡される この場合は変数を使うことはできない

App.svelte
<script>
    import { Sub } from "./Sub.svelte";
</script>
<Sub>
    <div>Hello, world!</div>
</Sub>
Sub.svelte
<script>
    const { children } = $props();
</script>
{@render children()}
kawarimidollkawarimidoll

Stores

値を管理する方法として有用…だったがSvelte 5では rune により必要性は減ったとのこと
SvelteKitでは使う可能性あるらしいけどいったん飛ばして良いかな…
チュートリアルで示されているのもStoreの使用法というよりはsvelte/motionの利用例ぽい

kawarimidollkawarimidoll

Advanced bindings

  • contenteditableを使える要素に対しては<div contenteditable bind:textContent={text}></div><div contenteditable bind:innerHTML={html}></div>が使用可能
  • {#each}内の要素に対してもbindを使える
  • <audio><video>の要素に対してもbindingがあるが専用APIなのでここでは省略
  • bind:clientWidth={w} bind:clientHeight={h}でブロック要素のサイズを受け渡せる
  • bind:this={var}でhtml要素自体のバインディングが可能 canvasくらいしか使い途がないのではないか
  • propsの値もバインディング可能 要するに子が親の値を更新できる
    • コンポーネント側でlet { var = $bindable('') } = $props()のように値を取り出す
    • 多用は避けるべき
kawarimidollkawarimidoll

Advanced transitions

'svelte/transition'に含まれているcrossfadeという関数で defer transitionを作成できる
crossfade[send, receive]の2つの関数を返し、これをin:receiveout:sendのトランジションに設定することで移動トランジションを表現できる
またトランジション本体以外の要素には'svelte/animate'flipanimate:flipというアトリビュートで設定することで移動が可能
これらはCSSとしてコンパイル(?)されるのでJSをブロックしないのも利点

kawarimidollkawarimidoll

Context API

'svelte'からインポートできるsetContext / getContextで値をやり取りできる
以下のkey / valueともに型の制限はないもよう

import { getContext, setContext } from 'svelte';
setContext(key, value);
getContext(key).value;

単純な親から子への値の受け渡しの範疇を超える場合はContextを介したほうが便利と思われる

kawarimidollkawarimidoll

Special elements

HTML body内に使われない要素にもイベントリスナや変数bindを設定できるよう、以下の特殊タグが提供されている

  • <svelte:window />
  • <svelte:document />
  • <svelte:body />
  • <svelte:head></svelte:head>

また、タグが動的に変更される場合は<svelte:element this="tag-name"></svelte:element>を使用できる
…と思ったが rune の世界ではこれは非推奨
https://svelte.dev/docs/svelte/compiler-warnings#svelte_component_deprecated

こっちにする

svelte-component
-<svelte:component this={item.condition ? Y : Z} />
+{@const Component = item.condition ? Y : Z}
+<Component />

Componentの定義はscriptタグに入れてもよい

svelte-component
<script>
	let condition = $state(false);
	const Component = $derived(condition ? Y : Z);
</script>

<Component />
kawarimidollkawarimidoll

script module

初期化のとき以外にJSを評価したい場合に<script module>を使えるらしい
非常に稀らしいのであんまり深入りしなくて良さそうかな…

kawarimidollkawarimidoll

↑↑↑ここまでSvelteチュートリアル↑↑↑


↓↓↓ここからSvelteKitチュートリアル↓↓↓

kawarimidollkawarimidoll

Introduction

たとえばこういうディレクトリ構成になる
ページファイルに+がついているのが特徴的

project
├ src
│ ├ app.html
│ └ routes
│   └ +page.svelte
├ static
│ └ shared.css
└ package.json
kawarimidollkawarimidoll

Routing

ファイルベースルーティングなのでディレクトリ構造がURLパス構造になる
なおURLのtrailing slashの有無は問わないもよう

File path URL
routes/+page.svelte /
routes/about/+page.svelte /about
routes/blog/[slug]/+page.svelte /blog/something

routes/+layout.svelteを作ると兄弟および子のファイルのページのレイアウトを定義できる
{@render children()}によって展開される

routes/+layout.svelte
<script>
	let { children } = $props();
</script>

<nav>
	navigation here
</nav>

<!-- pages are deployed here -->
{@render children()}

パスパラメータは+page.server.jsファイル内のload関数の引数として渡され、load関数の返り値が data propに保存される
'@sveltejs/kit'のerrorを使ってerror(404)のようにエラー返却を行うことができる

routes/posts/[slug]/+page.server.js
import { error } from '@sveltejs/kit'

const posts = someAPI();

export function load({ params }) {
	const post = posts.find(({ slug }) => slug === params.slug);
	return post ? { post } : error(404);
}
routes/posts/[slug]/+page.svelte
<script>
	let { data } = $props();
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>

ファイル名を+layout.server.jsにすると、同ディレクトリ以下のページが読み込まれる際にload()を実行する担当する この返り値も同様にdataオブジェクトから取得できる

kawarimidollkawarimidoll

Headers and cookies

load()関数にわたされる引数はsetHeadersという関数をフィールドに持ち、これを使うことでheaderの値を更新できる

+page.server.js
export function load({ setHeaders }) {
	setHeaders({
		'Content-Type': 'text/plain'
	});
}

ただし'Set-Cookie'ヘッダーは使用できず、その場合はcookies APIを使用しなければならない

+page.server.js
export function load({ cookies }) {
	const visited = cookies.get('visited');
	cookies.set('visited', 'true', { path: '/' });

	return {
		visited: visited === 'true'
	};
}

内部的には jshttp/cookie を使用しており、SvelteKitは自動的にクッキーをセキュアにするキーを追加する

`load()`の引数
+page.server.js
export function load(params) {
	console.dir(params);
}

{
  cookies: {
    get: [Function: get],
    getAll: [Function: getAll],
    set: [Function: set],
    delete: [Function: delete],
    serialize: [Function: serialize]
  },
  fetch: [Function: fetch],
  getClientAddress: [Function: getClientAddress],
  locals: {},
  params: {},
  platform: undefined,
  request: Request {
    [Symbol(realm)]: { settingsObject: [Object] },
    [Symbol(state)]: {
      method: 'GET',
      localURLsOnly: false,
      unsafeRequest: false,
      body: null,
      client: [Object],
      reservedClient: null,
      replacesClientId: '',
      window: 'client',
      keepalive: false,
      serviceWorkers: 'all',
      initiator: '',
      destination: '',
      priority: null,
      origin: 'client',
      policyContainer: 'client',
      referrer: 'client',
      referrerPolicy: '',
      mode: 'cors',
      useCORSPreflightFlag: false,
      credentials: 'same-origin',
      useCredentials: false,
      cache: 'default',
      redirect: 'follow',
      integrity: '',
      cryptoGraphicsNonceMetadata: '',
      parserMetadata: '',
      reloadNavigation: false,
      historyNavigation: false,
      userActivation: false,
      taintedOrigin: false,
      redirectCount: 0,
      responseTainting: 'basic',
      preventNoCacheCacheControlHeaderModification: false,
      done: false,
      timingAllowFailed: false,
      headersList: [HeadersList],
      urlList: [Array],
      url: [URL]
    },
    [Symbol(signal)]: AbortSignal {
    [Symbol(kEvents)]: SafeMap(0) [Map] {},
    [Symbol(events.maxEventTargetListeners)]: 10,
    [Symbol(events.maxEventTargetListenersWarned)]: false,
    [Symbol(kAborted)]: false,
    [Symbol(kReason)]: undefined,
    [Symbol(kComposite)]: false,
    [Symbol(realm)]: [Object]
    },
    [Symbol(headers)]: Headers {
    [Symbol(headers list)]: [HeadersList],
    [Symbol(guard)]: 'request',
    [Symbol(realm)]: [Object]
    }
  },
  route: { id: '/' },
  setHeaders: [Function: setHeaders],
  url: <ref *1> URL {
    searchParams: URLSearchParams {
      [Symbol(query)]: [Array],
      [Symbol(context)]: [Circular *1]
    },
    href: [Getter],
    pathname: [Getter],
    search: [Getter],
    toString: [Getter],
    toJSON: [Getter],
    [Symbol(context)]: URLContext {
      href: 'http://localhost:5173/?theme=light',
      protocol_end: 5,
      username_end: 7,
      host_start: 7,
      host_end: 16,
      pathname_start: 21,
      search_start: 22,
      hash_start: 4294967295,
      port: 5173,
      scheme_type: 0
    },
    [Symbol(nodejs.util.inspect.custom)]: [Function (anonymous)]
  },
  isDataRequest: false,
  isSubRequest: false,
  depends: [Function: depends],
  parent: [AsyncFunction: parent],
  untrack: [Function: untrack]
}
kawarimidollkawarimidoll

Shared modules

ディレクトリ構造が深くなると共通コードへの相対参照が難しくなる

project
├ src
│ ├ app.html
│ ├ lib
│ │ └ shared.js
│ └ routes
│   ├ a
│   │ └ deeply
│   │   └ nested
│   │     └ route
│   │       └ +page.svelte // ←ここからshared.jsを指すのが大変
│   └ +page.svelte
├ static
│ └ shared.css
└ package.json

src/lib/にあるコードはsrc配下のどこからでも$lib/...で参照できる

import { sharedFunc } from '$lib/shared.js';
kawarimidollkawarimidoll

Forms

+page.server.jsにform actionを書ける

+page.server.js
export function load({ cookies }) {
	// load
}

export const actions = {
	create: async ({ cookies, request }) => {
		// create record
	},
	delete: async ({ cookies, request }) => {
		// delete record
	}
};
// actionsが一つしかない場合は`default`という名前も使える

actionsの名前はformのaction属性で指定する(defaultの場合はaction不要)

+page.svelte
<form method="POST" action="?/create">
    <label>
        add a record:
        <input name="title" autocomplete="off" />
    </label>
</form>

server actionsには返り値を設定することができ…

+page.server.js
import { fail } from '@sveltejs/kit';
export const actions = {
	createRecord: async ({ cookies, request }) => {
		if (condition) {
			return { message: "Complete"}
		} else {
			return fail(422, { error: "Unprocessable Content"})
		}
	}
};

ページからはformpropsで取得できる

+page.svelte
<script>
	let { form } = $props();
</script>

{#if form?.error}
	<p class="error">{form.error}</p>
{:else if form?.message}
	<p>{form.message}</p>
{/if}

formタグを使っているのでJS無効環境でも動作するが、enhanceを加えるだけでJS有効環境に対応した描画改善を加えられる(都度ページ全体をリロードするのではなく必要な部分だけ再描画するようになり、トランジション等を加えることができる)

+page.svelte
<script>
	import { enhance } from '$app/forms';
	import { fly, slide } from 'svelte/transition';
</script>

<form method="POST" action="?/create" use:enhance>
	...
</form>

また、use:enhanceはフック関数を渡す事が可能
フォーム送信時に関数自体が呼ばれ、フォーム処理終了でresponseが帰ってきたときに返り値の関数が呼ばれる
以下はフォーム処理完了時にupdateを呼んで描画を更新している

+page.svelte
<form method="POST" action="?/create" use:enhance={() => {
	creating = true;
	return async ({ update }) => {
		await update();
		creating = false;
	};
}}>
	...
</form>

ほかにもcancel()などが使える 詳細はenhanceのドキュメント参照

kawarimidollkawarimidoll

API routes

前述の通りSvelteKitはファイルベースのルーティングだが、ページではないAPIのルートも定義できる
エンドポイントが/hogeの場合はsrc/routes/hoge/+server.jsになる

+server.js
export function GET() {
	const message = "hello API";
	return new Response(message, {
		headers: { 'Content-type': 'application/json' }
	});
}

返り値はResponseである必要があるが、json値を返す場合が多いのでSvelteKitがラッパーを提供している

+server.js
+import { json } from '@sveltejs/kit';
 export function GET() {
 	const message = "hello API";
-	return new Response(message, {
-		headers: { 'Content-type': 'application/json' }
-	});
+	return json(message);
 }

POSTも使えるがSvelteKitとしてはForm Actionの使用を推奨(JS無効環境でも動作するため)
とはいえ同様にほかのメソッドも対応できる

+server.js
export function GET() {
	return new Response(...);
}
export function POST() {
	return new Response(...);
}
export function PUT() {
	return new Response(...);
}
export function DELETE() {
	return new Response(...);
}
kawarimidollkawarimidoll

Stores

前述の通りSvelte 5では rune により必要性は減っており自分から値を保存することは特に考えなくて良い

SvelteKitでは読み取り専用のstoresが用意されている
'$app/stores'からpage / navigating / updatedの3種を使用することができる

中でもpageurlをはじめとしたよく使う情報が自動的に保存される
マークアップでアクセスするときには$を前置する必要がある

たとえば以下のようにすると現在のページに応じてaria-currentを付与できる

+layout.svelte
<script>
	import { page } from '$app/stores';
</script>

<nav>
	<a href="/" aria-current={$page.url.pathname === '/'}>home</a>
	<a href="/about" aria-current={$page.url.pathname === '/about'}>about</a>
</nav>

navigatingはページ遷移(通常のリンクのほか、ブラウザバックなども含む)が発生した場合に遷移前後のページや遷移の種別が含まれる 普通にページを表示している間はnull
updatedはページを開いている間に更新バージョンがデプロイされたときにtrueになる真偽値 プロダクションでのみ動作する あまり意識しなくて良さそう

kawarimidollkawarimidoll

Errors and redirects

想定されているエラーにはerror関数を使う ここに入れたメッセージはページ上にも表示される

+page.server.js
import { error } from '@sveltejs/kit';

export function load() {
	error(420, 'Enhance your calm');
}

対して通常のエラーやthrowで作られたエラーは単に500 Internal Errorと表示される
内部情報を露出させないため

+page.server.js
export function load() {
	throw new Error('Waaaaa!');
}

エラーページの内容はsrc/routes/+error.svelteで調整可能 これはsrc/routes/foo/bar/+error.svelteのように階層に入れることで特定のパスで生じるエラー専用のエラーページを作成できる

また、src/routes/+layout.server.jsのような上位階層でエラーが起きてルートのレイアウトすらレンダリングできない場合は標準のエラーページ(ステータスコードとメッセージが表示される)が表示される
これはsrc/error.htmlによって調整でき、エラー情報は%sveltekit.status%%sveltekit.error.message%で取得できる
このへんの取得はsrc/app.htmlと同じやね

リダイレクトは'@sveltejs/kit'からインポートできるredirect関数を使う

+page.server.js
import { redirect } from '@sveltejs/kit';
export function load() {
	redirect(307, path);
}

SvelteKitの推奨値は以下 302じゃないんだ?

  • 303 フォームアクションの成功によるリダイレクト
  • 307 一時的リダイレクト
  • 308 恒久的リダイレクト

ほーん 307は302と、308は301と同じような感じみたいだ

https://developer.mozilla.org/ja/docs/Web/HTTP/Status#リダイレクトメッセージ

kawarimidollkawarimidoll

Hooks

SvelteKitにはいくつかのhookが用意されている

handle

もっとも基本的なhook
デフォルトはこう

handle
export async function handle({ event, resolve }) {
	return await resolve(event);
}

ようするにhandlerなので、resolveに引数を渡すか自分で定義するかしてResponseを返せば良い

以下は改造例

src/hooks.server.js
export async function handle({ event, resolve }) {
	if (event.url.pathname === '/ping') {
		return new Response('pong');
	}
	return await resolve(event, {
		transformPageChunk:({ html })=>html.replace(
			'<body',
			'<body style="color: hotpink"'
		)
	});
}

event.locals

event.localsオブジェクトには任意の値を保存できる
hooksから各ページのサーバーに値を受け渡すのに便利である

たとえばlocalsに適当な値を入れ

src/hooks.server.js
export async function handle({ event, resolve }) {
	event.locals.answer = 42;
	return await resolve(event);
}

↓それをserverでdataオブジェクトに仕込み

src/routes/+page.server.js
export function load(event) {
	return {
		message: `the answer is ${event.locals.answer}`
	};
}

↓各ページで参照できる

src/routes/+page.svelte
<script>
	let { data } = $props();
</script>

<h1>{data.message}</h1>
eventの中身 上の方で出てきた`load()`の引数と同じぽい

こちらでは日本語の説明

  • cookies 前述のSvelte Cookies API
  • fetch Web標準のFetch APIに少し機能追加されたもの
  • getClientAddress() クライアントIP取得関数
  • isDataRequest 真偽値 クライアントがデータを問い合わせている間はtrue
  • locals 任意のデータをおけるオブジェクト
  • params routesパラメータ
  • request Requestオブジェクト
  • route 現在のルートを表すオブジェクト { id: '/' }みたいな値が入る
  • setHeaders() 前述のsetHeaders関数
  • url 現在のリクエストのURLオブジェクト

handleFetch

上記のeventに含まれるevent.fetchはcookie/authヘッダーをいい感じに処理してくれたり相対パスを使えたりサーバー上で動くときに不要なHTTPリクエストをカットしてくれたりとSvelteKit内で使うのに便利な特徴を備えている

この挙動は変更できる 以下がデフォルト

src/hooks.server.js
// ここではeventは使われていない
export async function handleFetch({ event, request, fetch }) {
	return await fetch(request);
}

例えば特定のパスのときに別のパスにリダイレクトするなど

src/hooks.server.js
export async function handleFetch({ event, request, fetch }) {
	const url = new URL(request.url);
	if (url.pathname === '/a') {
		return await fetch('/b')
	}
	return await fetch(request);
}

handleError

前述の'@sveltejs/kit'error ではない エラー(要するに管理されていない一般のエラー)の挙動はhandleErrorで調整できる
ここの返り値は+error.svelte内で参照できる

hooks.server.js
export function handleError({ event, error }) {
	console.error(error.stack);
	return {
		message: 'handleError',
		code: '999'
	};
}
src/+error.svelte
<script>
	import { page } from '$app/stores';
</script>
<h1>Error {$page.status}</h1>
<div>{$page.error.message}</div>
<div>{$page.error.code}</div>

これはユーザー向けの返り値を調整するというよりはエラー情報を開発者に通知するなどの用途で有用そう

kawarimidollkawarimidoll

Page options

\+(page|layout)(\.server)?\.jsファイルではload関数だけでなくさまざまなオプションをexportできる

ssr

サーバーでHTMLを構築する デフォルトはtrue
このほうがクローラを含む非JS環境などでも動作が保証できる
基本的にはSSRで動作するようにアプリケーションを作るべきだが、ブラウザスクリーンサイズなど、一部のクライアントサイドでしか動かないAPIを使用したい場合にはfalseにできる

+page.server.js
export const ssr = false;

+layout.server.jsの中でssrfalseに設定すると、アプリ全体がSPAとなる

csr

ページをインタラクティブにする デフォルトはtrue
特にインタラクティブな要素がなくても、SvelteKitはページ遷移時にパーシャルロードを行うのでJSを使っている
これをfalseにするとクライアントに完全にJSが送られなくなる

prerender

ページをビルド時に生成したものに固定する デフォルトはfalse
何らかの理由で完全に静的ページにしたいときは有効にする
ページ単体の処理は早くなるがビルドに時間がかかるほか、再デプロイ以外にページを更新する方法がない

+layout.server.jsの中でprerendertrueに設定すると、SvelteKitはSSGとなる

trailingSlash

trailing slashの有無を統一する

  • 'never' trailing slashのあるパスを指定されても、ないパスへリダイレクトする デフォルト値
  • 'always' trailing slashのないパスを指定されても、あるパスへリダイレクトする
  • 'ignore' リダイレクトを行わない 非推奨
+page.server.js
export const trailingSlash = 'always';

この値は静的ファイルのビルドパスに関わり、alwaysの場合はalways/index.htmlのようにパス名/index.htmlが、neverの場合はnever.htmlのようにパス名.htmlが生成される

kawarimidollkawarimidoll

preloading

ページのロードに時間がかかる場合に備え、ユーザーが実際に遷移する前に遷移先のページを読み込むことができる

+page.svelte
<a href="dst" data-sveltekit-preload-data>link_to</a>

なおこれは個別のリンクのみならずリンクの親要素にもつけることができ、かつSvelteKitの標準のボイラープレートがbodyタグに設定している ということで実際はあまり気にしなくてもいい

app.html
<body data-sveltekit-preload-data="hover">
	<div style="display: contents">%sveltekit.body%</div>
</body>

ページ自体ではなく遷移先で必要なJSを読み込むdata-sveltekit-preload-codeというのもある

reloading

SvelteKitは効率化のためにパーシャルロードを行うが、遷移時にページ全体をリロードしたい場合、個別のリンクタグまたは親要素にdata-sveltekit-reloadを付加する

その他

チュートリアルでは上記のオプションしか紹介しないがほかにもあるとのこと

https://svelte.dev/docs/kit/link-options

  • data-sveltekit-replacestate
  • data-sveltekit-keepfocus
  • data-sveltekit-noscroll
kawarimidollkawarimidoll

Advanced routing

Optional params

すでにroutingの説明で[page]/+page.svelteのようにブラケットを使うことでダイナミックルーティングを定義できることを示した
これを[[lang]]/+page.svelteと二重にすることでオプショナルなパスにすることができる
たとえばexample.com/page/1でページ1を英語で、example.com/fr/page/1でページ1をフランス語で表示する…というような場合に便利である

Rest params

パスを[...path]/+page.svelteと書くことで任意長のパスにマッチさせることができる
たとえばexample.com/pathにもexample.com/nested/linkにもexample.com/super/deep/nested/long/urlにもマッチする
なおネスト部が末尾である必要はない
[...catchall]/に404などエラーハンドリングするルートを作っておくと便利である

Param matchers

ダイナミックルーティングはするが、ルート文字列を特定のもののみに限りたい場合がある
例えばcolors/fe8d0aは良いが、colors/anythingは拒否したい場合など
このような場合、src/paramsにmatcherを定義することができる
ここに定義したjsファイルではmatch関数をexportする必要がある これはルートを受け取って真偽値を返す関数

src/params/hex.js
export function match(value) {
	return /^[0-9a-fA-F]{6}$/.test(value);
}

で、ページファイルのルーティングをsrc/routes/colors/[color]からsrc/routes/colors/[color=hex]に変更する イコールの後の文字列はmatcherファイルの名前
これによりcolors/???のマッチには上記のhex.jsmatch()が使われ、マッチする場合はsrc/routes/colors/[color=hex]/+page.svelteが表示され、マッチしない場合は404となる

Route groups

parenthesで囲んだディレクトリを使うことで実際のルートには影響せずにグルーピングを行うことができる
例えば以下のようなファイル構成を考える

project
├ src
│ ├ app.html
│ └ routes
│   ├ about
│   │ └ +page.svelte
│   ├ account
│   │ └ +page.svelte
│   ├ app
│   │ └ +page.svelte
│   ├ +layout.svelte
│   └ +page.svelte
├ static
│ └ shared.css
└ package.json

ここで、aboutはパブリックにアクセスできるが、accountappはログインしないと見られないようにしたい場合、(authed)ディレクトリにこれらを移動し、ここの+layout.svelteにログインメニューを、+layout.server.jsにログイン確認処理を入れることができる

project
├ src
│ ├ app.html
│ └ routes
│   ├ about
│   │ └ +page.svelte
│   ├ (authed)
│   │ ├ account
│   │ │ └ +page.svelte
│   │ ├ app
│   │ │ └ +page.svelte
│   │ ├ +layout.svelte
│   │ └ +layout.server.js
│   ├ +layout.svelte
│   └ +page.svelte
├ static
│ └ shared.css
└ package.json

Layout除去

ページは上位のレイアウトを継承するので、例えばsrc/routes/a/b/c/+page.svelteを表示する場合、

  • src/routes/+layout.svelte
  • src/routes/a/+layout.svelte
  • src/routes/a/b/+layout.svelte
  • src/routes/a/b/c/+layout.svelte

が読み込まれる

これを調整したい場合は@を使用し、例えばsrc/routes/a/b/c/+page@b.svelteとするとb/+layout.svelteまでの読み込みで止まり、cのものが読み込まれなくなる
同様に+page@a.svelteならbcの読み込みを、+page@.svelteならabcの読み込みをスキップできる
ただしルートのレイアウトは必ず使用され、スキップすることはできない

kawarimidollkawarimidoll

Advanced loading 1

ページが外部APIから値を読み込む場合や、+page.server.jsの値がシリアライズできない場合などには+page.jsを使うことができる
+page.server.jsをServer load function、+page.jsをUniversal load functionという
チュートリアルの例では嬉しいポイントがわからないので将来的にドキュメントを見て勉強したい

https://svelte.dev/docs/kit/load#Universal-vs-server

これらは併用でき、Universal load functionのほうでServer load functionの返り値を読み込むことができる

+page.server.js
export async function load() {
	return {
		message: 'this data came from the server-load-function',
	};
}
+page.js
export async function load({ data }) {
	return {
		message: data.message + ' and the universal-load-function',
	};
}

値はマージされず、+page.svelteに渡されるのは常にUniversal load functionの返り値

+page.svelte
<script>
	let { data } = $props();
</script>

<div>{data.message}</div>

また、+page.svelte+layout.svelteが親のSever load functionの返り値にアクセスできるように、Universal load functionも親のSever load functionの返り値にアクセスできる
これにはload関数の引数の一つであるasync parent関数を使う

src/routes/+layout.server.js
export async function load() {
	return { a: 1 };
}
src/routes/sum/+layout.js
export async function load({ parent }) {
	const { a }= await parent()
	return { b: a + 1 };
}
src/routes/sum/+page.js
export async function load({ parent }) {
	const { a, b } = await parent();
	return { c: a + b };
}

すごいスパゲッティになりそう
あまりこれに頼ったアーキテクチャにはしたくない

kawarimidollkawarimidoll

Advanced loading 2

load関数はページ遷移時に読み込まれるが、兄弟同士のページを移動した際、親階層のloadが再実行されないことがある(この問題認識は合っているのかびみょう チュートリアルの例ではURL依存関係の問題か?)
SvelteKitにload再実行を強制するためには、'$app/navigation';からインポートできるinvalidate()にパスを渡す このパスに更新が生じたとしてリロードが行われる

なおinvalidate()にはパス文字列やURLだけでなくURLを受け取って真偽値を返す関数を渡しても良い 詳しくはドキュメント参照

https://svelte.dev/docs/kit/$app-navigation#invalidate

invalidateで使うキーはURLとして適正なら良く、depends(url)関数を使ってdepends('date:now')のようなキーを定義しても良い この場合はinvalidate('date:now')でリロードが行われる

https://svelte.dev/docs/kit/load#Rerunning-load-functions-Manual-invalidation

なお、パスとかキーとか無視して全てリロードするinvalidateAll()という関数もある
影響範囲が広そう

kawarimidollkawarimidoll

Environment variables

.envに環境変数を設定してimport { ENV_NAME } from '$env/static/private';で読み込むことができる

.env.local.env.developなども利用可能これはVite準拠

https://vite.dev/guide/env-and-mode.html#env-files

これはサーバーサイドでしか使用できないほか、環境変数を利用した分岐はビルド時に削除されて最適化される

ビルド後に動的に読み込みたい場合はimport { env } from '$env/dynamic/private';のようにする
この場合はenv.ENV_NAMEのように使用する

.envファイルに含まれる値でも名前の頭にPUBLIC_が付いているものはpublicとして扱うことができ、'$env/static/public' '$env/dynamic/public'から読み込む

静的・プライベートじゃない環境変数を.envに入れるか?と疑問があるのでstatic/privateだけ覚えていれば良いんじゃないかな

このスクラップは3日前にクローズされました