@ryoppippi

SvelteKit, Progressive Enhancement, Form, Type Safety, そしてSuperforms

28 Apr 2023 ・ 14 min read


SvelteKit で​最近さまざまな​案件が​できていて​嬉しい​限りである。 さて、​SvelteKit の​ドキュメントに​しばしば​登場する​ Progressive Enhancement と​いう​概念が​ある。 この​概念に​自分は​全く​明るくなかったので​調べてみた。

この​記事では​まず、​Progressive Enhancement とは​何かを​説明する。 次に、​Sveltekit に​おいて​この​概念が​よく​表れている​ Form の​扱いに​ついて​触れる。 最後に、​SvelteKit に​おける​型安全に​ついて​触れ、​この​型安全を​強化する​ Superforms と​いう​ライブラリを​紹介する。

この​記事は​ Rich Harris 氏の​先日の​講演の​影響を​多分に​受けている。 /blog/2023-04-26-zenn-8addfe62eb4d3e-ja

Progressive Enhancement

https://www.shopify.com/partners/blog/what-is-progressive-enhancement-and-why-should-you-care

https://accessible-usable.net/2010/06/entry_100606.html

https://developer.mozilla.org/ja/docs/Glossary/Progressive_Enhancement

これらの​記事が​詳しいが、​簡単に​解説する。 Progressive Enhancement とは、​任意の​環境で​全ての​ユーザーが​使用できるよう、​基本と​なる​機能は​全ての​ブラウザで​動作するように​して、​その上に​新しい​環境で​のみ​動作するより​高度な​機能や​装飾を​追加実装する​開発哲学である。 特段 Web 開発に​おいては​以下の​手法を​取る​ことが​多い。

  • HTML のみで​基本的な​機能を​動作させ、​情報が​伝える
  • その上で​ CSS に​よる​見た​目の​装飾を​行う
  • さらに​その上で​ JavaScript を​用いてより​快適な​インタラクションを​実現する

これらの​開発の​メリットは

  • HTML ベースで​必要な​機能が​動く​ため、​想定された​ JavaScript が​動作する​環境が​ない​場合でも​動作する
  • 古い​環境に​合わせて​実装を​用意する​必要が​なく、​1つの​コードベースで​動作する

と​いった​ものが​ある。

