SvelteKitという名前を聞く機会が増えているのでSvelteKitを学習してみたいという人を対象にSvelteKitを使いこなす上で必須となる基本機能について解説を行っています。

SvelteKitについて

SvelteKitはSvelte(ユーザインターフェイス(UI)を担当)にRouting、SSR(Sever Side Rendering)やTypeScriptなどの機能が追加され効率的にWEBアプリケーションを構築することができるフルスタックのJavaScriptフレームワークの一つです。SvelteKitと比較されるフルスタックフレームワークにはReactのNext.js、Remix、Vue.jsのNuxtなどがあります。ReactとNext.js、Vue.jsとNuxtでは開発者は異なりますが、SvelteとSvelteKitは同じ開発者によって開発が行われているということもあり他のフレームワークに比べて一貫性のある開発が行われているという違いもあります。

Svelteについての記事も公開しているのでSvelteを使ったことがない人は参考にしてみてください。

SvelteKitのプロジェクトの作成

SveteKitのプロジェクトはnpm create svelte@latestコマンドで作成することができます。プロジェクトを作成する前にプロジェクトのテンプレート、TypeScript、ESLint、Prettier、Playwright、Vitestなどの機能を利用するか質問されます。本文書ではテンプレートにはSkeleton projectを選択し、TypeScript、ESLint、Prettierを選択しています。プロジェクトは任意の名前をつけることができここではsveltekit-firstという名前にしています。


 % npm create svelte@latest sveltekit-first

create-svelte version 2.3.2

Welcome to SvelteKit!

? Which Svelte app template? › - Use arrow-keys. Return to submit.
    SvelteKit demo app
❯   Skeleton project
    Barebones scaffolding for your new SvelteKit app
    Library skeleton project
? Add type checking with TypeScript? › - Use arrow-keys. Return to submit.
    Yes, using JavaScript with JSDoc comments
❯   Yes, using TypeScript syntax
    No

? Add ESLint for code linting? › No / Yes
? Add Prettier for code formatting? › No / Yes
? Add Playwright for browser testing? › No / Yes
? Add Vitest for unit testing? › No / Yes
Your project is ready!
✔ Typescript
  Inside Svelte components, use <script lang="ts">
✔ ESLint
  https://github.com/sveltejs/eslint-plugin-svelte3
✔ Prettier
  https://prettier.io/docs/en/options.html
  https://github.com/sveltejs/prettier-plugin-svelte#options

Install community-maintained integrations:
  https://github.com/svelte-add/svelte-adders

Next steps:
  1: cd sveltekit-first
  2: npm install (or pnpm install, etc)
  3: git init && git add -A && git commit -m "Initial commit" (optional)
  4: npm run dev -- --open

To close the dev server, hit Ctrl-C

Stuck? Visit us at https://svelte.dev/ch

コマンドを実行するとsveltekit-firstフォルダが作成されるので作成されたフォルダに移動してnpm installコマンドを実行してください。


 % cd sveltekit-first
 % npm install

npm run devコマンドを実行すると開発サーバが起動します。 Skeleton projectの初期画面は下記の通りです。

Skelton projectの初期画面
Skelton projectの初期画面

画面に表示されている内容はsrc/routesフォルダの+page.svelteファイルに記述されいます。インストール直後のフォルダ構成は非常にシンプルで.svelte-kit以外にnode_modules, src, staticフォルダのみです。staticファイルは静的ファイル専用のフォルダなのでsrcフォルダの中にアプリケーションを動かすために必要なコードを記述したファイルを追加していきます。

インストール直後のフォルダ構成
インストール直後のフォルダ構成

+page.svelteファイルの内容はsrcフォルダのapp.htmlファイルの%sveltekit.body%の場所に挿入されます。


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%sveltekit.assets%/favicon.png" />
    <meta name="viewport" content="width=device-width" />
    %sveltekit.head%
  </head>
  <body data-sveltekit-preload-data="hover">
    <div style="display: contents">%sveltekit.body%</div>
  </body>
</html>

ルーティング

Skeleton Projectではデフォルトの状態ではhttp://127.0.0.1:5173/にアクセスした場合に表示されるページしか存在しませんがSvelteKitではファイルシステムベースルーティング(フォルダベース)を採用しているので/aboutページを作成した場合にはsrc/routesフォルダの下にaboutフォルダを作成してその下にファイルを作成することで自動でルーティングが設定されます。

ページの追加

/aboutにアクセスした時にページを表示させるためにsrc/routesにaboutフォルダを作成してその下にページファイルである+page.svelteファイルを作成してください。+page.svelteファイルにはh1タグでAbout Pageを設定します。ページファイルは拡張子がsvelteとなっておりsvelteの書式でコードを記述していきます。


<h1>About Page</h1>

ブラウザから/aboutにアクセスすると+page.svelteに記述した内容が表示されます。

aboutページの表示
aboutページの表示

ページを追加したい場合にはフォルダを作成してその下に+page.svelteを作成する必要があることがわかりました。ファイル名の前に”+”がつくことを忘れないでください。最初は不自然に感じるかもしれませんがページとコンポーネントを区別にも役立ち慣れるとすぐに気にならなくなります。

リンクの設定

/(ルート)と/aboutの2つのページを作成しましたがリンクがないためページを表示するためにはブラウザに直接URLを入力する必要があります。routes/+page.svelteにリンクを設定して/aboutページに移動できるようにします。


<nav>
  <ul>
    <li><a href="/about">About</a></li>
  </ul>
</nav>
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>

ページの上部に追加したAboutページへのリンクが表示されます。

リンクの表示
リンクの表示

クリックすると/aboutページに移動することができます。 SvelteKitではaタグを利用すると移動する際にページのリロード(再読み込み)を行うことなくスムーズに移動ができます。他のフレームワークではaタグの代わりにLinkコンポーネントを利用する場合もありますがSvelteKitではaタグを利用できます。

通常のHTMLファイルではaタグを設定してページを移動すると移動する度にページのリロードが行われます。

+page.svelteファイル

SvelteKitのドキュメントには+page.svelteは最初のリクエストではサーバ側(SSR)でレンダリングされ、その後のページ移動ではクライアント側でレンダリングが行われると説明があります。つまりここまでの動作確認の例を利用すると/(ルート)にアクセスした場合にはサーバ側で作成されたHTMLを受け取りその内容を表示させます。リンクボタンをクリックして/(ルート)ページからaboutページに移動するとJavaScriptファイルを利用してブラウザ側でHTMLを作成してその内容を表示させます。

実際にどのような処理が行われるのかブラウザのデベロッパーツールのネットワークタブを利用してサーバからのResponseの内容で動作を確認してみましょう。できるだけ本番環境に近いようにnpm run buildでビルドを行っています。ビルドで作成されたファイルを利用してローカル環境でプレビューを行うためnpm run previewコマンドを実行しています。

/(ルート)にアクセスしてサーバから戻されるReponseを確認するとページのそのままの情報(HTML分)を取得していることがわかります。

初回アクセス時のネットワークタブの確認
初回アクセス時のネットワークタブの確認

次のページ移動時にサーバから送信されてくるファイルをわかりやすくするため”/”にアクセスした際に表示されているファイルの情報を削除するためNetworkタブの直下にあるclearボタンをクリックして表示されている内容のクリアを行ってください。

クリアした状態でページ上部になるAboutページのリンクの上にカーソルを当ててください(Hover)。まだクリックは行わないでください。カーソルを当てると2つのJavaScriptファイルがダウンロードされます。

カーソルを当てるとダウンロードされるファイル
カーソルを当てるとダウンロードされるファイル

_page.svelte-XXXがビルド時に作成されたaboutページの内容を含むJavaScriptファイルです。AboutページへのリンクをクリックするとAboutページが表示されます。ダウンロードしたJavaScriptファイルを利用してブラウザ側で内容が表示されます。

