【最低限抑えたい】SvelteKitの書き方(ルーン対応済み)

2025/01/30に公開

はじめに

皆さん、こんにちは。

今回はSvelteKitの基本的な使い方をご紹介します。最近Svelteはバージョンが上がり新しくルーンという表現が追加されました。今回の内容はSvelteのルーンに対応しております。

https://svelte.jp/tutorial/kit/introducing-sveltekit

https://svelte.jp/docs/kit/introduction

サンプルコードのリポジトリ

https://github.com/peter-norio/svelte/tree/main/my-sveltekit

雛形とファイル構成

概要

  • 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関数と universal load関数

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}

server load関数と universal load関数

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メソッド(GETPOSTPUTDELETEなど)に対応する関数をエクスポートすることで、各メソッドに対するリクエストハンドラを設定します。関数名を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を使用する際、受信したリクエストからcookieauthorizationヘッダー(認証情報)を自動的に継承します。また内部リクエストの最適化が行われます。サーバーサイドで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.tsload関数を定義し、fetchでデータを取得してルートレイアウトに渡します。

あとは任意の子コンポーネントでgetContextを使用して、共有されたデータにアクセスできます。

/src/lib/models/User.ts(setContextgetContextで指定する用の型を用意)

// ユーザーを表すを定義
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 を無効化する際は、慎重に検討することが重要です。

参考リンク集

特別なファイル名まとめ

  1. ページ関連
  • +page.svelte:各ページのコンポーネントを定義します。このファイルが存在するディレクトリが、そのままルートパスとして対応します。
  • +page.ts:ページのload関数などを記述します。+page.svelte と組み合わせて使用され、データの取得などを行います。
  1. レイアウト関連
  • +layout.svelte:複数のページ間で共通のレイアウトを定義します。ネストされたディレクトリ構造により、特定の階層以下のページに適用されます。
  • +layout.ts:レイアウトのload関数などを記述します。+layout.svelte と組み合わせて使用され、レイアウト全体で必要なデータの取得などを行います。
  1. サーバー関連
  • +server.ts:API エンドポイントやサーバーサイドの処理を定義します。HTTP メソッド(GET、POST など)に対応する関数をエクスポートすることで、特定のリクエストに応答します。
  1. エラー処理
  • +error.svelte:エラーページをカスタマイズするためのコンポーネントです。特定のルートやレイアウト内でエラーが発生した際に表示されます。
  1. その他
  • [param] ディレクトリ:動的ルートパラメータを定義します。

おわりに

SvelteKit は Svelte 単体では不足しているアプリケーション開発に必要な機能を提供しています。これらはもはやセットで使うことが当たり前なので、どの機能がどちらから提供されているのか混乱しないようにしておく必要がありますね。

Discussion