さて、​現代に​おいて、​JavaScript が​動作しない​環境など​あるのだろうか。 IE の​死を​迎え、​モダンブラウザが​ Desktop/Mobile ともに​普及している​現代に​おいて、​JavaScript が​動作しない​環境は​ほぼないと​言っていいだろう。 しかし​現実には、​ JavaScript が​動かない、​提供されないと​いった​ことは​起きている​^[Everyone has JavaScript, right?(https://www.kryogenix.org/code/browser/everyonehasjs.html) ]。 また、​通信制限の​かかっている​スマートフォンで​ JavaScript を​完全に​読み込むのに​時間が​かかりすぎて、​想定した​操作が​行えない​ことは​誰しもが​経験している​ことだろう。

しかし、​リッチな​体験を​提供する​ためには​ JavaScript は​必要不可欠である。​ゼロに​する​ことは​できない。 Progressive Enhancementの​考え方は、​ユーザーの​それぞれの​環境で​ベストな​パフォーマンス、​ベストな​体験を​提供しようとする​ものである。

ちなみに​この​反対と​して​Graceful degradation^[Graceful degradation (グレースフルデグラデーション)(https://developer.mozilla.org/ja/docs/Glossary/Graceful_degradation)]と​いう​開発哲学も​ある。

Form に​ついておさら​い

Form は​ HTML の​タグであり、​Form タグを​使えば​ユーザーからの​入力を​サーバーに​送る​ことができる。 例えば、​以下は​ Form タグを​使った​簡単な​ログイン画面の​コードである。 Form 内では​おなじみinputタグを​用いて​ユーザーからの​入力を​受け取り、​submit​ 属性の​あるbuttonタグを​用いて、​その​入力を​サーバーに​送信する。

<form action="" method="POST">
	<label for="username">Username</label>
	<input type="text" id="username" name="username" />

	<label for="password">Password</label>
	<input type="password" id="password" name="password" />

	<button type="submit">Login</button>
</form>

サーバー側の​処理は​どんな​言語でも​良いが、​SvelteKit では​このように​書く

export const actions = {
	default: async (event) => {
		const data = await request.formData();
		const username = data.get('username');
		const password = data.get('password');
		return { message: `Hello ${username}!` };
	},
};

Form + Fetch API = 💔

https://drewdevault.com/2021/10/17/Reliability.html

some stupid reason some asshole developer decided to reimplement all of the form semantics in JavaScript, and now I can’t pay my electricity bill without opening up the dev tools

さて、​この​ Form に​よる​データの​送信は、​JavaScript が​動作しない​環境でも​動作する。 しかし、​この​ Form の​実装では、​送信時に​画面が​遷移してしまうと​いう​問題が​ある。 その​ため、​以下のように​ JavaScript を​用いて、​画面遷移を​防ぎつつ、​Form の​データを​送信する​ことが​多い。

<script>
	const form = document.querySelector('form');
	const url = 'https://example.com/login';

	form.addEventListener('submit', async (event) => {
		/** 本来のFormの動作を停止させ、画面遷移を止める */
		event.preventDefault();

		const formData = new FormData(form);
		const response = await fetch(url, {
			method: form.method,
			body: formData,
		});
		const data = await response.json();
		console.log(data);
	});
</script>

<form method="POST">
	<label for="username">Username</label>
	<input type="text" id="username" name="username" />

	<label for="password">Password</label>
	<input type="password" id="password" name="password" />

	<button type="submit">Login</button>
</form>

上で​述べた​通り、​この​実装では​画面遷移が​ないため、​例えば​ Loading Indicator などの​インタラクションを​実装する​ことができる。 しかし、​この​実装では、​JavaScript が​動作しない​環境では​動作しない。 その​ため一度​ JavaScript の​読み込みに​問題が​起きると​いかなる​操作も​できなくなる。 ただログインしたいだけなのに…。

SvelteKit の​ Form に​おける​ Progressive Enhancement

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

こちらも​ Document に​全てが​書いてあるが、​簡単に​説明する。

SvelteKit では、use:enhanceaction を​ Form に​付与するだけで、​Progressive Enhancement を​実現できる。 言い​換えれば、use:enhanceが​付与されている​ Form では、​JavaScript が​動作しない​環境では​伝統的な​ Form の​送信、​JavaScript が​動作する​環境では​ JavaScript を​用いた​リッチな​体験を​ともなった​ Form の​送信が​行われる。

<script>
	import { enhance } from '$app/forms';
</script>

<form method="POST" action="" use:enhance>
	<label for="username">Username</label>
	<input type="text" id="username" name="username" />

	<label for="password">Password</label>
	<input type="password" id="password" name="password" />

	<button type="submit">Login</button>
</form>

このように​簡単な​実装で、​Project Enhancement を​実現できる。 また、​アニメーションや​データ加工等の​処理を​ Client Side で​行いたい​場合は、use:enhanceの​代わりにuse:enhance={options}を​用いる​ことで、​より​複雑な​処理を​行うことができる。

以下に、use:enhanceを​適用させた​Form を​用いた​簡単な​デモを​用意した。 https://sveltekit-form-examples.vercel.app/ https://github.com/ryoppippi/sveltekit-form-examples

この​サイトは、​名前と​何秒後に​レスポンスを​返すかを​入力すると、​その​秒数後に​ Hello {name}! と​いう​メッセージを​返す。 是非とも​ブラウザで​ JavaScript を​無効化したり、​遅い​回線を​エミュレートして​試していただきたい。

form_1 通常の​回線での​ Form の​挙動。​ローディングアニメーションなどリッチな​画面が​実現できている。

form_2 50kbps の​回線での​ Form の​挙動。​JavaScript が​完全に​読み込まれていないため、​通常の​ Form の​挙動に​なっている。

SvelteKit に​おける​型安全

https://svelte.jp/blog/zero-config-type-safety

さて、​ここで​趣向を​変えて​SvelteKit に​おける​型安全に​ついて​説明する。

SvelteKitは​型安全の​保証を​頑張っていて、​かなり​開発体験が​良い。

以下に​スクリーンショットを​掲載する。

form_3

SvelteKitに​馴染みが​ない方に​説明すると、 SvelteKitでは​ページを​表すための​ファイルが​3種類ある。+page.js, +page.server.js, +page.svelteである。 ざっくり​言えば、+page.svelteは​表示部分を​担当する​Markup Languageであり、+page.js, +page.server.jsは​それぞれ表示部分に​流し込むデータを​用意したり、​サーバーの​挙動を​定義したりする​ファイルである。 +page.server.jsにはload関数を​定義する。​これが​ページの​読み込み時に​実行される​関数である。 この​返り値は、+page.sveltedata変数に​渡る​ことになる。

素晴らしい​ことに、​この​2つの​関数/変数は​それぞれ別々の​ファイルに​またがっているのにも​かかわらず、​Svelteの​Language Serverが​解析を​頑張っている​おかげで​綺麗に​型安全が​保証されている。 上の​スクリーンショットでは、+page.server.jsload関数の​返り値を​変更すると、+page.sveltedata変数の​型が​変わっている​ことがわかる。

さて、​ページの​レンダリング時の​型安全が​保証されている​ことは​わかったが、​では、​Form の​送信時の​型安全は​どうなっているのだろうか? サーバー側で​Formを​受け取った​時の​処理は+page.server.jsactionで​定義する。

https://github.com/ryoppippi/sveltekit-form-examples/blob/986ffa369721ebdd45f063f131c5604db4bb307f/src/routes/%2Bpage.server.js#L3-L13

そして​残念ながら、​ここでは​型安全が​保証されていない。​この​コード上のusernameは​nullかもしれないし、​stringかもしれない。​単なるFormData型である。 現状では​SvelteKitの​標準では​Form Actionの​型安全を​保証する​方​法は​ない。

私は​この​数ヶ月、​この​問題を​打破しようと、​Formを​使うのを​やめて​tRPC+Zodを​導入してみたりと​数種類の​試みを​していた。 しかし​これでは​Progressive Enhancementが​達成できず、​せっかく​SvelteKitが​提供してくれる​開発体験が​台無しに​なってしまう。 どうしようかと​考えていた​3月頃、​Superformsと​いう​ライブラリに​出会った。

Superforms + Zod = 💘

https://superforms.vercel.app

Superformsは​Zodを​用いて、​Formの​型安全を​保証する​ライブラリである。 ご存じZodは​ランタイム時の​型安全を​保証してくれる​ライブラリであるが、​これを​用いる​ことで、​Formの​送信時の​型安全を​保証する​ことができる。 以下に​例を​示す。

https://sveltekit-form-examples.vercel.app/superforms

https://github.com/ryoppippi/sveltekit-form-examples/blob/82bd6695798b027c3b7abfd052092e8793144066/src/routes/superforms/%2Bpage.server.js#L6-L14

Superformsでは​Zodを​用いて​Form Schemaを​定義する。​そして​その​情報をload関数の​返り値に​渡す​ことで、+page.svelteに​Formの​定義を​渡している。

https://github.com/ryoppippi/sveltekit-form-examples/blob/82bd6695798b027c3b7abfd052092e8793144066/src/routes/superforms/%2Bpage.svelte#L1-L15

そして、+page.svelteではdata変数で​受けた​Zod Schemaを​用いて​Svelte Store^[Stores / Writable stores • Svelte Tutorial(https://svelte.jp/tutorial/writable-stores)]を​生成している。

https://github.com/ryoppippi/sveltekit-form-examples/blob/82bd6695798b027c3b7abfd052092e8793144066/src/routes/superforms/%2Bpage.svelte#L17-L29

この​Storeを​Formの​それぞれのinputタグのbindディレクティブに​渡す​ことで、​Formの​値を​Storeに​反映させている。

さて、​送信時の​型を​どのように​検証しているのだろうか。

https://github.com/ryoppippi/sveltekit-form-examples/blob/82bd6695798b027c3b7abfd052092e8793144066/src/routes/superforms/%2Bpage.server.js#L16-L24

ここでは​action内で​先ほど​定義した​Schemaから​Validatorを​生成し、​Formの​値を​検証している。 そして、​検証に​失敗した​場合はfail関数を​呼び出し、​失敗した​ことを​Clientに​伝えている。 もし成功した​場合は、​そのまま​サーバーで​処理を​行い、​結果を​渡している。

駆け足で​解説したが、​このように​Superformsを​用いる​ことで、​SvelteKitの​開発体験を​損なう​ことなく、​Formの​型安全を​保証する​ことができる。 もちろんProgressive Enhancementも​達成できるので、​JavaScriptが​なくても​動作する。

ちなみに​この​Superformsは​先日のSvelteHackで​見事Best Library賞に​輝いていた。

まとめ

本記事では、​SvelteKitに​おける​Progressive Enhancement、​特に​Formに​おいて​それを​いかに​達成しているかに​ついて​解説した。 また、​SvelteKitに​おける​型安全、​また​それを​強化する​ライブラリである​Superformsに​ついても​解説した。 この​記事が​皆様の​よきSveltekit Lifeを​送る​お手伝いに​なれば​幸いである。

comment on bluesky / twitter
CC BY-NC-SA 4.0 2022-PRESENT © ryoppippi