ここまでの動作確認で最初にアクセスした際にはサーバでレンダリングされたデータを受け取って表示し、ページ移動を行った際にはJavaScriptファイルを利用してブラウザ側でレンダリングが行われることがわかりました。

preloadについて

リンクにカーソルを当てるだけでJavaScriptファイルがpreloadされることがわかりました。preloadする設定はapp.htmlファイルのbodyタグのdata-sveltekit-preload-data属性で設定が行われています。


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

data-sveltekit-preload-data属性の値をtapに変更することでpreloadの設定を解除することができます。tapの場合はクリックするとJavaScriptファイルがダウンロードされます。SvelteKitではデフォルトの状態でも少しでも高速に動作するような設定が行われています。

Layoutファイルの設定

routesフォルダの+page.svelteファイルにのみナビゲーションのリンクを設定しましたがaboutページにもナビゲーションを設定したい場合に同じ内容を記述することは非効率なのでページの共通の内容についてはLayoutファイルを利用します。通常はヘッダーやフッターの情報をLayoutファイルに記述することになります。

routesフォルダに+layout.svelteファイルを作成して以下の内容を記述します。新たに/(ルート)へのリンクも追加しています。slotタグの部分にlayoutファイルを利用するページファイルの内容が挿入されます。


<nav>
  <a href="/">home</a>
  <a href="/about">about</a>
</nav>

<slot />

routes/+page.svelteファイルから追加してリンクは削除しておきます。/(ルート), /aboutページどちらにも+layout.svelteファイルで記述した内容が表示されます。+layout.svelteファイルをroutesフォルダに作成するだけで自動でLayoutファイルとして動作することがわかります。

layoutファイル設定後のページ
layoutファイル設定後のページ

Activeリンクの設定

複数ページで構成されているアプリケーションの場合にナビゲーションに表示されているリンクを見て現在どのページを閲覧しているのはわかればユーザにとって有益な情報となります。現在閲覧しているページがわかるようにリンクにスタイルを適用します。リンクを適用するためには現在アクセス中のページのURLの情報を知る必要があります。

storeがもつpageを利用してURLを取得することができます。pageは”$app/stores”からimportします。


<script>
	import { page } from '$app/stores';
	console.log($page)
</script>

<nav>
	<a href="/">home</a>
	<a href="/about">about</a>
</nav>

<slot />

ブラウザのデベロッパーツールのコンソールでpageオブジェクトの中身を確認することができます。さまざまな情報がpageオブジェクトに含まれていることがわかります。

pageオブジェクトの中身
pageオブジェクトの中身

現在アクセスしているページのURLとしてrouteのid、urlのpathnameが利用できることがわかります。どちらのプロパティもスタイルの設定に利用することができますが本書ではpathnameを利用します。

$page.url.pathnameが持つ値をチェックすることでページを移動すると正しくURLが取得できているか確認します。


<script>
	import { page } from '$app/stores';
	$: pathname = $page.url.pathname;
	$: console.log(pathname);
</script>

<nav>
	<a href="/">home</a>
	<a href="/about">about</a>
</nav>

<slot />

ナビゲーションのリンクを利用して/aboutページに移動すると/about、/に戻ると/が表示されることがわかります。

各リンクのaタグの中でif文による条件分岐を利用して/(ルート)へのリンクであればpathnameの値が”/”の場合にはactiveクラスが適用され、/aboutへのリンクであればpathnameの値が”/about”の場合にactiveクラスが適用されるように設定を行います。classによるstyleの設定はsvelteファイルのstyleタグで行うことができます。


<script>
	import { page } from '$app/stores';
	$: pathname = $page.url.pathname;
</script>

<nav>
	<a href="/" class:active={pathname === '/'}>home</a>
	<a href="/about" class:active={pathname === '/about'}>about</a>
</nav>

<slot />

<style>
	.active {
		color: red;
		font-weight: 900;
	}
</style>

設定後、/aboutページにアクセスするとリンクにstyleが適用されていることが確認できます。

リンクへのスタイルの適用
リンクへのスタイルの適用

Layoutのネスト化

デフォルトではroutesフォルダに作成した+layout.svelteのレイアウトの設定がすべてのページファイル(*.svelte)ファイルに適用されます。about以下のフォルダに追加のレイアウトを作成した場合はaboutフォルダに+layout.svelteファイルを作成することでレイアウトを適用することができます。


<p>レイアウトファイルのネスト化</p>
<slot />

routesフォルダの+layout.svelteとaboutファイルの+layout.svelteファイルのレイアウトが適用されていることが確認できます。

レイアウトファイルのネスト化の確認
レイアウトファイルのネスト化の確認

その他のレイアウトの設定についてはSvelteKitのドキュメントのAdvanced Layoutsに記載されています。

Loading data

作成した/about/+page.svelteファイルでは記述した内容がそのままページに表示されていましたが外部リソースから取得したデータを表示させる方法を確認していきます。

/userにアクセスするとユーザ一覧が表示されるようにroutesフォルダの下に/userフォルダを作成してその下に+page.svelteファイルを作成します。


<h1>ユーザ一覧</h1>

+layout.svelteファイルにも/userへのリンクを追加しておきます。


<nav>
	<a href="/" class:active={pathname === '/'}>home</a>
	<a href="/about" class:active={pathname === '/about'}>about</a>
	<a href="/user" class:active={pathname === '/user'}>user</a>
</nav>

ブラウザから/userにアクセスすると”ユーザ一覧”の文字列が表示されます。ここまでの設定であれば外部リソースへのアクセスはないので表示されている内容は異なりますがaboutページと動作に違いはありません。

ユーザページ
ユーザページ

+page.svelteファイルでのデータ取得

外部リソースには無料で利用できるJSONPlaceHolderを使用します。https://jsonplaceholder.typicode.com/usersにアクセスすると10名分のユーザ情報が取得できます。

データの取得はライフサイクルフックのonMountを利用しておりコンポーネントがDOMにマウントされた直後に実行されます。取得したユーザ情報は{#each …}を利用して展開しています。


<script lang="ts">
  import { onMount } from 'svelte';

  type User = {
    name: string;
  };

  let users: User[] = [];

  onMount(async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const data = await response.json();
    users = data;
  });
</script>

<h1>ユーザ一覧</h1>
<ul>
  {#each users as user}
    <li>{user.name}</li>
  {/each}
</ul>

/userにブラウザからアクセスするとユーザ一覧が表示されます。

ユーザ一覧の表示
ユーザ一覧の表示

ブラウザ側でデータの取得処理が行われるためページのソースコードを見てもユーザ一覧の情報は追加されていません。

ソースコードの確認
ソースコードの確認

SvelteKitではデータの取得方法(Loading Data)としてコンポーネントがrenderされる前にデータを取得するためload関数を利用します。load関数はpage.ts(page.js)ファイルまたは+page.server.ts(+page.server.js)ファイルで定義することができます。page.tsで定義したload関数はサーバまたはクライアントで実行されますが+page.server.jsで定義したload関数は必ずサーバで実行されます。

page.tsとpage.server.tsの設定

ここでは拡張子がsvelteではなく、tsのファイルを作成していきます。JavaScript環境で動作確認をしている場合はファイルの拡張子はtsではなくjsとなります。最初にpage.tsファイルで、次に+page.server.tsファイルで同じ処理を行い違いを確認します。

最初page.ts(page.js)を利用した動作確認を行います。

userフォルダの下に+page.tsファイルを作成します。page.tsファイルではload関数を利用してデータの取得を行います。load関数の中では先ほどと同様にfetch関数を利用してJSONPlaceHolderにアクセスしてデータを取得してreturnで取得したユーザの情報を戻しています。さらにTypeScriptのsatisfies operatorを利用してPageLoadを設定しています。


import type { PageLoad } from './$types';

type User = {
	id: number;
	name: string;
};

export const load = (async ({ fetch }) => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const users:User[] = await response.json();
  return {
    users
  };
}) satisfies PageLoad;

