Closed59

Svelte 公式チュートリアル Part 3: Basic SvelteKit をやっていく

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Introduction / What is SvelteKit?

https://learn.svelte.dev/tutorial/introducing-sveltekit

Svelte がコンポーネントフレームワークであるのに対し、SvelteKit はアプリケーションフレームワークであり、次のような機能を提供する。

  • ルーティング
  • サーバーサイドレンダリング
  • データ取得
  • サービスワーカー
  • TypeScript との統合
  • 事前レンダリング
  • シングルページアプリケーション
  • ライブラリのパッケージ化
  • 本番環境に最適化されたビルド
  • 様々なプロバイダーへのデプロイ

SvelteKit の特徴

  • デフォルトではサーバーサイドでレンダリングする(初回ロードや SEO のため)。
  • その後はクライアントサイドでのナビゲーションに移行する(ユーザー体験のため)。

プロジェクト構造

  • package.json:依存関係やスクリプトが含まれる。
  • svelte.config.js:プロジェクトの設定が含まれる。
  • vite.config.js:Vite の設定が含まれる、SvelteKit は Vite を使っている。
  • src:アプリのソースコードを保存する。
  • src/app.html:ページのテンプレート、%sveltekit.head%sveltekit.body が置き換えられる。
  • src/routes:アプリのルートに関するディレクトリ。
  • static:Favicon や robot.txt などが含まれる、ビルド時に単純にコピーされるのかな?

https://kit.svelte.dev/docs/configuration

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Routing / Pages

https://learn.svelte.dev/tutorial/pages

src/routes/about/+page.svelte
<nav>
	<a href="/">home</a>
	<a href="/about">about</a>
</nav>

<h1>about</h1>
<p>this is the about page.</p>

SvelteKit ではファイルシステムベースのルーティングを使っている。

ファイル名には +page.svelte を使用する。

例えば /about ページを作りたい場合には src/routes/about ディレクトリを作成し、このディレクトリ内に +page.svelte ファイルを作成する。

初回ロードはサーバー側で描画されるがそれ以降のページ移動ではページは更新されず、ページの中身が更新される。

これにより、高速な初回ページ読み込みと高速なページ移動の両方が実現される。

この振る舞いがデフォルトだが、ページオプション設定を行うことで変更できる。

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

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Routing / Layouts

https://learn.svelte.dev/tutorial/layouts

src/routes/+layout.svelte
<nav>
	<a href="/">home</a>
	<a href="/about">about</a>
</nav>

<slot />

各ページに共通するヘッダーやフッターなどは +layout.svelte ファイルにまとめることができる。

+layout.svelte は同じディレクトリと下位ディレクトリのページに適用される。

レイアウトはネストすることができる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Routing / Route parameters

https://learn.svelte.dev/tutorial/params

src/routes/blog/[slug]/+page.svelte
<h1>blog post</h1>

[slug] のようなディレクトリ名を作成すると動的なルートを作成できる。

この点は Next.js と似ているので覚えやすい。

なんと [bar]x[baz] のようなルートも作成できる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Loading data / Page data

https://learn.svelte.dev/tutorial/page-data

src/routes/blog/+page.server.js
import { posts } from './data.js';

export function load() {
	return {
		summaries: posts.map((post) => ({
			slug: post.slug,
			title: post.title
		}))
	};
}
src/routes/blog/+page.svelte
<script>
	export let data;
</script>

<h1>blog</h1>

