Svelte / SvelteKit メモ
作業中のメモ
+layout.server.js
や +page.server.js
などで複数の load
関数がある場合、+page.svelte
ではそれぞれが結合されたデータを受け取るっぽい
...
return {
items
}
}
...
return {
params
}
}
<script>
export let data;
console.log(data);
</script>
{
items: [...],
params: { slug: 'hoge' }
}
同じ名前のキーがあった場合、後の方で上書き?優先?されるっぽい
...
const items = 'foo'
return {
items
}
}
{
items: 'foo'
params: { slug: 'hoge }
}
複数の
load
関数が同じキーを持つデータを返した場合、最後に返したものが'勝ちます'。レイアウトのload
が{ a: 1, b: 2 }
を返し、ページのload
が{ b: 3, c: 4 }
を返すと、結果は{ a: 1, b: 3, c: 4 }
となります。
Loading data • Docs • SvelteKit https://kit.svelte.jp/docs/load#layout-data
+page.js
のload
関数が、直接の親だけなく、両方のレイアウトのload
関数からマージされたたデータを受け取っていることにご注意ください。
Loading data • Docs • SvelteKit https://kit.svelte.jp/docs/load#using-parent-data
親のデータを明示的に呼び出すこともできるのね
load
関数が親のload
関数からのデータにアクセスできたら便利なことがあるでしょう。await parent()
でこれを行うことができます:
Loading data • Docs • SvelteKit https://kit.svelte.jp/docs/load#using-parent-data
Tailwind CSS を追加する
npx svelte-add@latest tailwindcss
svelte-add/tailwindcss: Add Tailwind CSS to your Svelte project https://github.com/svelte-add/tailwindcss
Tailwind CSS を追加する
https://zenn.dev/link/comments/95b5122a2845e7
daisyUI を追加する
npm i daisyui
module.exports = {
//...
plugins: [require("daisyui")],
}
Install daisyUI as a Tailwind CSS plugin — Tailwind CSS Components https://daisyui.com/docs/install/
daisyUI / Tailwind CSS メモ https://zenn.dev/tenkao/scraps/40974b45d8c762
サンプル
SvelteKit + Firebaseでリアルタイムクイズ大会開催システムを作った話【前編】 - GREE DX Tech Blog
SvelteKit + Prisma + Supabaseで開発できる環境をセットアップする - Qiita
Use Supabase with SvelteKit | Supabase Docs
Build a User Management App with SvelteKit | Supabase Docs
Adding Database Access to your SvelteKit App with Prisma
SvelteKit+Superforms+Prisma+Luciaでログイン機能を爆速で実装する
【SvelteKit入門】SvelteKit + Prismaによる掲示板アプリ作成 - RAKUS Developers Blog | ラクス エンジニアブログ
Become a Full Stack Developer with Svelte, Postgres, Vercel, and Gitpod
Learn Full Stack Development with Next.js and Supabase by Building a Twitter Clone.
Learn SvelteKit and Tailwind CSS by Building a Web Portfolio
以下のような {#await ...}
ブロックの {:catch error}
表示を確認したい場合
<script>
const func = async () => {
await fetch(`/hoge`)
.then((res) => res.json())
}
let promise = func()
const handleClick = () => {
promise = func()
}
</script>
{#await promise}
<div>読み込み中…</div>
{:then value}
<div>読み込み完了</div>
{:catch error}
<div>読み込み失敗 {error.message}</div>
{/await}
<button on:click={handleClick}>もっと見る</button>
fetch
をオーバーライドしてエラーをスローするダミー関数を使用するとよい、と Chat-GPT さんが教えてくれた
window.fetch = () => {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Dummy fetch error'))
}, 1000)
})
}
このままだと ReferenceError: window is not defined
エラーになるので、browser
を使用してブラウザの場合のみ実行するようにする
<script>
import { browser } from '$app/environment'
if (browser) {
window.fetch = () => {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Dummy fetch error'))
}, 1000)
})
}
}
...
done 🚨
{#await ...} - Logic blocks • Docs • Svelte
Await blocks • Svelte Examples
Logic / Await blocks • Svelte Tutorial
document
やwindow
に依存しているクライアントサイドオンリーなライブラリはどう使えばよいですか?
FAQ • SvelteKit https://kit.svelte.jp/faq#how-do-i-use-a-client-side-only-library-that-depends-on-document-or-window
await ブロックを使用すると、Promise が取りうる 3 つの状態(pending(保留中)、fulfilled(成功)、rejected(失敗))に分岐できます。SSR モードでは、サーバー上では pending の状態だけがレンダリングされます。
https://svelte.jp/docs/logic-blocks#await
実行前の状態も出し分けしたい場合、{#await}
だけじゃダメってことかな?
保留中だけ <button disabled> ... </button>
にしたい、とか
Svelte / SvelteKit の練習に TMDB API を使ったサイトをつくってみた
Anime Movie Xplorer
あと Tailwind CSS と今どきの(?)CSS レイアウトの練習に
- Tailwind CSS なるほど便利
-
float
レイアウトバリバリ世代の浦島太郎状態なのでflex
とgrid
まだまだ難しいけどとても便利
Lighthouse
デスクトップ
モバイル
Stark WCAG Audit
Violations は色のコントラストが足りないのが2件と、残りはリンクが target=_blank
になってるのにユーザーに通知がない
後で対応方法を調べる
↑ 参考)
Stark: The suite of integrated accessibility tools
Todo)
取得するデータに外れ値(?)が時々含まれるからそれを非表示にするような処理を入れるGoogle Analytics いれるVersel Analytics 入れる画像をフェードインする画像をレスポンシブにする- WAI-ARIA 対応
- 機能追加
- ページング
- 公開年月で絞り込み
- タイトルで検索
- (ダークモード)
Vercel Analytics の始め方
Vercel Web Analytics Quickstart | Vercel Docs
開発環境を判定する
How to differentiate between Svelte dev mode and build mode? - Stack Overflow https://stackoverflow.com/questions/64245188/how-to-differentiate-between-svelte-dev-mode-and-build-mode
import { dev } from '$app/environment';
if (dev) {
//do in dev mode
}
dev
Whether the dev server is running. This is not guaranteed to correspond toNODE_ENV
orMODE
.
Modules • Docs • SvelteKit https://kit.svelte.jp/docs/modules#$app-environment-dev
GA トラッキング用のスクリプトを開発環境では読み込まないようにする
<script>
import { dev } from '$app/environment'
</script>
<svelte:head>
{#if !dev}
<!-- gtag 用のスクリプト -->
{/if}
</svelte:head>
要素追加時にフェードインして表示する
-
svelte/action
を使用して、要素が追加された際に関数を実行する-
https://svelte.jp/docs/svelte-action
-
action は、要素が作成されたときに呼び出される関数です
-
-
https://svelte.jp/docs/svelte-action
- CSS の
transition-property: opacity;
を使用して、要素が追加された際に透明度を変化させる- Tailwind CSS の場合
-
transition-opacity
-
opacity-0
,opacity-100
-
- Tailwind CSS の場合
-
svelte/transition
は状態の変化をトリガーにするので要素の追加時には使えないっぽい
<script>
...
// 100ms 後、class 属性値に .opacity-100 を追加する
const fadeIn = (node) => {
setTimeout(() => {
node.classList.add('opacity-100')
}, 100)
}
</script>
...
<!-- 追加される要素 -->
<div use:fadeIn class="opacity-0 transition-opacity duration-500">
...
</div>
...
例)
Anime Movie Xplorer
表示するデータが変わる場合(?)は transition
でいけるっぽい
(CSR な画面遷移時とか)
<script>
import { fade } from 'svelte/transition'
</script>
<ul>
<!-- key (item.id) が必要 -->
{#each items as item (item.id)}
<li>{item.name} x {item.qty}</li>
{/each}
</ul>
Logic blocks • Docs • Svelte https://svelte.jp/docs/logic-blocks#each
key の式(各リストアイテムを一意に識別できる必要があります)が与えられた場合、Svelte は、データが変化したときに(末尾にアイテムを追加したり削除するのではなく)その key を使用してリストの差分を取ります。key はどんなオブジェクトでもよいですが、そのオブジェクト自体が変更されたときに同一性を維持できるようにするため、文字列か数値をお勧めします。
例)
2023年11月公開のアニメ映画 - Anime Movie Xplorer
Svelte と関係無いけど)
画像をレスポンシブにする
-
min-width: 1280px
の場合、幅 200px で表示 - それ以下の場合、幅 160px で表示
- 外部 Web API の画像を使用しているため、画像の幅はそれぞれ 342px, 500px
<img
srcset="
w342.png 320w,
w500.png 400w"
sizes="
(max-width: 1279px) 160px,
200px"
src="w342.png"
alt=""
/>
とりあえず ↑ で想定通りの表示になったけど、サイズの指定とかが今ひとつよく分からない… 🤔
画面解像度も影響するのかな?
今回は <img srcset="" 〜>
使ってみたけど、<picture>
の方が楽だったりするのかしら
参考)
レスポンシブ画像 - ウェブ開発を学ぶ | MDN
imgタグのsrcset・sizes属性とpictureタグの使い方 - レスポンシブイメージで画像表示を最適化 - ICS MEDIA
サイト名など各階層・ページで共通して使用する文字列を設定する
export const load = () => {
return {
siteTitle: 'Hoge',
fuga: 'Fuga'
}
}
<script>
export let data
</script>
<svelte:head>
<title>{data.siteTitle}</title>
</svelte:head>
{data.fuga}
Loading data • Docs • SvelteKit https://kit.svelte.jp/docs/load#layout-data
input
の pattern
属性を使う場合
<form>
<input type="month" pattern="[0-9]{4}-[0-9]{2}" />
</form>
このままだと { }
が JS の式と見做され(?)、属性値が [0-9]4-[0-9]2
となってしまい正しく動作しない
- 参照文字を使用する
{
}
→{
}
-
String.raw()
を使用する
<!-- 参照文字 -->
<form>
<input type="month" pattern="[0-9]{4}-[0-9]{2}" />
</form>
<!-- String.raw() -->
<form>
<input type="month" pattern={String.raw`[0-9]{4}-[0-9]{2}`} />
</form>
Input does not validate correcetly according to pattern when the pattern contains numbers · Issue #2130 · sveltejs/svelte https://github.com/sveltejs/svelte/issues/2130
HTML 属性: pattern - HTML: ハイパーテキストマークアップ言語 | MDN https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/pattern
String.raw() - JavaScript | MDN https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/raw
Safari (macOS) と Firefox は <input type="month">
に対応しないのかしら 🤔
<input type="month"> - HTML: ハイパーテキストマークアップ言語 | MDN
リンクの処理を CSR ではなく通常の画面遷移にしたい場合、a
要素に data-sveltekit-reload
属性を付与する
(form[method=get]
要素も同様)
<a href="/" data-sveltekit-reload>リンク</a>
<form action="/" method="get" data-sveltekit-reload>
...
</form>
Link options • Docs • SvelteKit https://kit.svelte.jp/docs/link-options#data-sveltekit-reload
data-sveltekit-reload
時には、SvelteKit にリンクを処理させないで、ブラウザに処理をさせる必要があります。
data-sveltekit-reload
属性をリンクに追加すると…
…リンクがクリックされたときにフルページナビゲーションが発生します。
rel="external"
属性があるリンクも同様に扱われます。
Glossary • Docs • SvelteKit https://kit.svelte.jp/docs/glossary#routing
ルーティング
デフォルトでは、(リンクをクリックしたりブラウザの 進む または 戻る ボタンを使って)新しいページにナビゲートするとき、SvelteKit はナビゲーションをインターセプトし、ブラウザが移動先のページのリクエストをサーバーにリクエストする代わりに、それを処理します。
...
このような、ナビゲーションが行われる際にそれに応じてクライアント側でページを更新するプロセスのことを、クライアントサイドルーティングと呼びます。SvelteKit では、デフォルトでクライアントサイドルーティングが使用されますが、data-sveltekit-reload でこれをスキップすることができます。
Form actions • Docs • SvelteKit https://kit.svelte.jp/docs/form-actions#get-vs-post
GET vs POST
サーバーにデータを
POST
する必要がないフォームもあるでしょう — 例えば検索入力(search inputs)です。これに対応するにはmethod="GET"
(または、method
を全く書かないのも同等です) を使うことができ、そして SvelteKit はそれを<a>
要素のように扱い、フルページナビゲーションの代わりにクライアントサイドルーターを使用します。
<a>
要素と同じように、data-sveltekit-reload
属性、data-sveltekit-replacestate
属性、data-sveltekit-keepfocus
属性、data-sveltekit-noscroll
属性を<form>
に設定することができ、ルーターの挙動をコントロールすることができます。
ある HTML 要素が閲覧中のブラウザで対応してるか確認して出し分けする
(例: <input type="month">
)
<script>
const isInputTypeMonthSupported = (() => {
const input = document.createElement('input')
input.setAttribute('type', 'month')
return input.type !== 'text'
})()
</script>
<form>
{#if isInputTypeMonthSupported}
<!-- 対応してる場合 -->
<input type="month">
{:else}
<!-- 対応してない場合 -->
<input type="text">
{/if}
</form>
document
はブラウザ限定なので SSR を無効にする
(サーバーで実行されると ReferenceError: document is not defined
になる)
export const ssr = false
<input type="month"> - HTML: ハイパーテキストマークアップ言語 | MDN https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/month#javascript
機能検出の実装 - ウェブ開発を学ぶ | MDN https://developer.mozilla.org/ja/docs/Learn/Tools_and_testing/Cross_browser_testing/Feature_detection
Detecting HTML5 Features - Dive Into HTML5 https://diveinto.html5doctor.com/detect.html#input-types
server side rendering - SvelteKit: disable SSR - Stack Overflow https://stackoverflow.com/questions/72251017/sveltekit-disable-ssr
Page options • Docs • SvelteKit https://kit.svelte.jp/docs/page-options#ssr
通常、SvelteKit ではページを最初にサーバーでレンダリングし、その HTML をクライアントに送信してハイドレーションを行います。もし
ssr
をfalse
に設定した場合、代わりに空の 'shell' ページがレンダリングされます。これはページがサーバーでレンダリングできない場合には便利 (例えばdocument
などのブラウザオンリーな globals を使用するなど) ですが、ほとんどの状況では推奨されません (appendix をご参照ください)。
ある HTML 要素が閲覧中のブラウザで対応してるか確認して出し分けする Ver.2
(例: <input type="month">)
<script>
import { browser } from '$app/environment'
let isInputTypeMonthSupported = true
if (browser) {
const input = document.createElement('input')
input.setAttribute('type', 'month')
isInputTypeMonthSupported = input.type !== 'text'
}
</script>
<form>
{#if isInputTypeMonthSupported}
<!-- 対応してる場合 -->
<input type="month">
{:else}
<!-- 対応してない場合 -->
<input type="text">
{/if}
</form>
- 初期値を設定し、サーバーで実行してもエラーにならないようにする(
let isInputTypeMonthSupported = true
) -
browser
を使用し、ブラウザの場合のみ対応してるか確認するようにする-
onMount()
でもいいっぽい?
-
Modules • Docs • SvelteKit https://kit.svelte.jp/docs/modules#$app-environment-browser
true
if the app is running in the browser.
svelte • Docs • Svelte https://svelte.jp/docs/svelte#onmount
onMount
関数は、コンポーネントが DOM にマウントされるとすぐに実行されるコールバックをスケジュールします
+layout.svelte
の <svelte:head>
に <title>
を入れている状態で、
下層の +page.svelte
の <svelte:head>
に <title>
があるページから無いページに遷移した際、タイトルが変わらなかった(あるページのタイトルのまま)
無いページにも入れることで解決した
レスポンスのヘッダーを設定する
(例: Cache-Control
)
export const load = ({ setHeaders }) => {
setHeaders({
'cache-control': 'public, s-maxage=3600',
})
return hoge
}
- Loading data • Docs • SvelteKit https://kit.svelte.jp/docs/load#headers
server
load
関数と universalload
関数はどちらもsetHeaders
関数にアクセスでき、サーバー上で実行している場合、 レスポンスにヘッダーを設定できます (ブラウザで実行している場合、setHeaders
には何の効果もありません)。これは、ページをキャッシュさせる場合に便利です
同じヘッダーを複数回設定することは (たとえ別々の
load
関数であっても) エラーとなります。指定したヘッダーを設定できるのは一度だけです。
例えば複数の +page.server.js
や +layout.server.js
で同じ設定をしていると Error: "cache-control" header is already set
になった
(ただし、 api/hoge/+server.js
の GET()
内だとエラーにならなかった。load
と GET
だから?)
- Web standards • Docs • SvelteKit https://kit.svelte.jp/docs/web-standards#fetch-apis-headers
Headers
インターフェイスでは、受け取ったrequest.headers
を読み取り、送信するresponse.headers
をセットすることができます。
- Cache-Control - HTTP | MDN https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Cache-Control
Cache-Control
は HTTP のヘッダーフィールドで、 キャッシュ をブラウザーや共有キャッシュ(プロキシーや CDN など)において制御するためのディレクティブ (指示) を、リクエストとレスポンスの両方で保持します。
- Cache-Controlとは? | キャッシュの概要 | Cloudflare https://www.cloudflare.com/ja-jp/learning/cdn/glossary/what-is-cache-control/
Cache-Controlとは、ブラウザーのキャッシュ動作を管理するHTTPヘッダーのことです。
簡単に言えば、誰かがWebサイトを訪問すると、ブラウザーはキャッシュと呼ばれるストアに画像やWebサイトデータといった特定のリソースを保存します。そのユーザーが同じWebサイトを再度訪問すると、Cache-Controlは、そのユーザーにローカルキャッシュからリソースを読み込ませるか、またはブラウザーがサーバーに新しいリソースを要求する必要があるかを決めるルールを設定します。
Cache-Controlをより深く理解するには、ブラウザキャッシュおよびHTTPヘッダーの基本を理解する必要があります。
Vercel にホストしてる本番サイトだと、ドキュメントのレスポンスは Cache-Control: public, max-age=0, must-revalidate
になってる
でも Fetch で受け取ってる __data.json
は Cache-Control: private, no-store
CSR で表示してるページ(データ)はキャッシュしないってことなのかな? 🤔
400, 404, 500 エラーの場合の画面を追加する
- Errors • Docs • SvelteKit https://kit.svelte.jp/docs/errors
- ルーティング • Docs • SvelteKit https://kit.svelte.jp/docs/routing#error
- HTTP レスポンスステータスコード - HTTP | MDN https://developer.mozilla.org/ja/docs/Web/HTTP/Status
エラーページをカスタマイズする
-
+error.svelte
を追加する
エラーをスローする
import { error } from '@sveltejs/kit'
if (hoge) {
throw error(400, 'Bad Request')
}
DOM の状態を一時的に保持・復元する
(例: 値とスクロール位置)
<script>
let hoge = ''
export const snapshot = {
capture: () => {
return {
hoge,
scrollPosition: window.scrollY,
}
},
restore: (value) => {
hoge = value.hoge
setTimeout(() => {
window.scroll(0, value.scrollPosition)
}, 0)
}
</script>
例えばサイドバーのスクロールポジションや、
<input>
要素の中身などの、一時的な DOM の状態(state)は、あるページから別のページに移動するときに破棄されます。例えば、ユーザーがフォームに入力し、それを送信する前にリンクをクリックして、それからブラウザの戻るボタンを押した場合、フォームに入力されていた値は失われます。入力内容を保持しておくことが重要な場合、DOM の状態を スナップショット(snapshot) として記録することができ、ユーザーが戻ってきたときに復元することができます。
Snapshots • Docs • SvelteKit https://kit.svelte.jp/docs/snapshots
SvelteKit では Snapshot という機能を使用してページの現在の状態を保存できるようになっています。
これを利用すると、無限スクロールで実装された検索ページから他のページに移動したあとで戻るボタンを押してページを戻ってきたときに前回と同じスクロール位置に表示を戻せます。
Svelte で無限スクロールを実装する https://zenn.dev/labbase/articles/6e97ee67c958e0#ページ遷移について