+page.tsファイルのload関数の中で実行され戻されたデータ(users)は+page.svelteファイルでdata propsとして受け取ることができます。受け取ったdataの中にusers情報が含まれています。$typesからimportするPageDataには戻されるusersの型情報が含まれています。


<script lang="ts">
	import type { PageData } from './$types';

	export let data: PageData;
</script>

<h1>ユーザ一覧</h1>
<ul>
	{#each data.users as user}
		<li>{user.name}</li>
	{/each}
</ul>

表示されるユーザ一覧は+page.svelteファイル内でfetch関数を利用した先ほどと変わりませんがページのソースを見るとユーザ一覧が表示されています。

load関数を利用した場合のソースの確認
load関数を利用した場合のソースの確認

ブラウザから最初に/userにアクセスした場合にはサーバ側で処理が行われ受け取るResponseのデータにユーザ一覧の情報が含まれています。

サーバから戻されるResponseの中身
サーバから戻されるResponseの中身

最初にAboutページにアクセス後にuserのリンクにカーソルを当てるとpreloadによりusersにアクセスを行いデータを取得していることがわかります。

ブラウザからJSONPlaceHolderへのアクセス
ブラウザからJSONPlaceHolderへのアクセス

+page.tsファイルではなく+page.server.tsファイルでload関数を実行します。ファイル名をpage.tsから+page.server.tsに変更してください。またファイル名を変更後は$typesからimportする型をPageLoadからPageServerLoadに変更してください。


import type { PageServerLoad } from './$types';
export const load = (async ({ fetch }) => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await response.json();
  return {
    users
  };
}) satisfies PageServerLoad;

最初にAboutページにアクセス後にuserのリンクにカーソルを当てるとpreloadが行われますが+page.server.tsファイルはサーバ上でload関数が実行されるためブラウザからJSONPlaceHolderのデータを取得するのではなくサーバ側で取得したデータがサーバから送信されていることが確認できます。

サーバから送信されてくるデータの確認
サーバから送信されてくるデータの確認

+page.server.tsファイルと名前にserverが入っている通りload関数はサーバ側で行われます。page.tsファイルの場合は最初のアクセス時にはサーバ側でload関数が実行されますがページを移動した場合にはクライアント側でload関数が実行されていることがわかります。

外部リソースにアクセスするために外部に漏れてはいけない情報を利用する場合はサーバ側で必ず実行される+page.server.tsを利用する必要があります。+page.tsファイルの処理はクライアント側でも実行されるためアクセスに必要な情報はクライアント側にダウンロードされることになるので動作の違いを理解しておくことは重要です。

load関数の設定方法と+page.tsファイルと+page.server.tsファイルを利用した場合の違いも確認することができました。

Dynamic Route

これまで作成した/, /about, /userはStaticなルートでいつも同じURLでアクセスを行います。例えばユーザ一覧に表示されているユーザ名をクリックするとユーザの詳細ページを表示したいとします。ユーザの識別子としてidが利用できるためidの値によって表示させる内容を変更することができます。その場合のURLはいつも同じURLではなく/user/1, /user/2…のように/user/に後のURLが動的に変わります。それをDynamic Routeと呼びます。

ユーザ一覧のユーザ名をクリックするとユーザの詳細ページを表示させるためユーザ名にaタグの設定を行います。user.idを利用するためリンク先のURLは/user/1, /user/2…となります。


<script lang="ts">
  import type { PageData } from './$types';

  export let data: PageData;
</script>

<h1>ユーザ一覧</h1>
<ul>
  {#each data.users as user}
    <li><a href={`/user/${user.id}`}>{user.name}</a></li>
  {/each}
</ul>

Dynamic Routeではidの値が動的に変化するためroutes/userフォルダにidをブラケットで囲んだ[id]フォルダを作成して+page.svelteファイルを作成します。[id]とすることでidの値が動的に変わるDynamic Routeであることを表します。[id]ファイルの+page.svelteファイルにはh1タグでユーザ詳細画面の文字列を追加します。


<h1>ユーザ詳細画面</h1>

設定後、/user/1, /user/2のように/user/以下の値を動的に変更しても同じページが表示されます。数値でなく文字列aaaを入れても同じように表示されます。

ユーザ詳細画面の表示
ユーザ詳細画面の表示

動的に変わる値によって表示する内容を変更するためには/user/1, /user/2における1, 2のパラメータ値をページファイルで取得する必要があります。パラメータの値はActiveリンクの際にも利用したpageから取得することができます。


<script>
  import { page } from '$app/stores';
  console.log($page);
</script>

<h1>ユーザ詳細画面</h1>

ユーザ一覧からユーザ名をクリックしてブラウザのデベロッパーツールのコンソールを確認します。表示されるpageオブジェクトを確認するとparamsにidの値が含まれていることがわかります。

pageオブジェクトの中身を確認
pageオブジェクトの中身を確認

$page.params.idから取得できる値をブラウザ上に表示できるように設定します。


<script>
	import { page } from '$app/stores';
	$: id = $page.params.id;
</script>

<h1>ユーザ詳細画面</h1>
<p>id:{id}</p>

idの値に3を持つユーザ名をクリックすると$page.params.idから取得したidがブラウザ上に表示されます。

idの表示
idの表示

[id]/+page.svelteファイルでidの値を取得することができましたがSvelteKitではload関数を利用してparamsのidを取得することができます。

+page.svelteファイルでもidの値を利用してデータを取得することはできますが+page.ts、+page.server.tsファイルのload関数を利用します。

+page.server.tsファイルを利用してデータの取得を行います。user/[id]フォルダに+page.server.tsファイルを作成、load関数を追加します。load関数の引数からparamsを取得することができます。


import type { PageServerLoad } from './$types';

export const load = (async ({ params, fetch }) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`);
  const user = await response.json();
  return {
    user
  };
}) satisfies PageServerLoad;

+server.tsファイルでもload関数を利用してデータ取得を行うことができます。


import type { PageServerLoad } from './$types';

export const load = (async ({ params, fetch }) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`);
  const user = await response.json();
  return {
    user
  };
}) satisfies PageLoad;

+page.svelteファイルで取得したデータをdata propsで受け取りブラウザ上に表示させます。


<script lang="ts">
  import type { PageData } from './$types';

  export let data: PageData;
</script>

<h1>ユーザ詳細画面</h1>
<p>id:{data.user.id}</p>
<p>name:{data.user.name}</p>

取得したデータはdata propsとして受け取ることができましたが$app/storeの$page.dataからもアクセスすることができます。


<script lang="ts">
	import type { PageData } from './$types';
	import { page } from '$app/stores';
	console.log($page);

	export let data: PageData;
</script>

<h1>ユーザ詳細画面</h1>
<p>id:{data.user.id}</p>
<p>name:{data.user.name}</p>

ブラウザのデベロッパーツールのコンソールを見るとdataオブジェクトの中にユーザ情報が含まれていることが確認できます。このように+page.server.tsファイルのload関数を利用してデータを取得してブラウザ上に表示させることができます。

$page.dataオブジェクトの中身を確認
$page.dataオブジェクトの中身を確認

layoutファイルである$page.dataはroute/+layout.svelteファイルから$pageを介してload関数で取得したデータにアクセスすることもできます。


<script>
  import { page } from '$app/stores';
  $: pathname = $page.url.pathname;
  console.log('layout', $page.data);
</script>

