【最低限抑えたい】SvelteKitの書き方(ルーン対応済み)
はじめに
皆さん、こんにちは。
今回はSvelteKitの基本的な使い方をご紹介します。最近Svelteはバージョンが上がり新しくルーンという表現が追加されました。今回の内容はSvelteのルーンに対応しております。
サンプルコードのリポジトリ
雛形とファイル構成
概要
-
npx sv create アプリ名
で雛形を作成 - 雛形の内容は選択したテンプレートによって異なる
-
src
フォルダ内にソースコードを配置 -
src/routes
フォルダにルート(ページ)を配置 -
src/lib
フォルダは$lib
エイリアスでアクセス可能
npx sv create アプリ名
で雛形を作成
npx sv create
コマンドを使用すると、新しい SvelteKit プロジェクトの雛形を作成できます。
npx sv create アプリ名
雛形の内容は選択したテンプレートによって異なる
コマンドを実行するといくつか質問されます。SvelteKitとTypeScriptで開発を始める際の選択に星マークをつけておきます。
-
テンプレートの選択: プロジェクトの初期テンプレートを選択します。
- SvelteKit minimal: 最小限の構成を持つテンプレート。⭐️
- SvelteKit demo: デモ用のアプリケーションが含まれるテンプレート。
- Svelte library: Svelteのライブラリ開発向けのテンプレート。
◇ Which template would you like? │ SvelteKit **m**inimal
-
TypeScriptの設定: 型チェックの有無と方法を選択します。
- Yes, using TypeScript syntax: TypeScript構文を使用します。⭐️
- Yes, using JSDoc annotations: JSDocによる型注釈を使用します。
- No: 型チェックを追加しません(推奨されません)。
◇ Add type checking with Typescript? │ Yes, using Typescript syntax
-
追加機能の選択: プロジェクトに追加するツールや機能を選択します。
◇ What would you like to add to your project? (use arrow keys / space bar) │ none
-
パッケージマネージャーの選択
◇ Which package manager do you want to install dependencies with? │ npm
雛形ができたらアプリ名のフォルダの階層に移動しnpm run dev
で起動できます。ブラウザで http://localhost:5173
にアクセスすると、SvelteKit アプリケーションの初期ページが表示されます。
cd アプリ名
npm run dev
minimalのテンプレートはシンプルなメッセージのみ表示されます。
demoのテンプレートはちょっとしたゲームのサンプルアプリが表示されます。
src
フォルダ内にソースコードを配置
作成した雛形のsrc
フォルダ内にソースコードを配置します。
src/lib
フォルダは$lib
エイリアスでアクセス可能
src/lib
フォルダ内には再利用可能な関数やコンポーネントなど、特定のルートに依存しないコードを配置します。このフォルダのモジュールやコンポーネントに対して、$lib
エイリアスを使用してアクセスできます。これにより、深いディレクトリ構造でも相対パスを気にせずにどこからでもインポートが可能となり、コードの可読性と保守性が向上します。
<script lang="ts">
// src/libフォルダのmessageCreator.tsをインポート
import { createMessage } from '$lib/messageCreator';
</script>
参考リンク集
ルーティング(ページルート)
概要
-
src/routes
フォルダ内の構造で表現 -
+page.svelte
ファイルがページを表す -
[パラメータ名]
フォルダでパラメータを利用
src/routes
フォルダ内の構造で表現
SvelteKitは、ファイルシステムベースのルーティングを採用しています。これは、フォルダ構造がそのままURL構造に対応する仕組みです。src/routes
フォルダ直下は/(ルートページ)
を表します。
src/routes
フォルダ以下に配置した+page.svelte
ファイルがページを表します。+page.svelte
ファイルはページを表すためページコンポーネントといいます。
[パラメータ名]
フォルダでパラメータを利用
角括弧 [ ]
を使用してディレクトリ名を指定することで、動的なパラメータをルートに組み込むことができます。例えばhome
フォルダの下に[title]
フォルダを作成した場合/home/パラメータ
に対応するルートとなります。
パラメータの値をコンポーネントで利用するには同階層に+page.ts
ファイルでload関数
をエクスポートします。データ取得のロジックと表示ロジックを分離でき、コードの可読性と再利用性が向上します。
+page.ts
ファイルではload関数
をエクスポートします。この関数は、ページがレンダリングされる前に実行され、取得したデータをコンポーネントに渡します。load関数
の引数に渡されたparams
プロパティからパラメータ名を指定して値を取り出します。load関数
の戻り値はオブジェクトの形式にし、中にパラメータの値を配置することでコンポーネント側に渡ります。
/home/[title]/+page.ts
// pate.tsでのload関数の型をインポート
import type { PageLoad } from "./$types";
// load関数でパラメータを取り出し戻り値に指定することで、コンポーネントに渡すことができる
export const load: PageLoad = ({ params }) => {
const { title } = params;
return { title};
}
コンポーネント側でパラメータを利用するにはpropsからdata
プロパティを受け取ります。data
プロパティにはload関数
からの戻り値が格納されています。ここから値を取り出して利用します。
/home/[title]/+page.svelte
<script lang="ts">
// 【load関数】load関数の戻り値の値と対応する型をインポート
import type { PageProps } from './$types';
// 【load関数】load関数からパラメータを受け取る
const { data }: PageProps = $props();
</script>
<h1>/home/[title] に対応</h1>
<!-- 【load関数】パラメータの値を表示 -->
<p>このページは /home/{data.title} に対応しています。</p>
参考リンク集
レイアウト
概要
-
+layout.svelte
ファイルでレイアウトを共通化 -
(グループ名)
フォルダでルーティンググループを定義 -
+page@フォルダ名
で特定の親レイアウトを直接利用
+layout.svelte
ファイルでレイアウトを共通化
複数のページで共通する部分(例えば、ナビゲーションバーやフッター)を一箇所にまとめて管理するために、+layout.svelte
ファイルを使用します。このファイルを特定のフォルダに配置することで、その階層以下のすべてのページに共通のレイアウトを適用できます。
+layout.svelte
ファイル内で$props
を使いchildren
を受け取ります。このchildren
には+layout.svelte
や+page.svelte
で作成したコンポーネントが渡されます。{@render children()}
を配置すると、各ページのコンテンツがその位置に挿入されます。この構文により、共通のレイアウト部分(例えば、ナビゲーションバーやフッター)の中に、各ページ固有のコンテンツを動的に表示できます。
src/routes/+layout.svelte(ルートレイアウト)
<script lang="ts">
// 現在のパスに沿ったコンポーネント(+layout.svelte か +page.svelte)を受け取る
const { children } = $props();
</script>
<div>
<h1>ルートレイアウト(src/routes/+layout.svelte)</h1>
<!-- ここにコンポーネントが表示される -->
{@render children()}
</div>
<style>
h1 {
color: red;
}
div {
border: 3px solid red;
padding: 10px;
}
</style>
src/routes/home/+layout.svelte(/home のレイアウト)
<script lang="ts">
const { children } = $props();
</script>
<div>
<h2>個別のレイアウト(/home/+layout.svelte)</h2>
{@render children()}
</div>
<style>
h2 {
color: blue;
}
div {
border: 3px solid blue;
padding: 10px;
}
</style>
(グループ名)
フォルダでルーティンググループを定義
丸括弧で囲ったフォルダを用意することで、URLパスに影響を与えずにルートをグループ化できます。機能(ルート)ごとに異なるレイアウトを適用することができます。これによりルートが増えても、機能ごとに整理できるため、可読性と管理しやすさが向上します。
(グループ名)
フォルダの直下に+layout.svelte
配置することでグループ内の共通レイアウトとすることができます。(必須ではありません)また関連するルートをまとめることで、プロジェクトの構造が整理され、管理が容易になります。
src/routes/(blog)/+layout.svelte(blog系機能の共通レイアウト)
<script lang="ts">
const { children } = $props();
</script>
<div>
<h2>(blog)のレイアウト((blog)/+layout.svelte)</h2>
{@render children()}
</div>
<style>
h2 {
color: green;
}
div {
border: 3px solid green;
padding: 10px;
}
</style>
+page@フォルダ名
で特定の親レイアウトを直接利用
ページはフォルダ構造に基づいて上位のすべてのレイアウトを継承します。特定のレイアウトから継承を開始したい場合、+page@フォルダ名
という命名規則を使用して、深くネストされたレイアウト階層から抜け出し、特定の上位レイアウトに直接移行することができます。
(user)/settings/profile/+page@(user).svelteの場合((user)のレイアウトを利用)
(user)/settings/profile/+page.svelteの場合(レイアウトの継承指定なし)
参考リンク集
ローディング(load関数)
-
load関数
でレンダリング前の処理を実装 - server
load関数
と universalload関数
load関数
でレンダリング前の処理を実装
load関数
を使いページやレイアウトがレンダリングされる前に、必要なデータを取得することができます。load関数
は、+page.svelte
や+layout.svelte
と同じ階層に配置したファイルで行います。(+page.ts
や+layout.ts
、または+page.server.ts
や+layout.server.ts
)
load関数
の戻り値はオブジェクトの形式で指定し、コンポーネント側で$props
を使いdata
プロパティから受け取ります。
/docs/+page.ts(load関数を定義)
// +page.tsでのload関数の型をインポート
import type { PageLoad } from "./$types";
// load関数で戻り値に指定した値を、コンポーネントで受け取ることができる
export const load: PageLoad = () => {
return { dummyData: '「この文字列は /home/+page.ts で用意したデータ」' };
}
/docs/+page.svelte(load関数の結果を表示)
<script lang="ts">
// load関数の戻り値の値と対応する型をインポート
import type { PageProps } from './$types';
// load関数から受け取ったデータを取得
const { data }:PageProps = $props();
</script>
<h1>/docs に対応</h1>
<p>このページは /docs に対応しています。</p>
<!-- load関数から受け取ったデータを表示 -->
{data.dummyData}
load関数
と universal load関数
server load関数
は実行される環境に応じて2種類あります。+page.server.ts
や+layout.server.ts
ファイル内に定義すると「サーバーload関数
」となり、+page.ts
や+layout.ts
ファイル内に定義すると「ユニバーサルload関数
」になります。
「サーバーload関数
」と「ユニバーサルload関数
」は実行環境が異なります。「サーバーload関数
」は、サーバー上でのみ実行されるため、機密性の高いデータの処理や、サーバーリソースへのアクセスが必要な場合に適しています。「ユニバーサルload関数
」は、クライアントサイドでも実行されるため、ユーザーの操作に応じて動的にデータを取得する場合や、クライアント側でのみ必要な処理に適しています。外部のAPIからデータを取得する際もサーバーを介さず行えるので効率的です。
参考リンク集
fetch関数・APIルート
概要
-
+server.ts
でAPIルートを作成- setHeadersでcontent-typeヘッダーを設定可能
-
Fetch API
でリクエスト -
load関数
やAPIルートで使用される特別なFetch
- 外部API呼び出しと、同一サーバー内の呼び出し
+server.ts
でAPIルートを作成
+server.ts
ファイルを作成することで、特定のルートに対するAPIエンドポイントを定義できます。このファイル内で、HTTPメソッド(GET
、POST
、PUT
、DELETE
など)に対応する関数をエクスポートすることで、各メソッドに対するリクエストハンドラを設定します。関数名をfallback
とすることで該当のリクエストメソッドに対応する関数がない場合に動作させることができます。型はRequestHandler
型を指定します。
リクエストハンドラはRequestEvent
を引数に取り、Response
オブジェクトを返します。RequestEvent
にはRequest
オブジェクトなどリクエストやルーティングに関するプロパティが存在し処理で利用することができます。
戻り値はReasponse
オブジェクトを指定します。JSONデータやテキストからResponse
オブジェクトを生成するjson関数
やtext関数
なども用意されています。第二引数のオプションにステータスコードを指定することもできます。
/src/routes/api/datas/+server.ts(/api/datas のGETとPOSTのAPIルートを用意)
import { json, text, type RequestHandler } from '@sveltejs/kit';
// GETリクエストに対応するハンドラ
export const GET: RequestHandler = () => {
// json関数でResponseオブジェクトを返す
return json({ dummy: 'GETリクエスト' });
};
// POSTリクエストに対応するハンドラ
export const POST: RequestHandler = async ({ request }) => {
// requestオブジェクトのjsonメソッドでリクエストボディを取得
const body = await request.json();
// 第二引数のオプションでステータスコードを指定
return json({ dummy: 'POSTリクエスト', body }, { status: 201 });
};
// 該当するメソッドがない場合のフォールバックハンドラ
export const fallback: RequestHandler = async ({ request }) => {
// text関数でResponseオブジェクトを返す
return text(`${request.method} には対応していません`, { status: 405 });
};
/src/routes/api/datas/[id]/+server.ts(/api/datas/[id] のPUTとDELETEのAPIルートを用意)
import { json, type RequestHandler } from '@sveltejs/kit';
// PUTリクエストに対応するハンドラ
export const PUT: RequestHandler = async ({ params, request }) => {
// パスパラメータを取得
const id = params.id;
// リクエストボディを取得
const body = await request.json();
// Responseオブジェクトを返す
// 例として、ステータスを204に設定して返す
return new Response(null, { status: 204 });
};
// DELETEリクエストに対応するハンドラ
export const DELETE: RequestHandler = ({ params }) => {
// パスパラメータを取得
console.log(params.id);
return json({ dummy: 'DELETEリクエスト' });
};
Fetch API
でリクエスト
SvelteKitは車輪の再発明をしないよう、標準的なAPIを活用します。通信ではFetch API
が利用できます。特別な機能が付加される場合もありますが、利用方法は通常通りです。
/src/routes/form/+page.svelte(コンポーネントでのfetchは通常通り)
<script lang="ts">
// 結果表示用のstate
let resData = $state();
// GETリクエスト
const getData = async () => {
const res = await fetch('/api/datas');
const data = await res.json();
resData = data.dummy;
};
// POSTリクエスト
const postData = async () => {
const res = await fetch('/api/datas', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: 'abcde' }),
});
const data = await res.json();
resData = data.dummy;
};
// PUTリクエスト
const putData = async () => {
const res = await fetch('/api/datas/1', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: 'abcde' }),
});
resData = res.status;
};
// DELETEリクエスト
const deleteData = async () => {
const res = await fetch('/api/datas/1', {
method: 'DELETE',
});
const data = await res.json();
resData = data.dummy;
};
</script>
<h1>/form に対応</h1>
<p>このページは /form に対応しています。</p>
<div>
<h2>fetchの確認</h2>
<p>fetchの結果:{resData}</p>
<button onclick={getData}>/api/datas にGET</button>
<button onclick={postData}>/api/datas にPOST</button>
<button onclick={putData}>/api/datas/1 にPUT</button>
<button onclick={deleteData}>/api/datas/1 にDELETE</button>
</div>
<style>
div {
border: 1px solid black;
padding: 10px;
}
</style>
load関数
、APIルートで使用される特別なFetch
load
関数やAPIルートなどで使用されるfetch
関数は、標準的なFetch APIと同様に動作しますが、いくつかの特別な機能が追加されています。
サーバーサイドでfetch
を使用する際、受信したリクエストからcookie
やauthorization
ヘッダー(認証情報)を自動的に継承します。また内部リクエストの最適化が行われます。サーバーサイドでfetch
を使用して内部の+server.ts
ルートにリクエストを送信する場合、SvelteKitはHTTP呼び出しのオーバーヘッドを回避し、直接ハンドラ関数を呼び出します。これにより、パフォーマンスが向上します。
特別な機能が追加されたとはいえ、記述方法は変わりません。load関数
の場合は引数のfetch
プロパティからfetch
関数を取り出します。
/src/routes/form/+page.ts(load関数
でGETリクエスト)
// page.tsでのload関数の型をインポート
import type { PageLoad } from "./$types";
// load関数でGETリクエストを送信
export const load: PageLoad = async({ fetch }) => {
const res = await fetch('/api/datas');
const data = await res.json();
return { data };
}
ServiceKtiをBFFとして利用しAPIルートからさらにサーバーアプリにリクエストを送信することもできます。APIルートでfetch
を利用するには各ハンドラの引数でfetch
プロパティを取り出します。
/src/routes/api/other/+server.ts(APIルートからjson-serverへGET)
import { json, type RequestHandler } from '@sveltejs/kit';
// GETリクエストに対応するハンドラ
export const GET: RequestHandler = async ({ request, fetch }) => {
// json-server(ダミー)にGETリクエストを送信
const res = await fetch('http://localhost:3000/data');
const data = await res.json();
return json({ data });
};
/src/routes/form/+page.svelte(コンポーネントに確認用の処理を追加)
<script lang="ts">
// 結果表示用のstate
let resData = $state();
// 他の関数は省略
// GETリクエスト(server.ts からさらに json-server にGET)
const getFromServer = async () => {
const res = await fetch('/api/other');
const data = await res.json();
resData = data.data[0].name;
};
</script>
<h1>/form に対応</h1>
<p>このページは /form に対応しています。</p>
<div>
<h2>fetchの確認</h2>
<p>fetchの結果:{resData}</p>
<!-- 他のボタンは省略 -->
<button onclick={getFromServer}>/api/other にGET(さらにサーバーアプリにGET)</button>
</div>
<div>
<h2>load関数でのfetchの確認</h2>
<p>load関数からの値:{data.data.dummy}</p>
</div>
<style>
div {
border: 1px solid black;
padding: 10px;
}
</style>
外部API呼び出しと、同一サーバー内の呼び出し
BFFとして利用
SvelteKitは、フロントエンドとバックエンドの両方の機能を備えたフルスタックなフレームワークであり、Backend for Frontend(BFF)としての役割を果たすことができます。BFFとは、フロントエンドの特定のニーズに合わせて設計されたバックエンド層を指します。フロントエンドとバックエンドの中間に位置し、双方の複雑な処理を緩和させる役割を持つアーキテクチャ設計パターンです。
具体的には、SvelteKitのAPIルート(+server.ts
)で受け取ったリクエストを、fetch
を使用して同一サーバー内の別のバックエンドサービスに転送し、そのレスポンスをフロントエンドに返すことで、フロントエンドとバックエンドの間の通信を最適化します。
このアプローチにより、フロントエンドはSvelteKitのAPIルートとやり取りするだけで済み、複数のバックエンドサービスの詳細を意識する必要がなくなります。このような構成は、以下の利点があります。さらにセキュリティの向上として、フロントエンドとバックエンドの間にBFFを設けることで、直接的な通信を避け、セキュリティリスクを低減します。また、バックエンドのAPI仕様変更時には、BFFであるSvelteKitのAPIルートを更新することで、フロントエンドへの影響を最小限に抑えることができます。
次のような流れです。
+page.svelte → (fetch) → server.ts → (fetch) → Springなどのアプリ
外部APIの呼び出し
外部のAPIからデータを取得する際、プライベートなクレデンシャルが不要な場合はユニバーサルload関数を利用すると便利です。サーバーを経由せず直接APIからデータを受け取れます。
参考リンク集
フォームアクションとプログレッシブエンハンスメント
概要
- フォームアクションは
<form>
を活用してデータを送信 -
+page.server.ts
ファイルのaction
オブジェクトで処理を定義 - JSに依存せず最低限の動作を提供(プログレッシブエンハンスメント)
-
use:enhance(関数)
でフォーム処理をカスタマイズ
<form>
を活用してデータを送信
フォームアクションはフォームアクションとは、HTMLの<form>
要素を使用して、クライアントからサーバーにデータを送信するための仕組みです。+page.server.ts
ファイル内でactions
オブジェクトをエクスポートし、その中にフォームの送信を処理する関数を定義します。
+page.svelte
では、通常のHTMLと同様にform
要素を配置します。method
属性はPOST
を指定します。action
属性には利用するアクションオブジェクトを指定します。?/アクション名
の形式で指定します。
また、アクションは別階層でも指定可能です。action="/パス?/アクション名"
のように記述することができます。
フォームアクションから値が返される場合は、$props
を使用してform
プロパティを取得します。
/src/routes/formaction/+page.svelte(ページでフォームを用意)
<script lang="ts">
import type { PageProps } from './$types';
// フォームアクションの結果を取得
const { form }: PageProps = $props();
</script>
<h1>/formaction に対応</h1>
<p>このページは /formaction に対応しています。</p>
<p>フォームアクションの結果:{form?.success}({form?.message})</p>
<hr />
<!-- 同階層で用意したデフォルトのアクションが実行されるフォーム -->
<h3>同階層のデフォルトアクションが動作</h3>
<h5>actionは未指定</h5>
<form method="POST">
<label for="message"
>メッセージ:
<input type="text" name="message" />
</label>
<button type="submit">送信</button>
</form>
<!-- 同階層で用意したcreateアクションが実行されるフォーム -->
<h3>同階層のcreateアクションが動作</h3>
<h5>action="?/create" を指定</h5>
<form method="POST" action="?/create">
<label for="message"
>メッセージ:
<input type="text" name="message" />
</label>
<button type="submit">送信</button>
</form>
<!-- 別階層(/other)で用意したcreateアクションが実行されるフォーム -->
<h3>別階層(/other)のcreateアクションが動作</h3>
<h5>action="/other?/create" を指定</h5>
<form method="POST" action="/other?/create">
<label for="message"
>メッセージ:
<input type="text" name="message" />
</label>
<button type="submit">送信</button>
</form>
my-sveltekit/src/routes/other/+page.svelte
別階層のアクションに対応するページ(フォームはないがアクションの結果を表示)
<script lang="ts">
import type { PageProps } from './$types';
const { form }: PageProps = $props();
</script>
<h1>/other に対応</h1>
<p>このページは /other に対応しています。</p>
<p>フォームアクションの結果:{form?.success}({form?.message})</p>
<hr />
+page.server.ts
ファイルのaction
オブジェクトで処理を定義
+page.server.ts
ファイル内でactions
オブジェクトを定義することで、フォームから送信されたデータをサーバーサイドで処理できます。このactions
オブジェクト内のメソッドに具体的なサーバーサイド処理を実装します。
フォームのmethod="POST"
リクエストに対応する関数を持ち、各関数はフォームのaction
属性やbutton
要素のformaction
属性で指定された名前にマッピングされます。
アクションの戻り値で、フォーム送信の結果をクライアント側に渡すことができます。戻り値を工夫することで、送信成功や失敗の状況をユーザーに伝えたり、ページやデータを更新する処理を制御することができます。
/src/routes/formaction/+page.server.ts(同階層にフォームアクションを定義)
import type { Actions } from './$types';
// フォームアクションを定義
// defaultと名前付きアクションは同時に記述できないので、コメントアウトが必要
export const actions: Actions = {
// デフォルトのアクション
default: async ({ request }) => {
// フォームデータを取得
const data = await request.formData();
// name属性がmessageの値を取得
const message = data.get('message');
// 戻り値にフォーム処理の結果を指定
return { success: true, message };
},
// 名前付きアクション
create: async ({ request }) => {
const data = await request.formData();
const message = data.get('message');
console.log(message);
// 戻り値はなくてもOK
}
};
/src/routes/other/+page.server.ts(別階層のフォームアクション)
import type { Actions } from './$types';
// フォームアクションを定義
export const actions: Actions = {
create: async ({ request }) => {
const data = await request.formData();
const message = data.get('message');
console.log(message);
return { success: true, message };
}
};
デフォルトのアクションを呼び出した結果(abcと入力した、名前付きアクションはコメントアウト)
名前付きアクションを呼び出した(デフォルトのアクションはコメントアウト)
別階層のアクションを呼び出した(abcと入力した)
JSに依存せず最低限の動作を提供(プログレッシブエンハンスメント)
プログレッシブエンハンスメントとは、基本的なフォーム送信機能を維持しつつ、JavaScriptが有効な環境ではユーザー体験を向上させる手法です。enhance
関数をインポートし、フォーム要素にuse:enhance
ディレクティブを適用することで有効になります。
JavaScriptが無効な場合は標準的なフォームの動作が行われます。action属性
で指定したパスにリクエストを行いページ更新が行われ、URLもaction
属性の値を含んだパスになります。
JavaScriptが有効な場合は、非同期通信となり、ページリロードではなく部分更新をします。URLも元のパスを維持します。
/src/routes/formaction/+page.svelte(プログレッシブエンハンスメントが有効なフォームを追記)
<!-- プログレッシブエンハンスメントを適用したフォーム -->
<h3>プログレッシブエンハンスメント</h3>
<form method="POST" action="?/update" use:enhance>
<label for="message"
>メッセージ:
<input type="text" name="message" />
</label>
<button type="submit">送信</button>
</form>
/src/routes/formaction/+page.server.ts(追加したアクション)
// 名前付きアクション
// プログレッシブエンハンスメントを有効化したフォームから呼ばれる
// 特別な記述の変更点はなし
update: async ({ request }) => {
const data = await request.formData();
const message = data.get('message');
return { success: true, message };
}
URLがそのまま維持される
use:enhance(関数)
でフォーム処理をカスタマイズ
use:enhance
に関数を渡すことで、フォーム送信時の動作をさらにカスタマイズできます。フォーム送信前と後の処理をカスタマイズすることが可能です。
use:enhance
に渡す関数は、フォーム送信前に実行され、その戻り値として非同期関数を返すことで、フォーム送信後の処理を定義できます。
<form
method="POST"
use:enhance={() => {
// フォーム送信の前処理
return async () => {
// フォーム送信の後処理
};
}}
>
フォームアクションから返した結果を利用するには、後処理の非同期関数でresult
を受け取ります。result
の型はActionResult
型です。このオブジェクトには{ type: 'success'; status: number; data?: Success }
の形式でデータが格納されています。data
にはフォームアクションから返却した任意のデータが含まれますので、後処理で利用することができます。
フォームアクションから任意のデータを返すコードの例
export const actions: Actions = {
default: async ({ request }) => {
// 色々処理
// 独自にresultDataを返却
return { success: true, resultData: 'abc' };
}
};
フォームの後処理にアクションの結果データを利用する例
<form
method="POST"
use:enhance={() => {
return async ({ result, update }) => {
//
if (result.type === 'success') {
const resultData = result.data?.resultData;
console.log(resultData);
}
// use:enhanceの既定の動作を実行
update();
};
}}
>
参考リンク集
エラー対応
概要
- 意図したエラー処理:
error
関数と+error.svelte
コンポーネント - 予期せぬエラー処理:
handleError
フックでサーバーとクライアントの処理 - アプリ全体共通エラーページ:
src/error.html
ファイルを作成
error
関数と+error.svelte
コンポーネント
意図したエラー処理:SvelteKitで意図的にエラーを発生させる際に、@sveltejs/kit
から提供されるerror
関数を使用します。この関数は、指定したHTTPステータスコードとエラーメッセージを持つエラーをスローし、SvelteKitは最も近い+error.svelte
コンポーネントをレンダリングしてエラーを処理します。+error.svelte
を配置することでルートごとのエラーページのカスタマイズができます。
error
関数では、HTTPステータスコードと{message: string}
型のオブジェクトを設定できます。この関数は、指定したステータスコードとエラーメッセージを持つエラーをスローし、エラーページから参照できます。
+error.svelte
内では、$app/state
から提供されるpage
オブジェクトを介してエラー情報にアクセスできます。status
プロパティではステータスコード、error
プロパティでは任意で設定したオブジェクトにアクセスできます。
/src/routes/error/expected/+page.ts(load関数内で意図的にエラーを発生させる)
import { error } from "@sveltejs/kit";
import type { PageLoad } from "./$types";
export const load: PageLoad = ({ params }) => {
if (true) {
// 意図的にエラーを発生させる
error(404, { message: '意図的にエラー' });
}
return {};
}
/src/routes/error/expected/+error.svelte(エラーページ)
<script lang="ts">
// エラー情報をもつpageを取得
import { page } from '$app/state';
</script>
<h1>/error/expected/+error.svelte のエラーページ</h1>
<!-- エラー情報を表示 -->
{page.status}
{page.error?.message}
/src/routes/error/expected/+page.svelte(どうせエラーで表示されないので空のファイル)
空のファイル
handleError
フックでサーバーとクライアントの処理
予期せぬエラー処理:予期しないエラーを処理するために、handleError
フックを使用します。このフックは、サーバーサイドとクライアントサイドの両方で利用可能であり、それぞれの環境に応じて適切なファイルに実装する必要があります。
サーバーサイドで発生するエラーを処理するには、src/hooks.server.ts
ファイルにhandleError
フックを実装します。このフックは、サーバー上で予期しないエラーが発生した際に呼び出され、エラーログの記録やエラーレポートの送信などを行うことができます。戻り値には{message: string}
型のオブジェクトを指定できます。
/src/hooks.server.ts(サーバーサイドの想定外のエラーに対応)
import type { HandleServerError } from '@sveltejs/kit';
// サーバー側で発生した予期せぬエラーを処理する
export const handleError: HandleServerError = ({ error, status, message }) => {
console.log('---- handleError ----');
console.log('ステータスコード:', status);
console.log('ステータスメッセージ:', message);
// errorの型はunknownなので、型を明示的に指定する
// unknown型なのはerrorオブジェクトは多岐にわたるため
console.log((error as Error).message);
// {message: string}型で返す
return {
message: (error as Error).message,
};
};
/src/routes/error/unexpected/+page.server.ts(サーバーサイドでわざとエラーにするために用意)
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = () => {
throw new Error('load関数でサーバーエラーが発生');
}
コンソールにメッセージを表示した結果
クライアントサイドで発生するエラーを処理するには、src/hooks.client.ts
ファイルにhandleError
フックを実装します。このフックは、クライアント上で予期しないエラーが発生した際に呼び出され、ユーザーへの通知やエラーログの送信などを行うことができます。
/src/hooks.client.ts
import type { HandleClientError } from '@sveltejs/kit';
// クライアント側で発生した予期せぬエラーを処理する
export const handleError: HandleClientError = ({ error, status, message }) => {
console.log('---- clienthandleError ----');
// errorの型はunknownなので、型を明示的に指定する
// unknown型なのはerrorオブジェクトは多岐にわたるため
console.log('クライアント:'+(error as Error).message);
// {message: string}型で返す
return {
message:'クライアント:'+ (error as Error).message,
};
};
src/error.html
ファイルを作成
アプリ全体共通エラーページ:アプリ全体共通のフォールバックエラーページをカスタマイズするために、プロジェクトのルートディレクトリに src/error.html
ファイルを作成します。このファイルは、アプリケーション内でエラーが発生し、適切なエラーハンドリングが行われない場合に表示されるページを定義します。
/src/error.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>%sveltekit.error.message%</title>
</head>
<body>
<h1>My custom error page</h1>
<p>Status: %sveltekit.status%</p>
<p>Message: %sveltekit.error.message%</p>
</body>
</html>
/src/routes/+layout.svelte(エラーの発生を仕込んだルートレイアウト)
<script lang="ts">
// 現在のパスに沿ったコンポーネント(+layout.svelte か +page.svelte)を受け取る
const { children } = $props();
throw new Error('/src/route/+layout.svelte でエラーが発生しました');
</script>
<div>
<h1>ルートレイアウト(src/routes/+layout.svelte)</h1>
<!-- ここにコンポーネントが表示される -->
{@render children()}
</div>
<style>
h1 {
color: red;
}
div {
border: 3px solid red;
padding: 10px;
}
</style>
ルートレイアウトでエラーが発生し用意したerror.htmlが表示された
参考リンク集
状態管理
概要
- 注意事項(サーバーで状態を持たない、
load関数
に副作用を入れない) - Svelteの機能で状態を共有(ContextAPIを利用)
SvelteKit自体は状態管理の仕組み提供していませんが、Svelteの組み込み機能でContext APIを活用して、効果的に状態管理を行うことができます。
load関数
に副作用を入れない)
注意事項(サーバーで状態を持たない、SvelteKitで状態管理を行う際は「サーバーで状態を持たない」「load関数
に副作用を入れない」という注意事項があります。これらの原則を守ることで、アプリケーションの予期しない動作やセキュリティ上の問題を防ぐことができます。
サーバーは複数のユーザーからのリクエストを処理するため、グローバル変数にデータを保存すると、ユーザー間でデータが共有されてしまう可能性があります。
load関数
は純粋関数として設計し、副作用を持たせないようにするべきです。例えば、load関数
内でグローバルなストアにデータを書き込むのは避けるべきです。そのようなコードでは、グローバルなストアにデータを書き込むことで、他のユーザーにデータが共有される可能性があります。代わりに、load関数
からデータを返し、必要なコンポーネントでそのデータを使用するようにします。
Svelteの機能で状態を共有(ContextAPIを利用)
SvelteKitでは、SvelteのContext APIを利用して状態を共有します。
具体的には、src/routes/+layout.svelte
(ルートレイアウト)でsetContext
を使用して、データを共有します。初期データを取得する場合は、src/routes/+layout.server.ts
でload関数
を定義し、fetch
でデータを取得してルートレイアウトに渡します。
あとは任意の子コンポーネントでgetContext
を使用して、共有されたデータにアクセスできます。
/src/lib/models/User.ts(setContext
やgetContext
で指定する用の型を用意)
// ユーザーを表すを定義
export interface User {
name: string;
age: number;
city: string
};
// コンテキストに保存するデータの型を定義
export interface UserStore {
users: User[];
addUser: (name: string) => void;
}
/src/routes/+layout.server.ts(サーバーから取得したデータを初期値にするのでload
で取得)
import type { LayoutServerLoad } from './$types';
// stateの初期値を取得して渡す
export const load: LayoutServerLoad = async ({ fetch }) => {
const response = await fetch('http://localhost:3000/data');
const users = await response.json();
return { users };
};
/src/lib/stores/userStore.svelte.ts(コンテキストに保存する状態と操作)
import type { User } from '$lib/models/User';
// コンテキストに保存するデータを用意する関数
// stateと操作の関数を返す
export function userStore(data: User[]) {
let users = $state<User[]>(data);
const addUser = (name: string) => {
const newUser = {
name: name,
age: 10,
city: 'Tokyo',
};
users.push(newUser);
};
return { users, addUser };
}
/src/routes/+layout.svelte(ルートレイアウトでsetContext
でデータを共有)
<script lang="ts">
import type { UserStore } from '$lib/models/User.js';
import { userStore } from '$lib/stores/userStore.svelte';
import { setContext } from 'svelte';
// 現在のパスに沿ったコンポーネント(+layout.svelte か +page.svelte)を受け取る
const { children, data } = $props();
// throw new Error('/src/route/+layout.svelte でエラーが発生しました');
// Context APIを使ってアプリケーション全体で共有するデータを設定
setContext('users', userStore(data.users));
</script>
<div>
<h1>ルートレイアウト(src/routes/+layout.svelte)</h1>
<!-- ここにコンポーネントが表示される -->
{@render children()}
</div>
<style>
h1 {
color: red;
}
div {
border: 3px solid red;
padding: 10px;
}
</style>
/src/routes/child/+page.svelte(任意の子コンポーネントでgetContext
でデータを取得)
<script lang="ts">
import AddUserForm from '$lib/components/AddUserForm.svelte';
import type { UserStore } from '$lib/models/User';
import { getContext } from 'svelte';
// コンテキストからデータを取得
const { users } = getContext<UserStore>('users');
</script>
<h1>/child に対応</h1>
<p>このページは /child に対応しています。</p>
<div>
{#each users as user}
<h2>{user.name}</h2>
<p>{user.age}</p>
{/each}
</div>
<!-- ユーザー追加フォームを配置 -->
<AddUserForm />
/src/lib/components/AddUserForm.svelte(他の子コンポーネントでもgetContext
を使う例)
<script lang="ts">
import type { UserStore } from '$lib/models/User';
import { getContext } from 'svelte';
// コンテキストからユーザー追加関数を取得
const { addUser } = getContext<UserStore>('users');
// 入力値を受け取る変数
let newUser = '';
</script>
<input bind:value={newUser} />
<button onclick={() => addUser(newUser)}>追加</button>
入力して追加すると一覧表示もリアクティブに変化する(コンポーネントを跨いでstateを共有)
参考リンク集
画面遷移
概要
-
<a>
要素でリンクを配置 -
goto
関数でプログラム的に遷移
<a>
要素でリンクを配置
SvelteKitでは、アプリ内のルート間のナビゲーションに特別なコンポーネントを使用せず、標準の<a>
要素を用います。
/src/routes/home/+page.svelte(<a>
要素でリンクを配置)
<script lang="ts">
const third = '333';
</script>
<h1>/home に対応</h1>
<p>このページは /home に対応しています。</p>
<!-- パラメータを指定したリンクを配置 -->
<a href="/home/first">/home/first</a>
<a href="/home/second">/home/second</a>
<!-- thirdパラメータには変数を指定 -->
<a href={`/home/${third}`}>/home/third</a>
goto
関数でプログラム的に遷移
コード内からプログラム的に画面遷移を行いたい場合、goto
関数を使用します。
/src/routes/home/+page.svelte(ボタン押下で動作する関数の中でgoto
関数での遷移)
<script lang="ts">
// goto関数をインポート
import { goto } from '$app/navigation';
const third = '333';
// /docsへプログラム的に遷移する関数
const navigateToDocs = () => {
// ここで何かしらの処理を行った後に /docs へ遷移
goto('/docs');
};
</script>
<h1>/home に対応</h1>
<p>このページは /home に対応しています。</p>
<!-- パラメータを指定したリンクを配置 -->
<a href="/home/first">/home/first</a>
<a href="/home/second">/home/second</a>
<!-- thirdパラメータには変数を指定 -->
<a href={`/home/${third}`}>/home/third</a>
<!-- /docsへプログラム的に遷移するボタン -->
<button onclick={navigateToDocs}>/docsへ</button>
リンクとボタン(プログラム的な遷移)を配置した画面
参考リンク集
サーバーサイドレンダリング
概要
- デフォルトでSSRが有効
- SSRを無効化することもできるが基本的に非推奨
デフォルトでSSRが有効
SvelteKit はデフォルトでサーバーサイドレンダリング(SSR)が有効になっています。SSR では、サーバー上で HTML を生成し、クライアントに送信します。その後、クライアント側でハイドレーションを行い、ページをインタラクティブにします。このプロセスにより、初回ロード時のパフォーマンス向上や SEO の改善が期待できます。
SSRを無効化することもできるが基本的に非推奨
SvelteKit ではサーバーサイドレンダリング(SSR)を無効化することは可能ですが、特別な理由がない限り、SSR を有効のままにしておくことが推奨されています。SSR を無効にすると、初回ロード時のパフォーマンスや SEO に影響を及ぼす可能性があります。そのため、SSR を無効化する際は、慎重に検討することが重要です。
参考リンク集
特別なファイル名まとめ
- ページ関連
-
+page.svelte
:各ページのコンポーネントを定義します。このファイルが存在するディレクトリが、そのままルートパスとして対応します。 -
+page.ts
:ページのload関数
などを記述します。+page.svelte
と組み合わせて使用され、データの取得などを行います。
- レイアウト関連
-
+layout.svelte
:複数のページ間で共通のレイアウトを定義します。ネストされたディレクトリ構造により、特定の階層以下のページに適用されます。 -
+layout.ts
:レイアウトのload関数
などを記述します。+layout.svelte
と組み合わせて使用され、レイアウト全体で必要なデータの取得などを行います。
- サーバー関連
-
+server.ts
:API エンドポイントやサーバーサイドの処理を定義します。HTTP メソッド(GET、POST など)に対応する関数をエクスポートすることで、特定のリクエストに応答します。
- エラー処理
-
+error.svelte
:エラーページをカスタマイズするためのコンポーネントです。特定のルートやレイアウト内でエラーが発生した際に表示されます。
- その他
-
[param]
ディレクトリ:動的ルートパラメータを定義します。
おわりに
SvelteKit は Svelte 単体では不足しているアプリケーション開発に必要な機能を提供しています。これらはもはやセットで使うことが当たり前なので、どの機能がどちらから提供されているのか混乱しないようにしておく必要がありますね。
Discussion