Svelte学習帳
やっていく
以下のコードブロックはハイライトがsvelteファイルに対応していないぽいのでhtmlまたはjsxで代用
Introduction
- とりあえず
App.svelte
にhtmlを書いていけばコンポーネントになる -
{var}
でhtml内に変数を書き出せる- シングルマスタッシュなんだ まあJSXも同じか
- この中はJSが解釈されるっぽい メソッドも書ける
<script>
const name = "Svelte";
</script>
<h1>Hello {name.toUpperCase()}!</h1>
- 属性の名前と変数名が同じ場合は省略可能
<script>
const src = './image.gif';
const alt = 'alt text';
</script>
<img {src} {alt} />
<!-- <img src={src} alt={alt} /> と書かなくて良い -->
- 別ファイルのコンポーネントを使う場合はscriptタグでimportする
- カスタムタグ名はパスカルケース
- svelteファイル内に書いたスタイルはそのファイル内でのみ適用される
<p>This is a sub component.</p>
<style>
p {
color: red;
}
</style>
<script>
import Sub from './Sub.svelte';
</script>
<p>This is a main component.</p>
<Sub />
<style>
p {
color: blue;
}
</style>
- 変数内のhtmlを直接展開したいときは
{@html var}
にする- これは覚えてなくてもいいや
Reactivity 1
$state
rune を使うことで変数をリアクティブにできる
以下のコードはボタンをクリックするとカウントアップするコード
<script>
let count = $state(0);
function increment() {
count ++;
}
</script>
<button onclick={increment}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
onclickに無名関数を突っ込んでも動いた
<script>
let count = $state(0);
</script>
<button onclick={(()=>count++)}>
Clicked {count}
{count === 1 ? 'time' : 'times'}
</button>
配列の値の変更も可能
<script>
let numbers = $state([1, 2, 3, 4]);
function addNumber() {
numbers.push(numbers.length + 1);
}
</script>
<p>{numbers.join(' + ')} = {numbers.reduce((t, n) => t + n, 0)}</p>
<button onclick={addNumber}>
Add a number
</button>
変数が別の$state
変数を参照している場合、参照先の更新を監視するためには$derived
rune を使う必要がある
以下ではnumbers
の更新に伴ってtotal
も更新されるが、$derived
を$state
にしたり外したりすると値が更新されない
<script>
let numbers = $state([1, 2, 3, 4]);
+ let total = $derived(numbers.reduce((t, n) => t + n, 0));
function addNumber() {
numbers.push(numbers.length + 1);
}
</script>
-<p>{numbers.join(' + ')} = {numbers.reduce((t, n) => t + n, 0)}</p>
+<p>{numbers.join(' + ')} = {total}</p>
<button onclick={addNumber}>
Add a number
</button>
Reactivity 2
リアクティブな値をconsole.log(stateValue)
しようとすると警告が出る
値がProxy https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy になっているため…らしいがよくわからない
$state.snapshot(stateValue)
することで警告を消せる たぶんdeep copyしている
または$inspect(stateValue)
というコードで値が更新されるたびにエコーすることができる これはproduction build時に消されるとのことなのでこっちを使うのが良さそう
$inspect(stateValue).with(console.trace)
するとより詳細な情報を出力できる
変数の変更時にどのように処理を行うかの$effect
を自作することができる
以下はコンポーネントマウントからの経過時間(elapsed
)を表示するコード
effect関数はcleanup関数を返すことができ、これは状態の更新時およびコンポーネント破棄時に実行される
<script>
let elapsed = $state(0);
let interval = $state(1000);
$effect(() => {
const id = setInterval(() => {
elapsed += 1;
}, interval);
// これがないと古いsetIntervalが動き続ける
return () => {
clearInterval(id);
};
});
</script>
<button onclick={() => interval /= 2}>speed up</button>
<button onclick={() => interval *= 2}>slow down</button>
<p>elapsed: {elapsed}</p>
<p>interval: {interval}</p>
なお上のようなコードはたぶんeffectを使わないと実現できないが、effectなしで実行できるのであれば可能な限りeffectを使わないべき
個人的にもあまり使いたくはない 副作用がめんどくさそうなので
なお、状態値もコンポーネントのようにexport
/ import
して使うことができるが、拡張子は.svelte
または.svelte.js
/ .svelte.ts
である必要がある
Props
属性値として渡された値を引き出すには$props()
rune を使用する
<script>
import Nested from './Nested.svelte';
</script>
<Nested text={"number"} answer={42} />
<Nested text={"default"} />
<script>
let { text, answer = "OK" } = $props();
</script>
<p>The {text} is {answer}</p>
The number is 42
The default is OK
通常のhtmlの属性値と同じく、spread operatorで省略が可能
<script>
import Nested from './Nested.svelte';
const attrs = { name: 'foo', anser: 'bar' };
</script>
<Nested {...attrs} />
Logic
論理構造を表現できる
-
{# ...}
: 開始 -
{: ...}
: 継続 -
{/ ...}
: 終了
if
- jsと異なり、ifの後の条件をカッコで囲む必要はない
<script>
let count = $state(0);
</script>
<button onclick={count++}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
{#if count > 5}
<p>{count} is greater than 5</p>
{:else if count < 2}
<p>{count} is less than 2</p>
{:else}
<p>{count} is between 2 and 5</p>
{/if}
each
- array-likeオブジェクトならeachで回すことができる
- インデックスを取ることも可能
<script>
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
</script>
{#each colors as color, i}
<div style="background: {color}">{i+1} {color}</div>
{/each}
- eachする要素を削除する場合はkeyed loopにするのが安全
- 具体的な状況を理解できていないのでここは勉強した記録を残すに留める
{#each things as thing (thing.id)}
<Thing name={thing.name} />
{/each}
await
-
{#await ...}
でpromiseを表現可能
<script>
import { roll } from './utils.js';
let promise = $state(roll());
</script>
<button onclick={() => promise = roll()}>
roll the dice
</button>
{#await promise}
<p>...rolling</p>
{:then number}
<p>you rolled a {number}!</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
- エラーが無いことがわかっている場合は
{#await ...}
と{:then ...}
をまとめて書ける
{#await promise then number}
<p>you rolled a {number}!</p>
{/await}
keyed loopは不要なレンダリングを避けるためにuniqueなキーを設定するやつですね。
- 他のFWでも似たような仕組みはある(https://leaysgur.github.io/posts/2023/06/14/122650/)
- staticなページじゃない限りはなるだけキーを指定するのがよさそう
- eslintでkeyを強制するルールもある(自分はonにしている)
あと、このセクションに関連してだけど、{#key ...}
blockもある
自分は使ったことないけど
わたしの経験あるところだとvueでもループ時にkeyを入れる記法があります
vueもあるのか、まあそうか
同じやつですな
Events
on<name>
でイベントハンドリングが可能
<script>
let b = $state(false);
function onclick(event) {
b = true;
}
</script>
<button {onclick}>
{b ? "clicked" : "click me"}
</button>
インラインも可
<script>
let b = $state(false);
</script>
<button onclick={(event) => {
b = true;
}}>
{b ? "clicked" : "click me"}
</button>
イベントハンドラがネストしている場合は内側から順に発火するっぽい
<div onclick={() => alert(`div clicked`)} role="presentation">
<button onclick={() => alert(`button clicked`)}>
click me
</button>
</div>
on<event>capture
を使うと逆順になる 混在している場合はcaptureを全て消化してから無captureになるみたい
これはあまり多用したくないな…
<div onclickcapture={() => alert(`div clicked`)} role="presentation">
<button onclickcapture={() => alert(`button clicked`)}>
click me
</button>
</div>
propsにインラインで関数を渡しても良い
<script>
import Stepper from './Stepper.svelte';
let value = $state(0);
</script>
<p>The current value is {value}</p>
<Stepper increment={()=>value++} decrement={()=>value--} />
<script>
let {increment, decrement} = $props();
</script>
<button onclick={decrement}>-1</button>
<button onclick={increment}>+1</button>
propsを変数に出してそれをspreadすることも可能
<script>
import MyButton from './MyButton.svelte';
function hook() {
alert("clicked");
}
</script>
<MyButton onclick={hook} />
<script>
let props = $props();
</script>
<button {...props}>Push me</button>
ただし$
関数を直接マークアップ内で使うことはできない
<button {...$props()}>Push me</button>
<!-- `$props()` can only be used at the top level of components as a variable declaration initializer -->
Bindings
原則としてデータフローは親から子に流れる
すなわちコンポーネントは自身または子の値をセットできるがその他(親兄弟)の値を書き換えることはできない
ではこのようにinputがvalueを持つ場合はどうなるか
<script>
let name = $state('world');
</script>
<input value={name} />
<h1>Hello {name}!</h1>
<input>
から見てname
は親(App)の変数なので直接変更できない
したがって原則どおりなら親がoninputを定義して変更をハンドリングすることになる
<script>
let name = $state('world');
+ function oninput(event) {
+ name = event.target.value;
+ }
</script>
-<input value={name} />
+<input value={name} {oninput} />
<h1>Hello {name}!</h1>
ただしこれは余りにも頻出なのでbindingの仕組みが整備されている
実際はbind:
を前置するだけで良い
<script>
let name = $state('world');
</script>
-<input value={name} />
+<input bind:value={name} />
<h1>Hello {name}!</h1>
変数名と同じ場合の短縮記法はマスタッシュなしで<input bind:value />
-
<input type="number" bind:value={value}>
と<input type="range" bind:value={value}>
では値を数値として扱う -
<input type="checkbox" bind:checked={value}>
では値を真偽値として扱う -
<select bind:value={selected}>...</select>
および<textarea bind:value={value}></textarea>
も使える - 複数のラジオボタンに同じ変数で
<input type="radio" value="hoge" bind:group={selected} />
とすると、複数の値の中から選んだ1つがselected
に入る - 複数のチェックボックスに同じ変数で
<input type="checkbox" value="hoge" bind:group={selected} />
とすると、選んだ複数の値を要素とする配列がselected
に入る- これは
<select multiple bind:group={selected}>...</select>
も同様
- これは
Classes and Styles
flex
という真偽値変数がtrue
の場合にclass="flex"
を付加したいとき、以下のような書き方ができる
3番目はこれまでも出てきた省略記法
<div class="{flex ? 'flex' : ''}">flex or not</div>
<div class:flex={flex}>flex or not</div>
<div class:flex>flex or not</div>
上記はclass
でなくstyle
でも同様
子コンポーネントにスタイルの値を渡したい場合、--varName
記法を用いてCSS変数を定義できる
<script>
import Box from './Box.svelte';
</script>
<Box --color="red" />
<div class="box">box</div>
<style>
.box {
background-color: var(--color, #ddd);
}
</style>
これでboxの背景がredになる
Actions
要素レベルのライフサイクル
関数を<div use:myFunc>
のように書ける
関数の第一引数は配置されたnode
ちょっとサンプルがでかいのでこの自習ノートに書けない
関数の第二引数以降はディレクティブ部分に引数として書くことができる
以下はTippy.jsを使った例
<script>
import tippy from 'tippy.js';
let content = $state('Hello!');
function tooltip(node, fn) {
$effect(() => {
const tooltip = tippy(node, fn());
return tooltip.destroy;
});
}
</script>
<input bind:value={content} />
<button use:tooltip={() => ({content})}>Hover me</button>
<style>
/* 略 */
</style>
$effect
の返り値は前述の通りcleanup関数
Transition 1
描画をいい感じに見せるTransitionが提供されている
以下は'svelte/transition'
からインポートできるfade
の例
チェックボックスのオンオフで表示がゆっくり切り替わる
<script>
import { fade } from 'svelte/transition';
let visible = $state(true);
</script>
<label>
<input type="checkbox" bind:checked={visible} />
visible
</label>
{#if visible}
<p transition:fade>Fades in and out</p>
{/if}
その他
-
transition:fly
スライドしつつ表示される -
transition:
で指定すると表示・非表示ともに適用されるが、in:
またはout:
でどちらかに限定できる
transition関数は普通のjs関数なので自作できる こんな具合である
返り値のcss
はトランジション中のスタイルの調整を行う
import { elasticOut } from 'svelte/easing';
function spin(node, { delay = 0, duration = 5000 }) {
return {
delay,
duration,
css: (t, u) => {
// t = 0 → 1, u = 1 - t
const eased = elasticOut(t);
return `
transform: scale(${eased}) rotate(${eased * 1080}deg);
`
}
};
}
nodeの情報の変更をしたい場合はtick
を用いる
以下を見ると、transition関数のtypewriter
は最初に一度呼ばれてconst text = node.textContent
が保存され、それ以降はtickによりtextContent
が変化してもtext
の値に影響しないことが見てとれる
// 簡易版
function typewriter(node, { speed = 1 }) {
const text = node.textContent;
return {
duration: text.length / (speed * 0.01),
tick: (t) => {
const i = Math.trunc(text.length * t);
node.textContent = text.slice(0, i);
}
};
}
Transition 2
以下のディレクティブでトランジションの開始・終了のフックを実行できる
onintrostart={onIntroStartFn}
onoutrostart={onOutroStartFn}
onintroend={onIntroEndFn}
onoutroend={onOutroEndFn}
リスト全体にトランジションを付ける場合は|global
を付加する
{#each items.slice(0, len) as item}
<div transition:slide|global>{item}</div>
{/each}
# svelte3では `|local` という記法だったので古いバージョンを触る場合は注意
#{key var} ... {/key}
ブロックでは変数var
が変化した場合にブロック内を再描画し、トランジションも再実行される
値だけでなくブロックを丸ごと更新したい場合に便利
Advanced Reactivity
- 通常のリアクティビティが不要な場合は
$state.raw()
を使うことができる たとえばリアルタイムで更新される要素など - 通常の値だけでなくJSのクラスのフィールドもreactiveにできる
- getter / setterを自動で認識する
- setterを使うと値のバリデーションに便利
- JS標準のデータ構造をSvelteのreactiveで使いやすくなるようラップしたsvelte/reactivityというパッケージがあるのでDateやURLを使う場合にはこちらを参照するのが良い
Reusing content
#{snippet name()} ... {/snippet}
を使用することで独立コンポーネントを定義せずとも要素を再利用できる
{#snippet greet()}
<div>Hello, world!</div>
{/snippet}
{@render greet()}
{@render greet()}
変数展開も可能
{#snippet greet(name)}
<div>Hello, {name}!</div>
{/snippet}
{@render greet("Alice")}
{@render greet("Bob")}
これまでの値のように別コンポーネントに渡すこともできる 以下の例だとあまり意味はないが…
<script>
import { Sub } from "./Sub.svelte";
</script>
{#snippet greet(name)}
<div>Hello, {name}!</div>
{/snippet}
<Sub {greet} />
<script>
const { greet } = $props();
</script>
{@render greet("Alice")}
{@render greet("Bob")}
また、コンポーネントのタグで囲むことで暗黙的にスニペットがpropsに渡される
<script>
import { Sub } from "./Sub.svelte";
</script>
<Sub>
{#snippet greet(name)}
<div>Hello, {name}!</div>
{/snippet}
</Sub>
さらに、#{snippet name()} ... {/snippet}
を省略するとchildren
という名前の特別なスニペットとしてpropsに渡される この場合は変数を使うことはできない
<script>
import { Sub } from "./Sub.svelte";
</script>
<Sub>
<div>Hello, world!</div>
</Sub>
<script>
const { children } = $props();
</script>
{@render children()}
Stores
値を管理する方法として有用…だったがSvelte 5では rune により必要性は減ったとのこと
SvelteKitでは使う可能性あるらしいけどいったん飛ばして良いかな…
チュートリアルで示されているのもStoreの使用法というよりはsvelte/motion
の利用例ぽい
Advanced bindings
-
contenteditable
を使える要素に対しては<div contenteditable bind:textContent={text}></div>
や<div contenteditable bind:innerHTML={html}></div>
が使用可能 -
{#each}
内の要素に対してもbindを使える -
<audio>
や<video>
の要素に対してもbindingがあるが専用APIなのでここでは省略 -
bind:clientWidth={w}
bind:clientHeight={h}
でブロック要素のサイズを受け渡せる -
bind:this={var}
でhtml要素自体のバインディングが可能 canvasくらいしか使い途がないのではないか - propsの値もバインディング可能 要するに子が親の値を更新できる
- コンポーネント側で
let { var = $bindable('') } = $props()
のように値を取り出す - 多用は避けるべき
- コンポーネント側で
Advanced transitions
'svelte/transition'
に含まれているcrossfade
という関数で defer transitionを作成できる
crossfade
は[send, receive]
の2つの関数を返し、これをin:receive
とout:send
のトランジションに設定することで移動トランジションを表現できる
またトランジション本体以外の要素には'svelte/animate'
のflip
をanimate:flip
というアトリビュートで設定することで移動が可能
これらはCSSとしてコンパイル(?)されるのでJSをブロックしないのも利点
Context API
'svelte'
からインポートできるsetContext
/ getContext
で値をやり取りできる
以下のkey
/ value
ともに型の制限はないもよう
import { getContext, setContext } from 'svelte';
setContext(key, value);
getContext(key).value;
単純な親から子への値の受け渡しの範疇を超える場合はContextを介したほうが便利と思われる
Special elements
HTML body内に使われない要素にもイベントリスナや変数bindを設定できるよう、以下の特殊タグが提供されている
<svelte:window />
<svelte:document />
<svelte:body />
<svelte:head></svelte:head>
また、タグが動的に変更される場合は<svelte:element this="tag-name"></svelte:element>
を使用できる
…と思ったが rune の世界ではこれは非推奨
こっちにする
-<svelte:component this={item.condition ? Y : Z} />
+{@const Component = item.condition ? Y : Z}
+<Component />
Component
の定義はscriptタグに入れてもよい
<script>
let condition = $state(false);
const Component = $derived(condition ? Y : Z);
</script>
<Component />
script module
初期化のとき以外にJSを評価したい場合に<script module>
を使えるらしい
非常に稀らしいのであんまり深入りしなくて良さそうかな…
まあまあ使うことはあるけど、後々で良いかも
ライブラリとか作るときはそこそこ使うかな
<script module>
ではなくて <script context='module'>
って書き方だった)
↑↑↑ここまでSvelteチュートリアル↑↑↑
↓↓↓ここからSvelteKitチュートリアル↓↓↓
Introduction
たとえばこういうディレクトリ構成になる
ページファイルに+
がついているのが特徴的
project
├ src
│ ├ app.html
│ └ routes
│ └ +page.svelte
├ static
│ └ shared.css
└ package.json
Routing
ファイルベースルーティングなのでディレクトリ構造がURLパス構造になる
なおURLのtrailing slashの有無は問わないもよう
File path | URL |
---|---|
routes/+page.svelte |
/ |
routes/about/+page.svelte |
/about |
routes/blog/[slug]/+page.svelte |
/blog/something |
routes/+layout.svelte
を作ると兄弟および子のファイルのページのレイアウトを定義できる
{@render children()}
によって展開される
<script>
let { children } = $props();
</script>
<nav>
navigation here
</nav>
<!-- pages are deployed here -->
{@render children()}
パスパラメータは+page.server.js
ファイル内のload
関数の引数として渡され、load
関数の返り値が data
propに保存される
'@sveltejs/kit'のerror
を使ってerror(404)
のようにエラー返却を行うことができる
import { error } from '@sveltejs/kit'
const posts = someAPI();
export function load({ params }) {
const post = posts.find(({ slug }) => slug === params.slug);
return post ? { post } : error(404);
}
<script>
let { data } = $props();
</script>
<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
ファイル名を+layout.server.js
にすると、同ディレクトリ以下のページが読み込まれる際にload()
を実行する担当する この返り値も同様にdata
オブジェクトから取得できる
Headers and cookies
load()
関数にわたされる引数はsetHeaders
という関数をフィールドに持ち、これを使うことでheaderの値を更新できる
export function load({ setHeaders }) {
setHeaders({
'Content-Type': 'text/plain'
});
}
ただし'Set-Cookie'
ヘッダーは使用できず、その場合はcookies
APIを使用しなければならない
export function load({ cookies }) {
const visited = cookies.get('visited');
cookies.set('visited', 'true', { path: '/' });
return {
visited: visited === 'true'
};
}
内部的には jshttp/cookie を使用しており、SvelteKitは自動的にクッキーをセキュアにするキーを追加する
`load()`の引数
export function load(params) {
console.dir(params);
}
↓
{
cookies: {
get: [Function: get],
getAll: [Function: getAll],
set: [Function: set],
delete: [Function: delete],
serialize: [Function: serialize]
},
fetch: [Function: fetch],
getClientAddress: [Function: getClientAddress],
locals: {},
params: {},
platform: undefined,
request: Request {
[Symbol(realm)]: { settingsObject: [Object] },
[Symbol(state)]: {
method: 'GET',
localURLsOnly: false,
unsafeRequest: false,
body: null,
client: [Object],
reservedClient: null,
replacesClientId: '',
window: 'client',
keepalive: false,
serviceWorkers: 'all',
initiator: '',
destination: '',
priority: null,
origin: 'client',
policyContainer: 'client',
referrer: 'client',
referrerPolicy: '',
mode: 'cors',
useCORSPreflightFlag: false,
credentials: 'same-origin',
useCredentials: false,
cache: 'default',
redirect: 'follow',
integrity: '',
cryptoGraphicsNonceMetadata: '',
parserMetadata: '',
reloadNavigation: false,
historyNavigation: false,
userActivation: false,
taintedOrigin: false,
redirectCount: 0,
responseTainting: 'basic',
preventNoCacheCacheControlHeaderModification: false,
done: false,
timingAllowFailed: false,
headersList: [HeadersList],
urlList: [Array],
url: [URL]
},
[Symbol(signal)]: AbortSignal {
[Symbol(kEvents)]: SafeMap(0) [Map] {},
[Symbol(events.maxEventTargetListeners)]: 10,
[Symbol(events.maxEventTargetListenersWarned)]: false,
[Symbol(kAborted)]: false,
[Symbol(kReason)]: undefined,
[Symbol(kComposite)]: false,
[Symbol(realm)]: [Object]
},
[Symbol(headers)]: Headers {
[Symbol(headers list)]: [HeadersList],
[Symbol(guard)]: 'request',
[Symbol(realm)]: [Object]
}
},
route: { id: '/' },
setHeaders: [Function: setHeaders],
url: <ref *1> URL {
searchParams: URLSearchParams {
[Symbol(query)]: [Array],
[Symbol(context)]: [Circular *1]
},
href: [Getter],
pathname: [Getter],
search: [Getter],
toString: [Getter],
toJSON: [Getter],
[Symbol(context)]: URLContext {
href: 'http://localhost:5173/?theme=light',
protocol_end: 5,
username_end: 7,
host_start: 7,
host_end: 16,
pathname_start: 21,
search_start: 22,
hash_start: 4294967295,
port: 5173,
scheme_type: 0
},
[Symbol(nodejs.util.inspect.custom)]: [Function (anonymous)]
},
isDataRequest: false,
isSubRequest: false,
depends: [Function: depends],
parent: [AsyncFunction: parent],
untrack: [Function: untrack]
}
Shared modules
ディレクトリ構造が深くなると共通コードへの相対参照が難しくなる
project
├ src
│ ├ app.html
│ ├ lib
│ │ └ shared.js
│ └ routes
│ ├ a
│ │ └ deeply
│ │ └ nested
│ │ └ route
│ │ └ +page.svelte // ←ここからshared.jsを指すのが大変
│ └ +page.svelte
├ static
│ └ shared.css
└ package.json
src/lib/
にあるコードはsrc
配下のどこからでも$lib/...
で参照できる
import { sharedFunc } from '$lib/shared.js';
Forms
+page.server.js
にform actionを書ける
export function load({ cookies }) {
// load
}
export const actions = {
create: async ({ cookies, request }) => {
// create record
},
delete: async ({ cookies, request }) => {
// delete record
}
};
// actionsが一つしかない場合は`default`という名前も使える
actionsの名前はformのaction属性で指定する(defaultの場合はaction不要)
<form method="POST" action="?/create">
<label>
add a record:
<input name="title" autocomplete="off" />
</label>
</form>
server actionsには返り値を設定することができ…
import { fail } from '@sveltejs/kit';
export const actions = {
createRecord: async ({ cookies, request }) => {
if (condition) {
return { message: "Complete"}
} else {
return fail(422, { error: "Unprocessable Content"})
}
}
};
ページからはform
propsで取得できる
<script>
let { form } = $props();
</script>
{#if form?.error}
<p class="error">{form.error}</p>
{:else if form?.message}
<p>{form.message}</p>
{/if}
formタグを使っているのでJS無効環境でも動作するが、enhance
を加えるだけでJS有効環境に対応した描画改善を加えられる(都度ページ全体をリロードするのではなく必要な部分だけ再描画するようになり、トランジション等を加えることができる)
<script>
import { enhance } from '$app/forms';
import { fly, slide } from 'svelte/transition';
</script>
<form method="POST" action="?/create" use:enhance>
...
</form>
また、use:enhance
はフック関数を渡す事が可能
フォーム送信時に関数自体が呼ばれ、フォーム処理終了でresponseが帰ってきたときに返り値の関数が呼ばれる
以下はフォーム処理完了時にupdateを呼んで描画を更新している
<form method="POST" action="?/create" use:enhance={() => {
creating = true;
return async ({ update }) => {
await update();
creating = false;
};
}}>
...
</form>
ほかにもcancel()
などが使える 詳細はenhanceのドキュメント参照
API routes
前述の通りSvelteKitはファイルベースのルーティングだが、ページではないAPIのルートも定義できる
エンドポイントが/hoge
の場合はsrc/routes/hoge/+server.js
になる
export function GET() {
const message = "hello API";
return new Response(message, {
headers: { 'Content-type': 'application/json' }
});
}
返り値はResponseである必要があるが、json値を返す場合が多いのでSvelteKitがラッパーを提供している
+import { json } from '@sveltejs/kit';
export function GET() {
const message = "hello API";
- return new Response(message, {
- headers: { 'Content-type': 'application/json' }
- });
+ return json(message);
}
POSTも使えるがSvelteKitとしてはForm Actionの使用を推奨(JS無効環境でも動作するため)
とはいえ同様にほかのメソッドも対応できる
export function GET() {
return new Response(...);
}
export function POST() {
return new Response(...);
}
export function PUT() {
return new Response(...);
}
export function DELETE() {
return new Response(...);
}
formを使うときはこのライブラリがおすすめ↑
Stores
前述の通りSvelte 5では rune により必要性は減っており自分から値を保存することは特に考えなくて良い
SvelteKitでは読み取り専用のstoresが用意されている
'$app/stores'
からpage
/ navigating
/ updated
の3種を使用することができる
中でもpage
はurl
をはじめとしたよく使う情報が自動的に保存される
マークアップでアクセスするときには$
を前置する必要がある
たとえば以下のようにすると現在のページに応じてaria-current
を付与できる
<script>
import { page } from '$app/stores';
</script>
<nav>
<a href="/" aria-current={$page.url.pathname === '/'}>home</a>
<a href="/about" aria-current={$page.url.pathname === '/about'}>about</a>
</nav>
navigating
はページ遷移(通常のリンクのほか、ブラウザバックなども含む)が発生した場合に遷移前後のページや遷移の種別が含まれる 普通にページを表示している間はnull
updated
はページを開いている間に更新バージョンがデプロイされたときにtrue
になる真偽値 プロダクションでのみ動作する あまり意識しなくて良さそう
余談だけど、
自分のページでは更新バージョンがデプロイされたらフルでリロードされるようにしてる
Errors and redirects
想定されているエラーにはerror
関数を使う ここに入れたメッセージはページ上にも表示される
import { error } from '@sveltejs/kit';
export function load() {
error(420, 'Enhance your calm');
}
対して通常のエラーやthrow
で作られたエラーは単に500 Internal Error
と表示される
内部情報を露出させないため
export function load() {
throw new Error('Waaaaa!');
}
エラーページの内容はsrc/routes/+error.svelte
で調整可能 これはsrc/routes/foo/bar/+error.svelte
のように階層に入れることで特定のパスで生じるエラー専用のエラーページを作成できる
また、src/routes/+layout.server.js
のような上位階層でエラーが起きてルートのレイアウトすらレンダリングできない場合は標準のエラーページ(ステータスコードとメッセージが表示される)が表示される
これはsrc/error.html
によって調整でき、エラー情報は%sveltekit.status%
と%sveltekit.error.message%
で取得できる
このへんの取得はsrc/app.html
と同じやね
リダイレクトは'@sveltejs/kit'
からインポートできるredirect
関数を使う
import { redirect } from '@sveltejs/kit';
export function load() {
redirect(307, path);
}
SvelteKitの推奨値は以下 302じゃないんだ?
- 303 フォームアクションの成功によるリダイレクト
- 307 一時的リダイレクト
- 308 恒久的リダイレクト
ほーん 307は302と、308は301と同じような感じみたいだ
Hooks
SvelteKitにはいくつかのhookが用意されている
handle
もっとも基本的なhook
デフォルトはこう
export async function handle({ event, resolve }) {
return await resolve(event);
}
ようするにhandlerなので、resolveに引数を渡すか自分で定義するかしてResponseを返せば良い
以下は改造例
export async function handle({ event, resolve }) {
if (event.url.pathname === '/ping') {
return new Response('pong');
}
return await resolve(event, {
transformPageChunk:({ html })=>html.replace(
'<body',
'<body style="color: hotpink"'
)
});
}
event.locals
event.localsオブジェクトには任意の値を保存できる
hooksから各ページのサーバーに値を受け渡すのに便利である
たとえばlocalsに適当な値を入れ
export async function handle({ event, resolve }) {
event.locals.answer = 42;
return await resolve(event);
}
↓それをserverでdataオブジェクトに仕込み
export function load(event) {
return {
message: `the answer is ${event.locals.answer}`
};
}
↓各ページで参照できる
<script>
let { data } = $props();
</script>
<h1>{data.message}</h1>
eventの中身 上の方で出てきた`load()`の引数と同じぽい
こちらでは日本語の説明
- cookies 前述のSvelte Cookies API
- fetch Web標準のFetch APIに少し機能追加されたもの
- getClientAddress() クライアントIP取得関数
- isDataRequest 真偽値 クライアントがデータを問い合わせている間はtrue
- locals 任意のデータをおけるオブジェクト
- params routesパラメータ
- request Requestオブジェクト
- route 現在のルートを表すオブジェクト
{ id: '/' }
みたいな値が入る - setHeaders() 前述のsetHeaders関数
- url 現在のリクエストのURLオブジェクト
handleFetch
上記のevent
に含まれるevent.fetch
はcookie/authヘッダーをいい感じに処理してくれたり相対パスを使えたりサーバー上で動くときに不要なHTTPリクエストをカットしてくれたりとSvelteKit内で使うのに便利な特徴を備えている
この挙動は変更できる 以下がデフォルト
// ここではeventは使われていない
export async function handleFetch({ event, request, fetch }) {
return await fetch(request);
}
例えば特定のパスのときに別のパスにリダイレクトするなど
export async function handleFetch({ event, request, fetch }) {
const url = new URL(request.url);
if (url.pathname === '/a') {
return await fetch('/b')
}
return await fetch(request);
}
handleError
前述の'@sveltejs/kit'
のerror
ではない エラー(要するに管理されていない一般のエラー)の挙動はhandleErrorで調整できる
ここの返り値は+error.svelte
内で参照できる
export function handleError({ event, error }) {
console.error(error.stack);
return {
message: 'handleError',
code: '999'
};
}
<script>
import { page } from '$app/stores';
</script>
<h1>Error {$page.status}</h1>
<div>{$page.error.message}</div>
<div>{$page.error.code}</div>
これはユーザー向けの返り値を調整するというよりはエラー情報を開発者に通知するなどの用途で有用そう
Page options
\+(page|layout)(\.server)?\.js
ファイルではload関数だけでなくさまざまなオプションをexportできる
ssr
サーバーでHTMLを構築する デフォルトはtrue
このほうがクローラを含む非JS環境などでも動作が保証できる
基本的にはSSRで動作するようにアプリケーションを作るべきだが、ブラウザスクリーンサイズなど、一部のクライアントサイドでしか動かないAPIを使用したい場合にはfalseにできる
export const ssr = false;
+layout.server.js
の中でssr
をfalse
に設定すると、アプリ全体がSPAとなる
csr
ページをインタラクティブにする デフォルトはtrue
特にインタラクティブな要素がなくても、SvelteKitはページ遷移時にパーシャルロードを行うのでJSを使っている
これをfalseにするとクライアントに完全にJSが送られなくなる
prerender
ページをビルド時に生成したものに固定する デフォルトはfalse
何らかの理由で完全に静的ページにしたいときは有効にする
ページ単体の処理は早くなるがビルドに時間がかかるほか、再デプロイ以外にページを更新する方法がない
+layout.server.js
の中でprerender
をtrue
に設定すると、SvelteKitはSSGとなる
trailingSlash
trailing slashの有無を統一する
-
'never'
trailing slashのあるパスを指定されても、ないパスへリダイレクトする デフォルト値 -
'always'
trailing slashのないパスを指定されても、あるパスへリダイレクトする -
'ignore'
リダイレクトを行わない 非推奨
export const trailingSlash = 'always';
この値は静的ファイルのビルドパスに関わり、always
の場合はalways/index.html
のようにパス名/index.html
が、never
の場合はnever.html
のようにパス名.html
が生成される
Link options
preloading
ページのロードに時間がかかる場合に備え、ユーザーが実際に遷移する前に遷移先のページを読み込むことができる
<a href="dst" data-sveltekit-preload-data>link_to</a>
なおこれは個別のリンクのみならずリンクの親要素にもつけることができ、かつSvelteKitの標準のボイラープレートがbodyタグに設定している ということで実際はあまり気にしなくてもいい
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
ページ自体ではなく遷移先で必要なJSを読み込むdata-sveltekit-preload-code
というのもある
reloading
SvelteKitは効率化のためにパーシャルロードを行うが、遷移時にページ全体をリロードしたい場合、個別のリンクタグまたは親要素にdata-sveltekit-reload
を付加する
その他
チュートリアルでは上記のオプションしか紹介しないがほかにもあるとのこと
- data-sveltekit-replacestate
- data-sveltekit-keepfocus
- data-sveltekit-noscroll
Advanced routing
Optional params
すでにroutingの説明で[page]/+page.svelte
のようにブラケットを使うことでダイナミックルーティングを定義できることを示した
これを[[lang]]/+page.svelte
と二重にすることでオプショナルなパスにすることができる
たとえばexample.com/page/1
でページ1を英語で、example.com/fr/page/1
でページ1をフランス語で表示する…というような場合に便利である
Rest params
パスを[...path]/+page.svelte
と書くことで任意長のパスにマッチさせることができる
たとえばexample.com/path
にもexample.com/nested/link
にもexample.com/super/deep/nested/long/url
にもマッチする
なおネスト部が末尾である必要はない
[...catchall]/
に404などエラーハンドリングするルートを作っておくと便利である
Param matchers
ダイナミックルーティングはするが、ルート文字列を特定のもののみに限りたい場合がある
例えばcolors/fe8d0a
は良いが、colors/anything
は拒否したい場合など
このような場合、src/params
にmatcherを定義することができる
ここに定義したjsファイルではmatch
関数をexportする必要がある これはルートを受け取って真偽値を返す関数
export function match(value) {
return /^[0-9a-fA-F]{6}$/.test(value);
}
で、ページファイルのルーティングをsrc/routes/colors/[color]
からsrc/routes/colors/[color=hex]
に変更する イコールの後の文字列はmatcherファイルの名前
これによりcolors/???
のマッチには上記のhex.js
のmatch()
が使われ、マッチする場合はsrc/routes/colors/[color=hex]/+page.svelte
が表示され、マッチしない場合は404となる
Route groups
parenthesで囲んだディレクトリを使うことで実際のルートには影響せずにグルーピングを行うことができる
例えば以下のようなファイル構成を考える
project
├ src
│ ├ app.html
│ └ routes
│ ├ about
│ │ └ +page.svelte
│ ├ account
│ │ └ +page.svelte
│ ├ app
│ │ └ +page.svelte
│ ├ +layout.svelte
│ └ +page.svelte
├ static
│ └ shared.css
└ package.json
ここで、about
はパブリックにアクセスできるが、account
とapp
はログインしないと見られないようにしたい場合、(authed)
ディレクトリにこれらを移動し、ここの+layout.svelte
にログインメニューを、+layout.server.js
にログイン確認処理を入れることができる
project
├ src
│ ├ app.html
│ └ routes
│ ├ about
│ │ └ +page.svelte
│ ├ (authed)
│ │ ├ account
│ │ │ └ +page.svelte
│ │ ├ app
│ │ │ └ +page.svelte
│ │ ├ +layout.svelte
│ │ └ +layout.server.js
│ ├ +layout.svelte
│ └ +page.svelte
├ static
│ └ shared.css
└ package.json
Layout除去
ページは上位のレイアウトを継承するので、例えばsrc/routes/a/b/c/+page.svelte
を表示する場合、
src/routes/+layout.svelte
src/routes/a/+layout.svelte
src/routes/a/b/+layout.svelte
src/routes/a/b/c/+layout.svelte
が読み込まれる
これを調整したい場合は@
を使用し、例えばsrc/routes/a/b/c/+page@b.svelte
とするとb/+layout.svelte
までの読み込みで止まり、c
のものが読み込まれなくなる
同様に+page@a.svelte
ならb
とc
の読み込みを、+page@.svelte
ならa
とb
とc
の読み込みをスキップできる
ただしルートのレイアウトは必ず使用され、スキップすることはできない
Advanced loading 1
ページが外部APIから値を読み込む場合や、+page.server.js
の値がシリアライズできない場合などには+page.js
を使うことができる
+page.server.js
をServer load function、+page.js
をUniversal load functionという
チュートリアルの例では嬉しいポイントがわからないので将来的にドキュメントを見て勉強したい
これらは併用でき、Universal load functionのほうでServer load functionの返り値を読み込むことができる
export async function load() {
return {
message: 'this data came from the server-load-function',
};
}
export async function load({ data }) {
return {
message: data.message + ' and the universal-load-function',
};
}
値はマージされず、+page.svelte
に渡されるのは常にUniversal load functionの返り値
<script>
let { data } = $props();
</script>
<div>{data.message}</div>
また、+page.svelte
や+layout.svelte
が親のSever load functionの返り値にアクセスできるように、Universal load functionも親のSever load functionの返り値にアクセスできる
これにはload
関数の引数の一つであるasync parent
関数を使う
export async function load() {
return { a: 1 };
}
export async function load({ parent }) {
const { a }= await parent()
return { b: a + 1 };
}
export async function load({ parent }) {
const { a, b } = await parent();
return { c: a + b };
}
すごいスパゲッティになりそう
あまりこれに頼ったアーキテクチャにはしたくない
Advanced loading 2
load
関数はページ遷移時に読み込まれるが、兄弟同士のページを移動した際、親階層のload
が再実行されないことがある(この問題認識は合っているのかびみょう チュートリアルの例ではURL依存関係の問題か?)
SvelteKitにload
再実行を強制するためには、'$app/navigation';
からインポートできるinvalidate()
にパスを渡す このパスに更新が生じたとしてリロードが行われる
なおinvalidate()
にはパス文字列やURLだけでなくURLを受け取って真偽値を返す関数を渡しても良い 詳しくはドキュメント参照
invalidate
で使うキーはURLとして適正なら良く、depends(url)
関数を使ってdepends('date:now')
のようなキーを定義しても良い この場合はinvalidate('date:now')
でリロードが行われる
なお、パスとかキーとか無視して全てリロードするinvalidateAll()
という関数もある
影響範囲が広そう
Environment variables
.env
に環境変数を設定してimport { ENV_NAME } from '$env/static/private';
で読み込むことができる
.env.local
や.env.develop
なども利用可能これはVite準拠
これはサーバーサイドでしか使用できないほか、環境変数を利用した分岐はビルド時に削除されて最適化される
ビルド後に動的に読み込みたい場合はimport { env } from '$env/dynamic/private';
のようにする
この場合はenv.ENV_NAME
のように使用する
.env
ファイルに含まれる値でも名前の頭にPUBLIC_
が付いているものはpublicとして扱うことができ、'$env/static/public'
'$env/dynamic/public'
から読み込む
静的・プライベートじゃない環境変数を.env
に入れるか?と疑問があるのでstatic/private
だけ覚えていれば良いんじゃないかな
チュートリアル終わったのでクローズ