$page.dataから取得した値を利用してページのheaderのtitleの値を更新することもできます。headerの値の設定は+page.svelteでも行うことができますが+layout.svelteファイルでも行うことができます。[id]フォルダにのみに適用する+layout.svelteファイルを作成します。


<script>
	import { page } from '$app/stores';
</script>

<svelte:head>
	<title>{$page.data.user.name}</title>
</svelte:head>
<slot />

設定したtitleについてはブラウザのタブから確認できます。

ブラウザのタブでtitleを確認
ブラウザのタブでtitleを確認

Form Actions

load関数を利用してデータの取得方法を確認しましたがアプリケーションではデータを取得するだけではなくブラウザ側に入力フォームを表示してフォームに入力した値を送信する処理も必要となります。

Form Actionsを利用することでformタグを利用して作成した入力フォームから送信するPOSTリクエストをサーバ側で受け取ることができます。POSTリクエストで送信したデータは通常データベースなどに保存を行います。理解を深めるためにPrismaを利用して実際にSQLiteデータベースを利用して動作確認を行います。

Prismaとは

Prismaはサーバからデータベースへの操作の仲介を行うORM(Object Relational Mapping)です。Prisma上でデータベースのスキーマを定義することでSQLではなくオブジェクトのメソッドを利用してデータベースを操作することができます。オブジェクトのメソッドを利用することで異なるデータベースでも同じメソッドを利用してデータベースの操作を行うことができます。データベースのテーブルも定義したスキーマを元にコマンドで一つで作成することができます。

Prismaの設定

Prismaのインストールを行います。


 % npm install prisma --save-dev

インストール後にPrismaの設定ファイルを作成するために初期化コマンド(npx prisma init)を実行します。接続するデータベースがSQLiteなのでオプションの–datasource-provider sqliteをつけます。デフォルトのデータベースはPostgreSQLなのでオプションをつけない場合は実行後に設定ファイルをSQLite用に更新する必要があります。


 % npx prisma init --datasource-provider sqlite

コマンドを実行するとprismaフォルダと.envファイルが作成されます。prismaフォルダにはPrismaの設定ファイルschema.prismaが作成されます。schema.prismaファイルにデータベースへの接続情報やデータモデルを記述します。データモデルはテーブルの構成情報です。


// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

SQLiteの設定になっているので上記の設定はそのままでデータモデルを記述します。データモデルの記述はデータベースによって異なるのでテーブルに追加した列の型の設定方法やリレーションシップなど不明な場合はPrismaのドキュメントを参考に設定を行ってください。

ここではTodoテーブルを作成するので以下を記述します。Todoテーブルはid, name, isCompleteの3つの列を持ち、idは自動で順番に番号が付与されます。nameはTodoの名前で文字列、isCompleteはTodoが完了したかどうかを表し、trueやfalseのboolean値のみ設定できます。


generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Todo {
  id      Int      @id @default(autoincrement())
  name   String
  isComplete Boolean @default(value: false)
}

schema.prismaの設定が完了したらデータベースファイル(SQLiteはファイルベースのデータベース)とテーブルを作成するためnpx prisma db pushコマンドを実行します。コマンドを実行するとprismaフォルダにdev.dbファイルが作成されます。


 % npx prisma db push

テーブルが作成できているかどうかはPrisma Studioを利用することができます。SQLiteのデータベースへの接続はコマンドによる接続やデータベースの管理ソフト(例:TablePlus)によっても行うことができます。


 % npx prisma studio
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Prisma Studio is up on http://localhost:5555

Prisma Studioからはデータの閲覧だけではなくデータの追加もできるので2件のデータ追加を行います。Prisma Studioからはデータ削除や更新も可能です。

保存したデータの確認
保存したデータの確認

srcフォルダにlibフォルダを作成してdb.tsファイルを作成してデータベースへのアクセスに必要Prisma Clientの設定を行います。


import { PrismaClient } from '@prisma/client';

export const prisma = new PrismaClient();

Prismaの設定は完了です。

Loading dataの設定

データベースに保存したデータをブラウザ上に保存するためデータベースからのデータの取得を行います。routesフォルダにtodoフォルダを作成してその中に+page.svelteファイルを作成してください。+page.svelteファイルではh1タグで”Todo”を追加します。


<h1>Todo</h1>

/todoにアクセスすると文字列の”Todo”が表示されることを確認してください。routes/+layout.svelteファイルにtodoへのリンクも追加しておきます。


<nav>
	<a href="/" class:active={pathname === '/'}>home</a>
	<a href="/about" class:active={pathname === '/about'}>about</a>
	<a href="/user" class:active={pathname === '/user'}>user</a>
	<a href="/todo" class:active={pathname === '/todo'}>todo</a>
</nav>

load関数を利用してデータの取得を行います。SQLite上に作成したtodoテーブルからデータを取得するためにprismaのfindManyメソッドを利用します。


import type { PageServerLoad } from './$types';
import { prisma } from '../../lib/db';

export const load = (async () => {
	const todos = await prisma.todo.findMany();
	return {
		todos
	};
}) satisfies PageServerLoad;

データベースから取得したデータはtodo/+page.svelteファイルでdata propsとして受け取り展開します。


<script lang="ts">
	import type { PageData } from './$types';

	export let data: PageData;
</script>

<h1>Todo</h1>