<ul>
	{#each data.summaries as { slug, title }}
		<li><a href="/blog/{slug}">{title}</a></li>
	{/each}
</ul>
src/routes/blog/[slug]/+page.server.js
import { error } from '@sveltejs/kit';
import { posts } from '../data.js';

export function load({ params }) {
	const post = posts.find((post) => post.slug === params.slug);

	if (!post) throw error(404);
	
	return {
		post
	};
}
src/routes/blog/[slug]/+page.svelte
<script>
	export let data;
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
  • SvelteKit のコア機能はルーティング・ローディング・レンダリングの 3 つ。
  • +page.svelte と同じ階層に +page.server.js を作成し、load() 関数をエクスポートすることでローディングを行うことができる。
  • +page.server.js は常にサーバーで実行され、+page.js はサーバーまたはブラウザのいずれかで実行される。
  • コンポーネントでローディングされたデータを受け取るには script タグ内で export let data; と受け渡し用の変数を用意する。
  • エラーページを表示するには @sveltejs/kit から error() 関数をインポートして使用する。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Loading data / Layout data

https://learn.svelte.dev/tutorial/layout-data

src/routes/blog/[slug]/+layout.svelte
<script>
	export let data;
</script>

<div class="layout">
	<main>
		<slot />
	</main>

	<aside>
		<h2>More posts</h2>
		<ul>
			{#each data.summaries as { slug, title }}
				<li>
					<a href="/blog/{slug}">{title}</a>
				</li>
			{/each}
		</ul>
	</aside>
</div>

<style>
	@media (min-width: 640px) {
		.layout {
			display: grid;
			gap: 2em;
			grid-template-columns: 1fr 16em;
		}
	}
</style>

+layout.server.js ファイルを作成することで下位のルートに共通するデータを読み込むことができる。

レイアウトのデータに加えてさらにページ固有のデータローディングが必要な場合はどうすれば良いのかな?

ドキュメントを読もう。

https://kit.svelte.dev/docs/load

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Headers and cookies / Setting headers

https://learn.svelte.dev/tutorial/headers

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

load() 関数で setHeaders() 関数を使うことで HTTP ヘッダーを設定できる。

まだ学んでいないがフォームアクション、フック、API ルートでも同じことができるようだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Headers and cookies / Reading and writing cookies

https://learn.svelte.dev/tutorial/cookies

src/routes/+page.server.js
export function load({ cookies }) {
	const visited = cookies.get('visited');

	cookies.set('visited', 'true', { path: '/' })

	return {
		visited
	};
}

setHeaders() 関数では Set-Cookie ヘッダーを設定できないのでその代わりに cookies API を使用する。

クッキーの読み込みには cookies.get() メソッドを使い、書き込みには cookies.set() メソッドを使う。

クッキーを書き込む時には path オプションを明示的に指定することが推奨される。

なぜなら現在のパスの 1 階層上がデフォルトの path になるため。

cookies.set() 関数を実行すると Set-Cookie ヘッダが書き込まれるが、それに加えて内部のクッキーのマップが書き換えられる。

したがって cookies.set() 関数を実行した後に cookies.get() を実行すると、cookies.set() 関数で設定された値が読み込まれる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Shared modules / The $lib alias

https://learn.svelte.dev/tutorial/lib

src/routes/a/deeply/nested/route/+page.svelte
<script>
	import { message } from '$lib/message.js';
</script>

<h1>a deeply nested route</h1>
<p>{message}</p>
src/routes/+page.svelte
<script>
	import { message } from '$lib/message.js';
</script>

<h1>home</h1>
<p>{message}</p>

src/routes ディレクトリ以下にモジュールやコンポーネントを置くこともできるが、共通に使用されるものを置くには src/lib ディレクトリが便利。

相対パスでインポートする代わりに $lib のエイリアスでインポートすることができる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Forms / The form element

https://learn.svelte.dev/tutorial/the-form-element

+page.svelte(追加分)
	<form method="POST">
		<label>
			add a todo:
			<input
				name="description"
				autocomplete="off"
			/>
		</label>
	</form>
src/routes/+page.server.js(追加分)
export const actions = {
	default: async ({ cookies, request }) => {
		const data = await request.formData();
		db.createTodo(cookies.get('userid'), data.get('description'));
	}
};

+page.server.js で actions オブジェクトをエクスポートすることでサーバー側でフォームから送信されたデータを処理することができる。

fetch() 関数などを一切呼び出さずにサーバーとクライアントの間でデータをやり取りできる。

仮に JavaScript が無効になっていても動作するので堅牢性も高い。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Forms / Named form actions

https://learn.svelte.dev/tutorial/named-form-actions

src/routes/+page.server.js(抜粋)
export const actions = {
	create: async ({ cookies, request }) => {
		const data = await request.formData();
		db.createTodo(cookies.get('userid'), data.get('description'));
	},

	delete: async ({ cookies, request }) => {
		const data = await request.formData();
		db.deleteTodo(cookies.get('userid'), data.get('id'));
	}
};
src/routes/+page.svelte(抜粋)
	<form method="POST" action="?/create">
		<label>
			add a todo:
			<input
				name="description"
				autocomplete="off"
			/>
		</label>
	</form>
src/routes/+page.svelte(抜粋)
	<ul class="todos">
		{#each data.todos as todo (todo.id)}
			<li>
				<form method="POST" action="?/delete">
					<input type="hidden" name="id" value={todo.id} />
					<span>{todo.description}</span>
					<button aria-label="Mark as complete" />
				</form>
			</li>
		{/each}
	</ul>

actions のフィールド名に default 以外を指定することで名前付きのアクションを作成できる。

フォーム側では action="?/create" のようにアクションを指定する。

名前付きアクションを使う場合はデフォルトアクションは使用できない。

他のページのアクションも実行することができる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Forms / Validation

https://learn.svelte.dev/tutorial/form-validation

src/routes/+page.svelte(style 割愛)
<script>
	export let data;
	export let form;
</script>

<div class="centered">
	<h1>todos</h1>

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

	<form method="POST" action="?/create">
		<label>
			add a todo:
			<input
				name="description"
				value={form?.description ?? ''}
				autocomplete="off"
				required
			/>
		</label>
	</form>

	<ul class="todos">
		{#each data.todos as todo (todo.id)}
			<li>
				<form method="POST" action="?/delete">
					<input type="hidden" name="id" value={todo.id} />
					<span>{todo.description}</span>
					<button aria-label="Mark as complete" />
				</form>
			</li>
		{/each}
	</ul>
</div>
src/lib/server/database.js(抜粋)
export function createTodo(userid, description) {
	if (description === '') {
		throw new Error('todo must have a description');
	}

	const todos = db.get(userid);

	if (todos.find((todo) => todo.description === description)) {
		throw new Error('todos must be unique');
	}
	
	todos.push({
		id: crypto.randomUUID(),
		description,
		done: false
	});
}
src/routes/+page.server.js(抜粋)
export const actions = {
	create: async ({ cookies, request }) => {
		const data = await request.formData();

		try {
			db.createTodo(cookies.get('userid'), data.get('description'));
		} catch (error) {
			return fail(422, {
				description: data.get('description'),
				error: error.message
			});
		}
	},

	delete: async ({ cookies, request }) => {
		const data = await request.formData();
		db.deleteTodo(cookies.get('userid'), data.get('id'));
	}
};

fail() 関数を使うことでエラーコードと共にサーバーのアクションからデータを返却できる。

クライアント側では export let form; と書いてデータにアクセスできる。

アクションから普通にデータを return しても同じことができる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Forms / Progressive enhancement

https://learn.svelte.dev/tutorial/progressive-enhancement

src/routes/+page.svelte(style 割愛)
<script>
	import { fly, slide } from 'svelte/transition';
	import { enhance } from '$app/forms';

	export let data;
	export let form;
</script>

<div class="centered">
	<h1>todos</h1>

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

	<form method="POST" action="?/create" use:enhance>
		<label>
			add a todo:
			<input
				name="description"
				value={form?.description ?? ''}
				autocomplete="off"
				required
			/>
		</label>
	</form>

	<ul class="todos">
		{#each data.todos as todo (todo.id)}
			<li in:fly={{ y: 20 }} out:slide>
				<form method="POST" action="?/delete" use:enhance>
					<input type="hidden" name="id" value={todo.id} />
					<span>{todo.description}</span>
					<button aria-label="Mark as complete" />
				</form>
			</li>
		{/each}
	</ul>
</div>

form に特に何も指定しなければ送信時にページ再読み込みが行われる。

use:enhance を指定することで JavaScript が実行されるようになる。

そのためには import { enhance } from '$app/forms'; が必要になる。

エンハンスメントを有効にすると遷移を行えるようになる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Forms / Customizing use:enhance

https://learn.svelte.dev/tutorial/customizing-use-enhance

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

	export let data;
	export let form;

	let creating = false;
	let deleting = [];
</script>

<div class="centered">
	<h1>todos</h1>

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

	<form
		method="POST"
		action="?/create"
		use:enhance={() => {
			creating = true;

			return async ({ update }) => {
				await update();
				creating = false;
			};
		}}
	>
		<label>
			add a todo:
			<input
				disabled={creating}
				name="description"
				value={form?.description ?? ''}
				autocomplete="off"
				required
			/>
		</label>
	</form>

	<ul class="todos">
		{#each data.todos.filter((todo) => !deleting.includes(todo.id)) as todo (todo.id)}
			<li in:fly={{ y: 20 }} out:slide>
				<form
					method="POST"
					action="?/delete"
					use:enhance={() => {
						deleting = [...deleting, todo.id];
						return async ({ update }) => {
							await update();
							deleting = deleting.filter((id) => id !== todo.id);
						};
					}}
				>
					<input type="hidden" name="id" value={todo.id} />
					<span>{todo.description}</span>
					<button aria-label="Mark as complete" />
				</form>
			</li>
		{/each}
	</ul>

	{#if creating}
		<span class="saving">saving...</span>
	{/if}
</div>

use:enhance にコールバックを指定することでフォーム送信時の動作をカスタマイズできる。

コールバックはフォーム送信時(正確にはフォーム送信の直前)に呼び出されるようだ。

コールバックは戻り値として非同期関数を返すことができる。

これはおそらくサーバー側でフォームが処理された後に呼び出される。

update() 関数は data や form を更新するために呼び出す必要があるのだろう。

詳しくはドキュメントを参照。

https://kit.svelte.jp/docs/form-actions#progressive-enhancement-customising-use-enhance

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / API routes / GET handlers

https://learn.svelte.dev/tutorial/get-handlers

src/routes/roll/+server.js
import { json } from '@sveltejs/kit';

export function GET() {
	const number = Math.floor(Math.random() * 6) + 1;

	return json(number);
}

routes ディレクトリの子孫として +server.js ファイルを作成することで API ルートを設けることができる。

+server.js では GET や POST など HTTP メソッドと同じ名前の関数をエクスポートする。

これらの関数では Response オブジェクトをリターンする。

レスポンスを JSON 形式で返す場合は import { json } from '@sveltejs/kit'; を利用できる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / API routes / POST handlers

https://learn.svelte.dev/tutorial/post-handlers

src/routes/+page.svelte(style 割愛)
<script>
	export let data;
</script>

<div class="centered">
	<h1>todos</h1>

	<label>
		add a todo:
		<input
			type="text"
			autocomplete="off"
			on:keydown={async (e) => {
				if (e.key === 'Enter') {
					const input = e.currentTarget;
					const description = input.value;

					const response = await fetch('/todo', {
						method: 'POST',
						body: JSON.stringify({ description }),
						headers: {
							'Content-Type': 'application/json'
						}
					});

					const { id } = await response.json();

					data.todos = [...data.todos, {
						id,
						description
					}];

					input.value = '';
				}
			}}
		/>
	</label>

	<ul class="todos">
		{#each data.todos as todo (todo.id)}
			<li>
				<label>
					<input
						type="checkbox"
						checked={todo.done}
						on:change={async (e) => {
							const done = e.currentTarget.checked;

							// TODO handle change
						}}
					/>
					<span>{todo.description}</span>
					<button
						aria-label="Mark as complete"
						on:click={async (e) => {
							// TODO handle delete
						}}
					/>
				</label>
			</li>
		{/each}
	</ul>
</div>
src/routes/todo/+server.js
import { json } from '@sveltejs/kit';
import * as database from '$lib/server/database.js';

export async function POST({ request, cookies }) {
	const { description } = await request.json();

	const userid = cookies.get('userid');
	const { id } = await database.createTodo({ userid, description });

	return json({ id }, { status: 201 });
}

POST や PUT ではリクエストのヘッダーや本文に含まれるデータを取得できる。

取得の仕方は +page.server.js の load() 関数やフォームアクションと同じ。

例えば POST メソッドでリクエストの本文とクッキーにアクセスする場合は export async function POST({ request, cookies }) { ... } のように書く。

可能なケースではフォームアクションを使った方がコーディング量も少なく、JavaScript が無くても動くので堅牢性が高い。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / API routes / Other handlers

https://learn.svelte.dev/tutorial/other-handlers

src/routes/todo/[id]/+server.js
import * as database from '$lib/server/database.js';

export async function PUT({ params, request, cookies }) {
	const { done } = await request.json();
	const userid = cookies.get('userid');

	await database.toggleTodo({ userid, id: params.id, done });
	return new Response(null, { status: 204 });
}

export async function DELETE({ params, cookies }) {
	const userid = cookies.get('userid');

	await database.deleteTodo({ userid, id: params.id });
	return new Response(null, { status: 204 });
}
src/routes/+page.svelte(style 割愛)
<script>
	export let data;
</script>

<div class="centered">
	<h1>todos</h1>

	<label>
		add a todo:
		<input
			type="text"
			autocomplete="off"
			on:keydown={async (e) => {
				if (e.key === 'Enter') {
					const input = e.currentTarget;
					const description = input.value;
					
					const response = await fetch('/todo', {
						method: 'POST',
						body: JSON.stringify({ description }),
						headers: {
							'Content-Type': 'application/json'
						}
					});

					const { id } = await response.json();

					data.todos = [...data.todos, {
						id,
						description
					}];

					input.value = '';
				}
			}}
		/>
	</label>

	<ul class="todos">
		{#each data.todos as todo (todo.id)}
			<li>
				<label>
					<input
						type="checkbox"
						checked={todo.done}
						on:change={async (e) => {
							const done = e.currentTarget.checked;

							await fetch(`/todo/${todo.id}`, {
								method: 'PUT',
								body: JSON.stringify({ done }),
								headers: {
									'Content-Type': 'application/json'
								}
							});
						}}
					/>
					<span>{todo.description}</span>
					<button
						aria-label="Mark as complete"
						on:click={async (e) => {
							await fetch(`/todo/${todo.id}`, {
								method: 'DELETE'
							});

							data.todos = data.todos.filter((t) => t !== todo);
						}}
					/>
				</label>
			</li>
		{/each}
	</ul>
</div>

{ params } 引数を使ってパスパラメーターを取得できる。

204 No Content レスポンスを返すときは return new Response(null, { status: 204 }); とする。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Stores / page

https://learn.svelte.dev/tutorial/page-store

src/routes/+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>

<slot />

SvelteKit では page / navigating / updated の 3 つの読み込み専用ストアが提供される。

これらは $app/stores からインポートできる。

page ストアでは現在のページの URL を示す url や パラメーターを示すparams などにアクセスできる。

通常のストアと同様にストアの値にアクセスするには $ を前置する。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Stores / navigating

https://learn.svelte.dev/tutorial/navigating-store

src/routes/+layout.svelte
<script>
	import { page, navigating } 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>

	{#if $navigating}
		navigating to {$navigating.to.url.pathname}
	{/if}
</nav>

<slot />

navigating はいわゆるページ移動を表現するストアのようだ。

普段は null だがリンクなどでページ移動が発生する時にオブジェクトになる。

主なプロパティは from / to / type で from と to は始点と終点のページの URL などを示し、type はリンクなどのページ移動の種類を示す。

詳しいドキュメントは下記ページ。

https://kit.svelte.dev/docs/types#public-types-navigation

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Stores / updated

https://learn.svelte.dev/tutorial/updated-store

src/routes/+layout.svelte
<script>
	import { page, navigating, updated } 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>

	{#if $navigating}
		navigating to {$navigating.to.url.pathname}
	{/if}
</nav>

<slot />

{#if $updated}
	<p class="toast">
		A new version of the app is available

		<button on:click={() => location.reload()}>
			reload the page
		</button>
	</p>
{/if}

updated ストアはページが表示されてからその時点までに新しいバージョンのアプリがデプロイされたかを示す真偽値が格納される。

このストアを使うには svelte.config.js で kit.version.pollInterval を設定する必要がある。

このストアは本番環境でのみ使うことができる。

アプリのバージョン確認は自動でポーリングによって行われるが、updated.check() 関数を呼び出して手動で行うこともできる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Errors and redirects / Basics

https://learn.svelte.dev/tutorial/error-basics

エラーには予期しているエラーと予期していないエラーの 2 種類がある。

予期しているエラーは error() 関数を使って作成され、throw 文で投げられる。

一方、予期していないエラーは error() 関数を使って作成されないが throw 文で投げられる点は同じ。

予期しているエラーではエラーコードが指定できるが、予期していないエラーは 500 になる。

予期しているエラーのメッセージはユーザーに表示されるが、予期していないエラーのメッセージはコンソールログに出力され、ユーザーには Internal Error と表示される。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Errors and redirects / Error pages

https://learn.svelte.dev/tutorial/error-pages

src/routes/+error.svelte
<script>
	import { page } from '$app/stores';
	import { emojis } from './emojis.js';
</script>

<h1>{$page.status} {$page.error.message}</h1>
<span style="font-size: 10em">
	{emojis[$page.status] ?? emojis[500]}
</span>
src/routes/expected/+error.svelte
<h1>this error was expected</h1>

サーバーで load() 関数の実行中にエラーが発生した場合、エラーページが表示される。

エラーページは +error.svelte ファイルを作成することでカスタマイズできる。

+error.svelte のコンテンツには +layout.svelte のレイアウトが反映される。

routes ディレクトリの直下以外にも +error.svelte ページを作成することができ、ページごとにエラーページの表示内容を細かくカスタマイズできる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Errors and redirects / Fallback errors

https://learn.svelte.dev/tutorial/fallback-errors

src/routes/+layout.server.js
export function load() {
	throw new Error('yikes');
}
src/error.html
<h1>Game over</h1>
<p>Code %sveltekit.status%</p>
<p>%sveltekit.error.message%</p>

レイアウトやエラーページのレンダリング中に例外が発生した場合には src/error.html ページが表示される。

このページでは %sveltekit.status% と %sveltekit.error.message% の 2 つの変数が利用でき、それぞれスターテスコードとエラーメッセージに置き換えられる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Part 3 / Errors and redirects / Redirects

https://learn.svelte.dev/tutorial/redirects

src/routes/a/+page.server.js
import { redirect } from '@sveltejs/kit';

export function load() {
	throw redirect(307, '/b');
}

throw redirect(307, '/b'); のように書くことで他のページにリダイレクトできる。

第 1 引数はステータスコードであり、目的に応じて適切なものを選ぶことが望まれる。

フォームアクションが成功した時には 303 が最適らしい、知らなかった。

https://developer.mozilla.org/ja/docs/Web/HTTP/Status/303

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

11/11 (土) はここまで

30 分経過したので累計 230 分、次はまとめから始める。

  • Introduction
  • Routing
  • Loading data
  • Headers and cookies
  • Shared modules
  • Forms
  • API routes
  • Stores
  • Errors and redirects
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Introduction まとめ

https://zenn.dev/link/comments/79284f4e6efb80

What is SvelteKit

  • SvelteKit はアプリケーションフレームワークであり、ルーティングやサーバーサイドレンダリングなどの機能を提供する。
  • デフォルトではサーバーサイドでレンダリングし、その後はクライアントサイドでのナビゲーションに移行することで初回ロード/SEO とユーザー体験のバランスを取っている。
  • ルートには package.json に加えて svelte.config.js や vite.config.js などの設定ファイルが含まれる。
  • アプリのソースコードは src 以下に格納される。
  • 静的ファイルは static ディレクトリに格納される。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Routing まとめ

https://zenn.dev/link/comments/5cab794d1a4778

Pages

  • SvelteKit ではファイルシステムベースのルーティングを使っている。
  • ファイル名には +page.svelte を使用する。
  • 例えば /about ページを作りたい場合には src/routes/about ディレクトリを作成し、このディレクトリ内に +page.svelte ファイルを作成する。
  • 初回ロードはサーバー側で描画されるがそれ以降のページ移動ではページは更新されず、ページの中身が更新される。
  • これにより、高速な初回ページ読み込みと高速なページ移動の両方が実現される。
  • この振る舞いがデフォルトだが、ページオプション設定を行うことで変更できる。

Layouts

  • 各ページに共通するヘッダーやフッターなどは +layout.svelte ファイルにまとめることができる。
  • +layout.svelte は同じディレクトリと下位ディレクトリのページに適用される。
  • レイアウトはネストすることができる。

Route parameters

  • [slug] のようなディレクトリ名を作成すると動的なルートを作成できる。
  • この点は Next.js と似ているので覚えやすい。
  • なんと [bar]x[baz] のようなルートも作成できる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Loading data まとめ

https://zenn.dev/link/comments/a8bd306f0f7958

Page data

  • SvelteKit のコア機能はルーティング・ローディング・レンダリングの 3 つ。
  • +page.svelte と同じ階層に +page.server.js を作成し、load() 関数をエクスポートすることでローディングを行うことができる。
  • +page.server.js は常にサーバーで実行され、+page.js はサーバーまたはブラウザのいずれかで実行される。
  • コンポーネントでローディングされたデータを受け取るには script タグ内で export let data; と受け渡し用の変数を用意する。
  • エラーページを表示するには @sveltejs/kit から error() 関数をインポートして使用する。

Layout data

  • +layout.server.js ファイルを作成することで下位のルートに共通するデータを読み込むことができる。
  • レイアウトのデータに加えてさらにページ固有のデータローディングが必要な場合、+page.server.js に load() 関数を設ければ良い。
  • この場合、レイアウトの load() 関数の戻り値にページの load() 関数の戻り値がマージされる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Headers and cookies まとめ

https://zenn.dev/link/comments/ab977a3d497d15

Setting headers

  • load() 関数で setHeaders() 関数を使うことで HTTP ヘッダーを設定できる。
  • フォームアクション、フック、API ルートでも同じことができる。

Reading and writing cookies

  • setHeaders() 関数では Set-Cookie ヘッダーを設定できないのでその代わりに cookies API を使用する。
  • クッキーの読み込みには cookies.get() メソッドを使い、書き込みには cookies.set() メソッドを使う。
  • クッキーを書き込む時には path オプションを明示的に指定することが推奨される、なぜなら現在のパスの 1 階層上がデフォルトの path になるため。
  • cookies.set() 関数を実行すると Set-Cookie ヘッダが書き込まれるが、それに加えて内部のクッキーのマップが書き換えられる。
  • したがって cookies.set() 関数を実行した後に cookies.get() を実行すると、cookies.set() 関数で設定された値が読み込まれる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Forms

https://zenn.dev/link/comments/1c7c32542d9720

The form element

  • +page.server.js で actions オブジェクトをエクスポートすることでサーバー側でフォームから送信されたデータを処理することができる。
  • fetch() 関数などを一切呼び出さずにサーバーとクライアントの間でデータをやり取りできる。
  • 仮に JavaScript が無効になっていても動作するので堅牢性も高い。

Named form actions

  • actions のフィールド名に default 以外を指定することで名前付きのアクションを作成できる。
  • フォーム側では action="?/create" のようにアクションを指定する。
  • 名前付きアクションを使う場合はデフォルトアクションは使用できない。
  • 他のページのアクションも実行することができる。

Validation

  • fail() 関数を使うことでエラーコードと共にサーバーのアクションからデータを返却できる。
  • クライアント側では export let form; と書いてデータにアクセスできる。
  • アクションから普通にデータを return しても同じことができる。

Progressive enhancement

  • form に特に何も指定しなければ送信時にページ再読み込みが行われる。
  • use:enhance を指定することで JavaScript でフォーム送信がエミュレートされる。
  • そのためには import { enhance } from '$app/forms'; が必要になる。
  • エンハンスメントを有効にすると遷移を行えるようになる。

Customizing use:enhance

  • use:enhance にコールバックを指定することでフォーム送信時の動作をカスタマイズできる。
  • コールバックはフォーム送信時(正確にはフォーム送信の直前)に呼び出される。
  • コールバックは戻り値として非同期関数を返すことができる。
  • これはおそらくサーバー側でフォームが処理された後に呼び出される。
  • update() 関数は data や form を更新するために呼び出す必要がある。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

API routes まとめ

https://zenn.dev/link/comments/01d3b594987f49

GET handlers

  • routes ディレクトリの子孫として +server.js ファイルを作成することで API ルートを設けることができる。
  • +server.js では GET や POST など HTTP メソッドと同じ名前の関数をエクスポートする。
  • これらの関数では Response オブジェクトをリターンする。
  • レスポンスを JSON 形式で返す場合は import { json } from '@sveltejs/kit'; を利用できる。

POST handlers

  • POST や PUT ではリクエストのヘッダーや本文に含まれるデータを取得できる。
  • 取得の仕方は +page.server.js の load() 関数やフォームアクションと同じ。
  • 例えば POST メソッドでリクエストの本文とクッキーにアクセスする場合は export async function POST({ request, cookies }) { ... } のように書く。
  • 可能なケースではフォームアクションを使った方がコーディング量も少なく、JavaScript が無くても動くので堅牢性が高い。

Other handlers

{ params } 引数を使ってパスパラメーターを取得できる。
204 No Content レスポンスを返すときは return new Response(null, { status: 204 }); とする。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Stores まとめ

https://zenn.dev/link/comments/77736a30fd48d7

page

  • SvelteKit では page / navigating / updated の 3 つの読み込み専用ストアが提供される。
  • これらは $app/stores からインポートできる。
  • page ストアでは現在のページの URL を示す url や パラメーターを示すparams などにアクセスできる。
  • 通常のストアと同様にストアの値にアクセスするには $ を前置する。
  • navigating はいわゆるページ移動を表現するストア。
  • 普段は null だがリンクなどでページ移動が発生する時にオブジェクトになる。
  • 主なプロパティは from / to / type で from と to は始点と終点のページの URL などを示し、type はリンクなどのページ移動の種類を示す。

updated

  • updated ストアはページが表示されてからその時点までに新しいバージョンのアプリがデプロイされたかを示す真偽値が格納される。
  • このストアを使うには svelte.config.js で kit.version.pollInterval を設定する必要がある。
  • このストアは本番環境でのみ使うことができる。
  • アプリのバージョン確認は自動でポーリングによって行われるが、updated.check() 関数を呼び出して手動で行うこともできる。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Errors and redirects

https://zenn.dev/link/comments/8a221f5a4c3a0b

Basics

  • エラーには予期しているエラーと予期していないエラーの 2 種類がある。
  • 予期しているエラーは error() 関数を使って作成され、throw 文で投げられる。
  • 一方、予期していないエラーは error() 関数を使って作成されないが throw 文で投げられる点は同じ。
  • 予期しているエラーではエラーコードが指定できるが、予期していないエラーは 500 で固定。
  • 予期しているエラーのメッセージはユーザーに表示されるが、予期していないエラーのメッセージはコンソールログに出力され、ユーザーには Internal Error と表示される。

Error pages

  • サーバーで load() 関数の実行中にエラーが発生した場合、エラーページが表示される。
  • エラーページは +error.svelte ファイルを作成することでカスタマイズできる。
  • +error.svelte のコンテンツには +layout.svelte のレイアウトが反映される。
  • routes ディレクトリの直下以外にも +error.svelte ページを作成することができ、ページごとにエラーページの表示内容を細かくカスタマイズできる。

Fallback errors

  • レイアウトやエラーページのレンダリング中に例外が発生した場合には src/error.html ページが表示される。
  • このページでは %sveltekit.status% と %sveltekit.error.message% の 2 つの変数が利用でき、それぞれスターテスコードとエラーメッセージに置き換えられる。

Redirects

  • throw redirect(307, '/b'); のように書くことで他のページにリダイレクトできる。
  • 第 1 引数はステータスコードであり、目的に応じて適切なものを選ぶことが望まれる。
  • フォームアクションが成功した時には 303 が適している。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

おわりに

20 分経過したので累計 270 分だった。

Svelte の学習も楽しかったが SvelteKit の学習はもっと楽しかった。

現在進行形で SvelteKit で簡単なアプリ開発をしているのでためになることばかりだった。

最後の Advanced SvelteKit も楽しく学んでいこう。

このスクラップは6ヶ月前にクローズされました