フューチャー技術ブログ

SvelteKitのデフォルトプロジェクトから学ぶ

ウェブアプリケーションフレームワークとして最近注目度が少しずつ上がっているのがSvelteです。以前、Svelteをちょびっとサンプルを触ってみた感じ、コードの雰囲気は<script setup> 版のVue 3っぽいなー、という感じです。あとはプロパティ入力のあるコンポーネントでHTMLを生成するという基本構成はReactもAngularもみんなそうなので、今時のウェブフロントエンドのフレームワークを触ったことがあれば難しくはない気がします。

1コンポーネントでおさまる範囲ではちょっと前にかんたんなアプリの試作をしてみました(毎週何回も使ってるので手間の割に活躍してます)が、じゃあ、一本分のアプリを作るときはどうなんだ、ということでSvelteKitでプロジェクトを新規で作ってみたけど、いろいろな未知の要素がでてきて、これはどうなんだ? というのをドキュメントから探す、という学び方もまあ悪くないな、ということでブログにしてみました。

ReactにNext.jsがあれば、VueにはNuxt.js、SvelteにはSvelteKitがあります。フロントエンドのフレームワークを拡張して、初回レンダリングをサーバーで行うサーバーサイドレンダリングなどの自分で環境を作ると不便なものが組み込まれていて、さらにサーバー側のAPI実装も同じフレームワーク内でサポートするなどの付加機能も提供してくれているものです。サーバーがNode.jsや、Node.jsベースのPaaSを使えば、JavaScriptだけでフロントもサーバーも完結します。

プロジェクトは次のコマンドで作っていきます。最初のコマンドでいろいろ聞かれるので、好きな条件を入れていきます。デモプロジェクトはYESにするといろいろなコードが生成されます。今回はこれを見ていきます。それ以外は全部YESにしてみました。

$ npm init svelte@next my-app

Welcome to SvelteKit!

This is beta software; expect bugs and missing features.

Problems? Open an issue on https://github.com/sveltejs/kit/issues if none exists already.

✔ Which Svelte app template? › SvelteKit demo app
✔ Use TypeScript? … No / Yes
✔ Add ESLint for code linting? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
✔ Add Playwright for browser testing? … No / Yes

(以下略)

$ cd my-app
$ npm install
$ npm run dev -- --open

サンプルプロジェクトのページ構成

サンプルプロジェクトは3つのページがあります。静的なAbout以外に、よくあるカウンターと、ToDoがあります。カウンターはSvelte単体でも実現できるような内容で、ToDoはウェブサービスアクセスを伴うサンプルです。

カウンターのサンプル TODOのサンプル

ページ周りのコードを抜き出してきたのがこれです。

├── src
│ ├── app.css
│ ├── app.html
│ └── routes
│ ├── __layout.svelte
│ ├── about.svelte
│ ├── index.svelte
│ └── todos
│ ├── _api.ts
│ ├── index.svelte
│ └── index.ts

ぱっと見て想像できるルールはこんな感じです。

src/routesの階層がURLになりそう

Next.jsのpagesみたいな感じのようです。Routingページを見て答え合わせすると、やはりこのファイルシステムがそのままURLになるよ、と書いてあります。便利ですよねこれ。

src/routes/__layout.svelteも共通部分を書きそう

おそらくこれはきっとどのページでも今日で使われるヘッダーとかフッターとかを書きそうで、.svelteだからきっと動的なコンポーネントも使えそうな気がします。

Layoutsを見て答え合わせをすると、確かにこのようです。書くページのコンテンツは、このコンポーネントの<slot>の中に表示されるとのこと。

<slot></slot>

複数階層にしてネストしてレイアウトを設定して使ったりもできて、同じフォルダで同じブレッドクラムを表示させたりというのもできるみたいですね。

面白かったのは、名前付きレイアウトで、src/routes/__layout-foo.svelteという名前のレイアウトを作っておいて、実際に作られるページのファイル名がsrc/routes/my-special-page@foo.svelteだとすると、この特別なfooレイアウトが使われるとのこと。複雑な継承とかもできるようです。

あと、このレイアウトのページにあった注目内容は__error.svleteですね。これでページが見つからなかったときのエラーページが設定できるようです。