<ul>
	{#each data.todos as todo}
		<li>{todo.name}</li>
	{/each}
</ul>

データベースから取得したデータをブラウザ上に表示することができました。

SQLiteから取得したデータの表示
SQLiteから取得したデータの表示

formの設定

TodoのNameを入力することができるシンプルな入力フォームを追加します。formタグとinputとbuttonで構成された通常のフォームです。


<script lang="ts">
	import type { PageData } from './$types';

	export let data: PageData;
</script>

<h1>Todo</h1>

<form method="POST">
	<div>
		<label for="name">Name:</label>
		<input name="name" id="name" />
	</div>
	<button type="submit">Add</button>
</form>

<ul>
	{#each data.todos as todo}
		<li>{todo.name}</li>
	{/each}
</ul>

input要素に文字列を入力後に”Add”ボタンをクリックするとHTTPのエラーコード405(Method Not Allowed)のメッセージが表示されます。

405のエラーメッセージの表示
405のエラーメッセージの表示

formタグにactionを設定していていないのでhttp://localhost:3000/todoに対してPOSTリクエストが送信されます。送信したPOSTリクエストはhttp://localhost:3000/todo受け付けてもらえなかったのでエラーとなっています。

+page.server.jsにactionsを設定することでPOSTリクエストをサーバ側で受け取ることができます。defaultプロパティに関数を設定して引数にrequestを設定します。request.formDataからPOSTリクエストで送信されてきたデータを取得することができます。


import type { PageServerLoad, Actions } from './$types';
import { prisma } from '../../lib/db';

export const actions = {
	default: async ({ request }) => {
		const data = await request.formData();
		console.log(data);
	}
} satisfies Actions;

export const load = (async () => {
	const todos = await prisma.todo.findMany();
	return {
		todos
	};
}) satisfies PageServerLoad;

再度ブラウザのフォームに文字列を入力してnpm run devコマンドを実行しているコンソールに取り出したデータが表示されます。Actionを利用してPOSTリクエストを受け取ることができるようになりました。


FormData {
  [Symbol(state)]: [ { name: 'name', value: 'learn Playwrite' } ]
}

nameの値のみ取得したい場合にはdata.get(‘name’)で取得することができます。取得したnameの値を利用してデータベースに追加を行います。データベースへの追加はprisma.todo.createメソッドで行うことができます。


export const actions = {
	default: async ({ request }) => {
		const data = await request.formData();
		const name = data.get('name') as string;
		await prisma.todo.create({
			data: {
				name
			}
		});
	}
} satisfies Actions;

フォームに文字列を入力して”Add”ボタンをクリックするとページが自動でリロードされ追加されたTodoの情報が表示されます。

フォームから入力後の画面
フォームから入力後の画面

Loadを利用してデータベースからのデータ取得、Actionを利用してデータベースへのデータ追加が行えるようになりました。

複数のActionsの設定

+page.svelteファイルではフォーム用の1つのActionしかありませんが1つのファイル内で複数のActionを設定することができます。追加したTodoを削除するためのbuttonを追加します。


<script lang="ts">
  import type { PageData } from './$types';

  export let data: PageData;
</script>

<h1>Todo</h1>

<form method="POST">
  <div>
    <label for="name">Name:</label>
    <input name="name" id="name" />
  </div>
  <button type="submit">Add</button>
</form>

<ul>
  {#each data.todos as todo}
    <li style="display:flex">
      <span>{todo.name}</span>
      <form method="POST">
        <input type="hidden" name="id" value={todo.id} />
        <button>X</button>
      </form>
    </li>
  {/each}
</ul>

+page.server.tsファイルではformDataの中身を確認してどちらのformからのデータを確認します。


import type { PageServerLoad, Actions } from './$types';
import { prisma } from '../../lib/db';

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    console.log(data);
} satisfies Actions;

export const load = (async () => {
  const todos = await prisma.todo.findMany();
  return {
    todos
  };
}) satisfies PageServerLoad;

実行するとどちらのPOSTリクエストもdefalultの処理が実行されます。POSTリクエストの中に識別子を加えて分岐を行うことで別々の処理を行うことができます。SvelteKitでは1つのページに複数のActionがある場合にも対応できるようにActionに名前をつけることができます。Actionの名前をdefaultからcreateに変更し、deleteを追加します。


export const actions = {
  create: async ({ request }) => {
    console.log('create');
    const data = await request.formData();
    console.log(data);
  },
  delete: async ({ request }) => {
    console.log('delete');
    const data = await request.formData();
  }
} satisfies Actions;

Actionの名前はformのaction属性で指定することができます。


<script lang="ts">
  import type { PageData } from './$types';

  export let data: PageData;
</script>

<h1>Todo</h1>

<form method="POST" action="?/create">
  <div>
    <label for="name">Name:</label>
    <input name="name" id="name" />
  </div>
  <button type="submit">Add</button>
</form>

<ul>
  {#each data.todos as todo}
    <li style="display:flex">
      <span>{todo.name}</span>
      <form method="POST" action="?/delete">
        <input type="hidden" name="id" value={todo.id} />
        <button>X</button>
      </form>
    </li>
  {/each}
</ul>

設定後入力フォームの”Add”ボタンをクリックするとコンソールには”create”、削除ボタンをクリックすると”delete”の文字列が表示されるようになります。1つのページに複数のActionが存在しても対応できるようになりました。

+page.server.tsファイルのdelete actionにTodoの削除処理を追加します。


delete: async ({ request }) => {
	const data = await request.formData();
	const id = Number(data.get('id'));
	await prisma.todo.delete({
		where: {
			id
		}
	});
}

削除処理を追加後、削除ボタンをクリックします。クリックしたTodoが削除されます。上部のURLを見ると/todo?/deleteになっていることが確認できます。

削除ボタンをクリックしてTodoを削除
削除ボタンをクリックしてTodoを削除

clickイベントによるPOSTリクエストとAPI Routes設定

Form Actionsを利用してPOSTリクエストを送信してサーバ側で処理を行うことを確認しましたがformではなくクリックイベントを利用してデータの送信、処理を行うこともできます。

formタグにon:submitイベントを設定して”Add”ボタンをクリックするとhandleSubmit関数が実行されます。handleSubmit関数ではこれから作成する/api/todo(API Routes)に対してinput要素に入力した値を送信します。処理が完了するとinvalidateAll関数を利用してLoad関数を再実行させます。


<script lang="ts">
  import { invalidateAll } from '$app/navigation';
  import type { PageData } from './$types';

  export let data: PageData;

  let name = '';

  const handleSubmit = () => {
    fetch('/api/todo', {
      method: 'POST',
      headers: { 'content-type': 'application-json' },
      body: JSON.stringify({ name })
    }).then(() => {
      invalidateAll();
    });
  };
</script>

<h1>Todo</h1>

<form on:submit|preventDefault={handleSubmit}>
  <div>
    <label for="name">Name:</label>
    <input name="name" id="name" bind:value={name} />
  </div>
  <button type="submit">Add</button>
</form>

<ul>
  {#each data.todos as todo}
    <li style="display:flex">
      <span>{todo.name}</span>
      <form method="POST" action="?/delete">
        <input type="hidden" name="id" value={todo.id} />
        <button>X</button>
      </form>
    </li>
  {/each}
</ul>

API Routesの/api/todoを設定するためにroutesフォルダに/api/todoフォルダを作成して+server.tsファイルを作成します。(これまでに出てきたファイルは+page.svelte, +page.server.ts, +page.tsでしたが新たに+server.tsが追加されます。)

POST関数を設定することでPOSTリクエストを受け取ることができます。primaを利用してtodoテーブルに新規のデータを追加し、json関数を利用してJSONとして作成したtodoを戻しています。json関数によりResponseのContent-typeのheadersは自動で設定されます。


import { json } from '@sveltejs/kit';
import { prisma } from '../../../lib/db';
import type { RequestHandler } from './$types';

export const POST = (async ({ request }) => {
  const { name } = await request.json();
  const todo = await prisma.todo.create({
    data: {
      name
    }
  });

  return json(todo);
}) satisfies RequestHandler;

ヘルパー関数jsonを利用せずReponseを利用することもできます。


return new Response(JSON.stringify(todo), {
	status: 200,
	headers: {
		'Content-Type': 'application/json'
	}
});

入力フォームにTodoの名前を入力し”Add”ボタンをクリックするとTodo一覧にデータ追加されます。JavaScriptで処理が行われているのでデータ追加後もページのリロードは行われません。

API Routesのserver.tsファイルではPOSTのほかにGET, PUT, PATCH, DELETEの関数も設定することができます。

POST関数が存在するserver.tsファイルにGET関数を追加するとブラウザに直接URLを指定しても毎回ランダムな値を戻してます。


import { json } from '@sveltejs/kit';
import { prisma } from '../../../lib/db';
import type { RequestHandler } from './$types';

export const POST = (async ({ request }) => {
  const { name } = await request.json();
  const todo = await prisma.todo.create({
    data: {
      name
    }
  });

  return json(todo);
}) satisfies RequestHandler;

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

  return json(number);
}

Validationの設定

入力フォームで入力した値がデータベースに保存するための妥当な値かチェックを行うことをValidationといいます。入力した値をチェック(Validation)するだけではなくデータを入力したユーザにエラーメッセージで入力した値に問題があることを伝える必要があります。SvelteKitではfail関数を利用することでHTTPステータスコードと共にエラーのメッセージと入力したデータを戻すことができます。

nameが空白の場合にValidationを行い、HTTPステータスコード400(Bad Request)とデータとしてメッセージを戻します。


export const actions = {
  create: async ({ request }) => {
    const data = await request.formData();
    const name = data.get('name') as string;
    if (!name) {
      return fail(400, { message: 'name is required.' });
    }
    await prisma.todo.create({
      data: {
        name
      }
    });
  },

戻したデータはpropsのformとして受け取ることができます。formにmessageが含まれているかチェックを行い、含まれている場合はmessageをブラウザ上に表示させます。


<script lang="ts">
  import type { PageData, ActionData } from './$types';

  export let data: PageData;
  export let form: ActionData;
</script>

<h1>Todo</h1>

<form method="POST" action="?/create">
  <div>
    <label for="name">Name:</label>
    <input name="name" id="name" />
    {#if form?.message}<p class="error">{form.message}</p>{/if}
  </div>
  <button type="submit">Add</button>
</form>
//略
<style>
	.error {
		padding: 0.5em;
		font-weight: 900;
		color: white;
		background-color: red;
	}
</style>

空白のまま”Add”ボタンをクリックするとブラウザ上にはメッセージが表示されるようになりました。ユーザはメッセージを確認することで処理を完了させるために正しい値がどのような値なのかを理解することができます。

エラーメッセージの表示
エラーメッセージの表示

エラーが発生した場合でも入力した値を保持するための設定を行います。空白ではなく文字が3文字以上ないとエラーになるようにValidationを変更します。

戻すデータに入力したnameを追加しています。


if (name.length > 3) {
	return fail(400, { name, message: 'name must contain 3 charactors' });
}

入力した値を戻しただけでは何も変化はありません。戻したデータに値がある場合にはinputのvalue属性に設定します。


<form method="POST" action="?/create">
  <div>
    <label for="name">Name:</label>
    <input name="name" id="name" value={form?.name ?? ''} />
    {#if form?.message}<p class="error">{form.message}</p>{/if}
  </div>
  <button type="submit">Add</button>
</form>

3文字以上でなければ入力した値が戻され、エラーメッセージが表示されます。

エラーメッセージ
エラーメッセージ

Validationの設定方法とValidationに失敗した場合のデータの戻し方とエラーの表示方法を理解することができました。

Progressive Enhancement

突然ですがSvelteKitではProgressive Enhancementという考えに基づいて開発されています。Progressive Enhancementという言葉に聞き慣れていない人もいるかもしれませんが、SvelteKitでのProgressive Enhancementは、JavaScriptが利用できない環境においてもブラウザが持つ機能を利用して動作するようにアプリケーションを構築することができ、ブラウザが持つ機能だけで動作することを満たした上でユーザがより快適に操作できるように機能を高めること(progressively enhance user experience)ができることを意味しています。

本当にJavaScriptを停止しても動作するのかを確認します。Chromeブラウザを利用している場合は”設定”→”プライバシーとセキュリティ”→”サイトの設定”のJavaScriptの設定でサイトにJavaScriptの使用を許可しないにしてください。

設定を行ってリンクをクリックしてください。これまでスムーズに移動していたページの移動がページの移動毎にリロードが行われるようになります。これはProgressive Enhancementによりリンクの移動はJavaScriptがなくても可能だがJavaScriptを利用して機能を向上させています。入力フォームの追加、削除はJavaScriptを停止してもこれまでと同じように動作します。

JavaScriptの設定を変更したら忘れずに戻すように設定してください。

JavaScriptを有効にしていてもフォームでTodoを追加した後やTodoを削除した時にページのリロードが行われます。リロードを行わせないように設定することも可能です。ユーザ体験を向上させるためenhanceを利用します。enhanceをimportしてformタグにuse:enhanceを設定します。


<script lang="ts">
  import { enhance } from '$app/forms';
  import type { PageData, ActionData } from './$types';

  export let data: PageData;
  export let form: ActionData;
</script>

<h1>Todo</h1>

<form method="POST" action="?/create" use:enhance>
  <div>
    <label for="name">Name:</label>
    <input name="name" id="name" value={form?.name ?? ''} />
    {#if form?.message}<p class="error">{form.message}</p>{/if}
  </div>
  <button type="submit">Add</button>
</form>

設定完了後はTodoを追加・削除してもページのリロードはなくなります。デフォルトの状態でも動作しますがuse:enhanceを設定することでユーザ体験を向上させることができました。

バリデーションライブラリZodの利用

バリデーションは自分でも設定を行うことができますがバリデーションライブラリの力を借りることで複雑なバリデーションもシンプルなコードで記述することができます。SvelteKitに関わらずさまざまな場所で利用されているバリデーションライブラリのzodを利用した場合のValidationの設定を確認しておきます。

zodを利用するためにはzodのインストールが必要になります。


% npm install zod

zodではスキーマを定義してバリデーションを設定します。TodSchemaではnameプロパティは文字列で文字列の長さが最低3必要であることを設定しています。


const TodoSchema = z.object({
	name: z.string().min(3)
});

例えばフォームにname以外にemailがある場合は下記のように行うことができます。


const TodoSchema = z.object({
	name: z.string().min(3),
        email: z.string().email,
});

zodによるバリデーションはparseメソッドまたはsofrparseメソッドを利用して行うことができます。2つ違いはバリデーションに失敗した場合はエラーをthrowするかどうかです。pareではエラーがthrowされます。

POSTリクエストから送信されてきたはObject.fromEntriesを利用してオブジェクトとして取り出すことができます。


Object.fromEntries(await request.formData());
//
{ name: 'learn SolidJS' }

TodoSchemaのparseメソッドの引数にはオブジェクトを指定します。バリデーションを通過した場合は引数に指定した値がそのまま戻されます。


const result = await TodoSchema.parse(formData);
const { name } = result;

バリデーションに失敗した場合にはエラーthrowされるのでtry, catch構文を利用してthrowしたエラーを取得します。


create: async ({ request }) => {
  const formData = Object.fromEntries(await request.formData());
  try {
    const result = await TodoSchema.parse(formData);
    const { name } = result;
    await prisma.todo.create({
      data: {
        name
      }
    });
  } catch (error) {
    if (error instanceof ZodError) {
      const { fieldErrors: errors } = error.flatten();
      const { name } = formData;
      return fail(400, { name, errors });
    }
  }
},

throwされたエラーがZodErrorのインスタンスかチェック(zodからthrowされたエラーかどうかチェック)を行いその場合はerrorオブジェクトが持つflattenメソッドでエラーを取り出します。エラーはfail関数でクライアントに戻します。戻されるerrorsオブジェクトの中身は下記の通りです。メッセージは配列になっています。


{ name: [ 'String must contain at least 3 character(s)' ] }

formオブジェクトがerrorsを持っているかチェックを行い、持っている場合にはエラーメッセージとして表示させるように設定しています。


<form method="POST" action="?/create" use:enhance>
	<div>
		<label for="name">Name:</label>
		<input name="name" id="name" value={form?.name ?? ''} />
		{#if form?.errors?.name}
			<p class="error">{form?.errors?.name[0]}</p>
		{/if}
	</div>
	<button type="submit">Add</button>
</form>

エラーメッセージの内容をデフォルト値ではなく日本語に設定した場合は下記のように行うことができます。


const TodoSchema = z.object({
  name: z.string().min(3, { message: '3文字以上入力してください。' })
});

Errorについて

SvelteKitではエラーをunexpectedエラーとexpectedエラーの2つのTypeで分類を行っています。expectedエラーについては開発者が予想可能なエラーで、ヘルパー関数errorを利用して作成することができます。それぞれについてどのようにエラーハンドリングするのか確認していきます。

exprected error

例えばJSONPlaceHolderを利用して個別ユーザの情報を取得する際に存在しないidでページにアクセス(/user/11など)するとJSONPlaceHolderからHTTPのステータスコード404が戻されるので戻されるステータスコードで分岐を行うことでヘルパー関数にerrorの引数に404を指定することができます。


import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load = (async ({ params, fetch }) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`);
  if (response.status === 404) {
    throw error(404, {
      message: 'Not found'
    });
  }
  const user = response.json();
  return {
    user
  };
}) satisfies PageServerLoad;

ブラウザ上にerror関数で指定したエラーコードとmessageで指定した文字列”Not found”が表示されます。

+error.svelte

表示されているエラーページをカスタマイズしたい場合にはroutesフォルダに+error.svelteファイルを作成します。


<script>
  import { page } from '$app/stores';
</script>

<h1>{$page.error?.message}</h1>

存在しないidのページ(/user/12)にアクセスするとカスタマイズしたページが表示されるようになります。

カスタマイズしたエラーページ
カスタマイズしたエラーページ

routesフォルダに作成しましたがページフォルダの中にも+error.svelteファイルを作成することができます。userフォルダに+error.svelteを作成します。階層の異なる場所に+error.svelteファイルを複数作成することができます。

routes以下の+error.svelteとroutes/user/+error.svelteのどちらのファイルの内容が表示されるか区別できるように内容を変えています。


<script>
  import { page } from '$app/stores';
</script>

<p>{$page.error?.message}</p>

routes/user/+error.svelteファイルが優先されることがわかります。

routes/usersフォルダに+error.svelteファイルを作成
routes/usersフォルダに+error.svelteファイルを作成

せっかくなのでroutes/user/[id]フォルダの下に+error.svelteファイルを作成します。


<script>
  import { page } from '$app/stores';
</script>

<h1>[id]]</h1>
<p>{$page.error?.message}</p>

Not foundエラーではなくInternal Errorが発生します。このエラーについては予想していなかったエラーなのでUnexpected Errorです。

Internal Error
Internal Error

Unexpected Errorの場合は開発者が予想しなかったエラーなので原因を調査する必要があります。npm run devコマンドを実行したターミナルを見ると”TypeError: Cannot read properties of undefined (reading ‘name’)”が発生しています。routes/user/[id]/+error.svelteを作成したことでエラーの画面が表示されずそのまま+page.svelteの処理が継続されたようです。この動作から+error.svelteを利用したい場合はエラーが発生するページフォルダのファイルではなくその上の階層のフォルダに+error.svelteファイルを作成する必要があることがわかります。

unexpected errorが発生した場合にはエラーのメッセージは”Internal Error”になることもわかりました。


{ "message": "Internal Error" }

Fallback errors

API Routesのserver.ts(server.js)やlayout.js, layout.server.jsファイルでエラーが発生するとfallback errorページが表示されます。先ほど設定した+error.svelteファイルで表示されるページとは異なるものです。

API Routesのserver.tsファイルでエラーが発生した場合にはfallback errorページが表示されるということなので作成済みの/api/todo/server.tsファイルで意図的にエラーをthrowさせます。


export function GET() {
	throw new Error('エラー発生!');
	const number = Math.floor(Math.random() * 6) + 1;

	return json(number);
}

ブラウザから直接/api/todoにアクセスするとInternal Errorのページが表示されます。先ほどのexpectedエラーやunexpectedエラーで表示された画面とは明らかに異なることがわかります。

fallback error page
fallback error page

このページをカスタマイズしたい場合には+error.svelteファイルではなくsrcフォルダにerror.htmlファイルを作成する必要があります。%sveltekit.error.message%にはエラーのメッセージ、%sveltekit.status%にはエラーのステータスコードが入ります。


<!DOCTYPE html>
<html lang="ja">
	<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>

error.htmlファイルを作成後再度アクセスするとerror.htmlファイルの中身が表示されることが確認できます。

error.htmlファイルの中身の表示
error.htmlファイルの中身の表示

Page Optionosの設定

SvelteKitはデフォルトではSSR(Server Side Rendring)が設定されています。SSRはサーバ上でHTMLを生成し、生成したHTMLのデータをブラウザに戻してブラウザがそのデータを受け取り画面が表示されます。デフォルトではSSRの設定が行われていますがSvelteKitはSSRを有効/無効を切り替える方法を提供しています。

SSRの有効/無効の切り替え以外にもCSR(Client Side Rendering)やPrerenderingを行うかどうかの設定も可能です。

CSRはデフォルトで有効になっておりJavaScriptを利用してブラウザ上でページのコンテンツを作成したり、インタラクティブな処理を行うことです。Svelteを利用した本来の動きです。Prerenderingはデフォルトでは無効になっており、SSRのようにリクエストがあったタイミングでページを作成するのではなく、ビルド時に静的なファイルの作成を事前に行うことです。SSG(Static Site Generator)はPrerenderingによって実現できます。

Prerendering、SSR、CSRの有効/無効のオプションが提供されておりページ単位で設定することが可能です。ページにオプションを変更することでどのような動作になるのか確認を行なっていきます。+layout.*ファイルで設定するとそのlayoutファイルを利用するページで設定が反映されます。

prerenderの設定

Prerenderingの有効化はprerenderのオプションを+layout.ts, +layout.server.ts, +page.ts, +page.server.tsファイルに設定することで行うことができます。

動作確認のためユーザの一覧画面のユーザ情報を取得するload関数を設定したroutes/user/+page.server.tsファイルにprerenderオプションを追加しprerenderの値をtrueに設定します。


import type { PageServerLoad } from './$types';
type User = {
  id: number;
  name: string;
};

export const prerender = true; //Prerenderingの設定

export const load = (async ({ fetch }) => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const users: User[] = await response.json();
  return {
    users
  };
}) satisfies PageServerLoad;

Prerenderingの動作確認は開発サーバではなく静的ファイルを作成するビルドで行います。


 % npm run build

prerenderを設定してビルドを行うと.svelte-kitフォルダのoutputフォルダにprerenderedのフォルダが作成されます。その中にはpagesフォルダが存在し静的ファイルのuser.htmlファイルを確認することができます。

ビルド後の静的ファイルの確認
ビルド後の静的ファイルの確認
prerenderをtrueに設定していない場合にはprerenderedフォルダが作成されることはありません。

より明確に静的ファイルが作成されることを確認するためにユーザの詳細画面のデータを取得するload関数が設定されたroutes/user/[id]/+page.server.tsファイルにprerenderを設定します。


import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const prerender = true;

export const load = (async ({ params, fetch }) => {
	const response = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`);
	if (response.status === 404) {
		throw error(404, {
			message: 'Not found'
		});
	}
	const user = response.json();
	return {
		user
	};
}) satisfies PageServerLoad;

ビルドを実行します。

.svelte-kitのprerenderedフォルダにはユーザ10名分の1.htmlから10.htmlまでのファイルが作成されます。

ユーザ分の静的ファイルの確認
ユーザ分の静的ファイルの確認

prerenderedのdependenciesフォルダにはページを移動する際にサーバから受け取るJSONファイルが保存されており、その中にはビルド時に取得したユーザ情報が保存されています。


{"type":"data","nodes":[null,{"type":"data","data":[{"user":1},{"id":2,"name":3,"username":4,"email":5,"address":6,"phone":14,"website":15,"company":16},1,"Leanne Graham","Bret","Sincere@april.biz",{"street":7,"suite":8,"city":9,"zipcode":10,"geo":11},"Kulas Light","Apt. 556","Gwenborough","92998-3874",{"lat":12,"lng":13},"-37.3159","81.1496","1-770-736-8031 x56442","hildegard.org",{"name":17,"catchPhrase":18,"bs":19},"Romaguera-Crona","Multi-layered client-server neural-net","harness real-time e-markets"],"uses":{"dependencies":["https://jsonplaceholder.typicode.com/users/1"],"params":["id"]}}]}

ブラウザから直接/user/1にアクセスした場合(最初のアクセス)には1.htmlファイルが戻されますがそこからリンクを経由して/user/2にページ移動した場合には__data.jsonファイルが戻されてそのデータを利用してブラウザ側でページが描写されます。実際にネットワークタブを見ると__data.jsonが戻されていることが確認できます。内容も先ほどdependenciesフォルダで確認した内容と同じです。

ネットワークタブでサーバから戻されるdata.jsonを確認
ネットワークタブでサーバから戻されるdata.jsonを確認

userフォルダの+page.svelte.tsファイルでprerenderをtrueにせず、[id]フォルダの+page.svelte.tsファイルでのみprerenderをtrueにするとビルド時に以下のエラーが表示され失敗します。[id]フォルダの+page.svelte.tsファイルでは動的に変化するidの値がどの値を持つのかわからないことが原因だと考えられます。prerenderを設定する場合には注意が必要です。


Error: The following routes were marked as prerenderable, but were not prerendered because they were not found while crawling your app:
  - /user/[id]

prerenderオプションを設定することでどのような違いがでるのかを理解することができました。

ssrの設定

ssrの無効化はssrのオプションを+layout.ts, +layout.server.ts, +page.ts, +page.server.tsファイルに設定することで行うことができます。SSRはデフォルトではtrueなのでssrのオプションを設定しない場合はSSRは有効化されています。

動作確認のためユーザの一覧画面のユーザ情報を取得するload関数を設定したroutes/user/+page.server.tsファイルにssrオプションを追加しssrの値をfalseに設定します。


import type { PageServerLoad } from './$types';
type User = {
	id: number;
	name: string;
};

export const ssr = true; //ssrを追加してfalseに設定

export const load = (async ({ fetch }) => {
	const response = await fetch('https://jsonplaceholder.typicode.com/users');
	const users: User[] = await response.json();
	return {
		users
	};
}) satisfies PageServerLoad;

ページのソースを確認するとSSRが有効の場合にはページが作成された状態で戻されるためソースにはユーザの一覧が追加されていますがSSRが無効の場合にはソースファイルにユーザ情報を見つけることができません。

SSRを無効にした場合
SSRを無効にした場合

これだけではSSRが有効化か無効化かわからない人もいると思うのでドキュメントを参考にブラウザ上でしかアクセスできないwindowオブジェクトを利用して動作確認を行います(サーバ側でwindowオブジェクトにアクセスしようとするとエラーになるはずです。userフォルダの+page.svelteファイルからwindowのinnerWidthとinnerHeightにアクセスします。


<script lang="ts">
  import type { PageData } from './$types';

  export let data: PageData;
</script>

<svelte:head>
  <title>ユーザ詳細画面</title>
</svelte:head>

<h1>ユーザ一覧</h1>
<h1>{window.innerWidth}x{window.innerHeight}</h1>

<ul>
  {#each data.users as user}
    <li><a href={`/user/${user.id}`}>{user.name}</a></li>
  {/each}
</ul>

ssrがfalseの場合はサーバ上で処理が行われないためページ上にブラウザの横幅と高さが表示されます。

ssrがfalseの時はwindowオブジェクトにアクセス可能
ssrがfalseの時はwindowオブジェクトにアクセス可能

user/+page.svelte.tsファイルでssrの値をtrueにするか削除するとssrが有効になるので同じページにアクセスすると500 Internal Error画面が表示されます。

開発サーバを起動したターミナルには”ReferenceError: window is not defined”のエラーが表示されます。SSRによりサーバ側で処理しようとしてもwindowオブジェクトが存在しないのでエラーになっています。

ssrオプションによりどのような変化が起こるのかを理解することができました。

csrの設定

csrの無効化はcsrのオプションを+layout.ts, +layout.server.ts, +page.ts, +page.server.tsファイルに設定することで行うことができます。CSRはデフォルトではtrueなのでcsrのオプションを設定しない場合はCSRは有効化されています。

動作確認のためユーザの一覧画面のユーザ情報を取得するload関数を設定したroutes/user/+page.server.tsファイルにcsrオプションを追加しcsrの値をfalseに設定します。


import type { PageServerLoad } from './$types';
type User = {
  id: number;
  name: string;
};

export const csr = false;

export const load = (async ({ fetch }) => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const users: User[] = await response.json();
  return {
    users
  };
}) satisfies PageServerLoad;

CSRが無効化になるとそのページでのJavaScriptが動作しなくなるためナビゲーションのリンクにマウスを当ててもpreloaderによりリンクを当てたページのJavaScriptファイルなどがダウンロードされなくなります。

これだけではわかりにくいので+page.svelteファイルにカウンターを追加してクリックイベントを設定します。


<script lang="ts">
  import type { PageData } from './$types';

  export let data: PageData;

  let count = 0;

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

<svelte:head>
  <title>ユーザ詳細画面</title>
</svelte:head>


<h1>ユーザ一覧</h1>
<button on:click={increment}>
  Clicks: {count}
</button>
<ul>
  {#each data.users as user}
    <li><a href={`/user/${user.id}`}>{user.name}</a></li>
  {/each}
</ul>

csrの値をfalseに設定すると表示されているJavaScriptが利用できないのでボタンをクリックしてもカウントが増えることはありません。csrの値をtrueするか削除するとボタンをクリックするとカウントが増えます。

カウンターの追加
カウンターの追加

さらにブラウザ上で動作しているかどうかは$app/environmentのbrowserの値から確認することもできます。browserからtrueの場合はCSRが有効(デフォルト状態)になっていることを表します。

csrオプションによりどのような変化が起こるのかを理解することができました。

SPAとしての利用

SvelteKitはSPA(Singl Page Application)として利用している場合には@sveltejs/adapter-staticを利用すること設定できます。@sveltejs/adapter-staticのインストールを行います。


% npm install -D @sveltejs/adapter-static

svelte.config.jsファイルで設定を行います。importしているadapterをインストールした@sveltejs/adapter-staticに変更を行い、adapterオプションで設定を行います。fallbackに設定にindex.htmlを設定するとビルドした際(npm run build)にbuildフォルダにindex.htmlファイルが作成されます。


import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/kit/vite';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	// Consult https://kit.svelte.dev/docs/integrations#preprocessors
	// for more information about preprocessors
	preprocess: vitePreprocess(),

	kit: {
		// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
		// If your environment is not supported or you settled on a specific environment, switch out the adapter.
		// See https://kit.svelte.dev/docs/adapters for more information about adapters.
		adapter: adapter({
			fallback: 'index.html'
		})
	}
};

もしWEBサーバにApacheを利用しており、buildファイルをアップロードしたフォルダにアクセスしてもページが表示されない場合には.htaccessの設定が必要になる場合があります。


<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

サーバのルート(http://your_domain/)ではなくhttp://your_domain/svelteのようにsvelteにアクセスした場合にビルトの内容を表示したい場合にはsvelte.config.jsファイルでoptionのbaseを設定することでビルド後に作成されるbuildフォルダのindex.htmlファイル内のパスにpathsで設定した値の/svelteが追加されます。


const config = {
	// Consult https://kit.svelte.dev/docs/integrations#preprocessors
	// for more information about preprocessors
	preprocess: vitePreprocess(),

	kit: {
		 paths: {
	 	base: '/svelte'
		},
		adapter: adapter({
			fallback: 'index.html'
		})
	}
};

.htaccssファイルは以下のように書き換えます。以下のように書き換えることでsvelte以下のURLにアクセスした際に必ずビルド後に作成されるindex.htmlファイルが読み込まれます。もし.htaccessを設定していない場合に直接svelte/aboutなどのページにアクセスした場合にはエラーとなりページが表示されません。


<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /svelte
  RewriteRule ^/svelte/index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /svelte/index.html [L]
</IfModule>

baseを設定する前に/aboutだったURLがbaseにsvelteを設定することで/svelte/aboutとなります。アプリケーション内部でのリンクやstaticに保存した画像へのURLの設定に影響がでるので注意が必要となります。

そのためhrefやimgタグでURLを設定する場合には$app/pathsからimportできるbase, assetsを利用することができます。


import {assets} from "$app/paths";
//略
<img src="{assets}/logo.png" alt="ロゴ" /<

その他

コンポーネントのimport

lib/componentsフォルダに保存したCounterコンポーネントファイルをimportする場合は$lib/components/Counter.svelteと”$”を利用することができます。