階層構造のサポートはSvelteのRouter機能のポイントらしく、レイアウトとかエラーページとかは特定のフォルダ内でのみに適用とかができるみたいです。

app.htmlが最終的に作られるアプリケーションの枠組みっぽい

__layout.svelteと違い、きっと静的な共通要素、例えばmetaタグとかはここに書くんだろうと思われます。しかし、これに関する直接的な解説はドキュメントにはありません。ドキュメントの中に書かれているapp.htmlに関する要素は、2つだけです。

Sapper側のドキュメントと合わせて読めば意味が理解できますね:

サーバーから返されるレスポンスのテンプレートとして使われるファイル。Svelteは次のタグをそれぞれの内容に置き換える:

  • %svelte.head% — ページ固有の<title>などの<head>に置かれるHTMLに置き換えられる
  • %svelte.body% — SvelteがレンダリングするボディのHTMLに置き換えられる

API周り

SvelteじゃなくてSvelteKitを選びたいニーズとしては主にサーバーもTypeScriptやJavaScriptも書きたいというのがあると思います。それ以外にもすでに説明したrouter周りで楽がしたい、静的コンテンツ生成に使いたい、というのもあると思いますが、ここではサーバーAPI提供側のコードを見ていきます。

API周りは以下のコードのようですね。src/routes/todos/index.svelteは/todosでアクセスしたときに表示されるページのコンテンツなので、index.tsがハンドラー定義のファイルみたいですね。_api.tsは名前からして共通コード置き場でrouterからは無視されそうな雰囲気。

├── src
│ └── routes
│ └── todos
│ ├── _api.ts
│ ├── index.svelte
│ └── index.ts

Next.jsはフォルダ構成をガッチリ決めることでAPIとHTMLを分けていましたが、ミックスできるのは便利ですね。でもこれだと、/todosでHTMLを要求するアクセスされたときと、APIのGETの区別が大変そうですね。ドキュメントを見ていきます。

Endpointsのドキュメントによると、.tsでエンドポイントにできることが書かれていますね。その中で、getとかpostという名前で関数を作ってあげるとエンドポイントになるとのこと。それ以外にも、post, put, patch, del(deleteは予約語なのでdel)に対応するとのことです。

export const get: RequestHandler = async () => {
}

ただ、これだとWebページのコンテンツとAPIの区別がつかないので、明治的にaccept: application/jsonをリクエストにつけるか、__data.jsonというのをリクエスト側で付与することでJSONのAPIの方を明示的に要求するらしい。確かに、サンプルコードの動きを見ると、__data.jsonがついていますね。

API

アンダースコアで除外できることはプライベートモジュールで説明されていました。ピリオドもプライベート扱い(.well-knownを除く)とのこと。

メソッドオーバーライド

動かしてみて、おっと思ったのが、_method=DELETEというところですね。HTTP的にはメソッドはたくさんありますが、JavaScriptを使わずにHTTPのフォームを使って送れるのはGETとPOSTのみです。そこで、POSTにいろいろなメソッドも振る舞わせるというメソッドオーバーライドというのがあります。

メソッドオーバーライド

設定を見たときに、メソッドオーバーライドという項目があるのに気づきました。

svelte.config.js
const config = {
preprocess: preprocess(),
kit: {
adapter: adapter(),
methodOverride: {
allowed: ['PATCH', 'DELETE']
}
}
};

メソッドオーバーライドのドキュメントにいろいろ書かれています。フロント側でがんばらなくてもできるようにする配慮があるのはいいですね。

フォームのパース

サンプルを見ると、requestのメソッドを使うことで、フロントから渡されるリクエストを処理できるみたいですね。

src/routes/todos/index.ts
export const post: RequestHandler = async ({ request, locals }) => {
const form = await request.formData();

await api('post', `todos/${locals.userid}`, {
text: form.get('text')
});

return {};
};

このリクエストオブジェクトはボディのパースのドキュメントのリンクを見る限り、ブラウザのAPIと同じっぽい。

サンプルのAPI実装のバックエンドの中身

サーバーコード側の実装を見てみたら、fetchでsvelteが提供しているサーバーにリクエストを飛ばしているっぽいですね。サンプル用にサーバー維持するのすごい。たしかにストレージ周りだとSQLにしてもMongoDBなどにしても、SvelteKitの書き方を伝えたい、というニーズ以上のさまざまな前提知識が発生しがちなので、この割り切りは理解できます。

/src/routes/todos/_api.ts
const base = 'https://api.svelte.dev';
import type { RequestHandler } from '@sveltejs/kit';

export function api(method: string, resource: string, data?: Record<string, unknown>) {
return fetch(`${base}/${resource}`, {
method,
headers: {
'content-type': 'application/json'
},
body: data && JSON.stringify(data)
});
}

NeDBみたいなのでもいいのに、と思ったら、NeDBはもうメンテナンス中止していたんですね。残念。

フック

ソースフォルダの中にhooks.tsという気になるファイルがありました。中を見ると、CookieからユーザーIDを取り出し、なければUUIDを生成してevent.locals.useridに格納しています。ウェブアプリケーションフレームワークに頻出するミドルウェアと近そうです。

├── src
│ └── hooks.ts

フックのドキュメントを見ると、このフックのようにサーバーへのリクエストをちょっと加工する以外に、カスタムコンテンツをフックで返してしまうとか、外部サーバーへのリクエストを加工するなど、いろいろなことができるみたいです。

src/hooks.ts
response.headers.set(
'set-cookie',
cookie.serialize('userid', event.locals.userid, {
path: '/',
httpOnly: true
})
);

Cookieへの書き込みは、ヘッダーに直接入れていますが、ドキュメントでもそうなっていますね。面白いですね。

それ以外の要素

生成されたコードにはないがドキュメントにある項目は以下の通りです。あとはこのあたりをピックアップして読んでみたら、SvelteKitの機能をざっと掴むには良いかなと思いました。

あとは、デプロイ時の環境ごとの違いはアダプターというものにまとめられているので、何かしらのアダプターについては学ぶことになるかと思います。

(補足)Playwright

SvelteKitのプロジェクト作成ではJestとかVitestのような普通のテスティングフレームワークではなくて、E2EのPlaywrightの生成のみに対応しています。

ですが、TypeScriptを使うよオプションと、Playwrightを同時に有効にするとエラーになってしまいました。Playwrightのマニュアルに従って事前にビルドしてからテストを実行するようにしたら修正できました。

tests/tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ES2015",
"moduleResolution": "Node",
"sourceMap": true,
"outDir": "../tests-out"
}
}
package.json
"pretest": "tsc --incremental -p tests/tsconfig.json",
"test": "playwright test -c tests-out",

TypeScript周りはいろいろIssueがどんどん修正されているので、もうちょっと新しいバージョンなら問題なくなるんじゃないかと思います。Issueを見ると、PlaywrightじゃないUnit Testについて はvitest側のsveltekit対応の改善待ちステータスのようです。

まとめ

SvelteKitを学ぼうと思ったけど、チュートリアル的なコンテンツがなく、上からドキュメント読むのもいいけど手っ取り早く概要を掴もうと思って、デフォルトプロジェクトのコードリーディングなどをしつつ、ドキュメントをつまみ食いするスタイルで学習してみました。

あと、書き終わってから気づいたのですが、日本語訳されたドキュメントもありましたので、日本語なら早く読めるぞという方はドキュメントを先に読むのでもいいかもしれません。

全体的に、コードを見ると動きが想像できそうなものが多いというか、Next.jsとか類似ソリューションに近いというか、あまり奇をてらったところがない感じがします。あと、Cookieの設定だったり、メソッドオーバーライドだったり、サーバーで使われるReqestがブラウザのそれと同じだったり、既存のウェブ周りの情報がある人には慣れ親しんでいる方法を選択してくれている感じは気に入りました。

テスト周りの対応を見るとまだまだ若い感じが伝わってきますし、今までReact/Vueでやってきたようなことをいきなり全部実現というのは少し手間暇があるかもしれませんが、活発に改善されていっているので、SvelteKitをじっくり触りながら変化を感じるのも楽しいと思います。