Open68

Svelte / SvelteKit メモ

hagiwarahagiwara

+layout.server.js+page.server.js などで複数の load 関数がある場合、+page.svelte ではそれぞれが結合されたデータを受け取るっぽい

+layout.server.js
...
  return {
    items
  }
}
+page.server.js
...
  return {
    params
  }
}
+page.svelte
<script>
  export let data;
  console.log(data);
</script>
console
{
  items: [...],
  params: { slug: 'hoge' }
}
hagiwarahagiwara

同じ名前のキーがあった場合、後の方で上書き?優先?されるっぽい

[slug]/+page.server.js
...
  const items = 'foo'

  return {
    items
  }
}
console
{
  items: 'foo'
  params: { slug: 'hoge }
}
hagiwarahagiwara

複数の 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

hagiwarahagiwara

サンプル

hagiwarahagiwara

以下のような {#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>
hagiwarahagiwara

fetch をオーバーライドしてエラーをスローするダミー関数を使用するとよい、と Chat-GPT さんが教えてくれた

  window.fetch = () => {
    return new Promise((_, reject) => {
      setTimeout(() => {
        reject(new Error('Dummy fetch error'))
      }, 1000)
    })
  }
hagiwarahagiwara

このままだと 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)
      })
    }
  }

  ...
hagiwarahagiwara

await ブロックを使用すると、Promise が取りうる 3 つの状態(pending(保留中)、fulfilled(成功)、rejected(失敗))に分岐できます。SSR モードでは、サーバー上では pending の状態だけがレンダリングされます。
https://svelte.jp/docs/logic-blocks#await

実行前の状態も出し分けしたい場合、{#await} だけじゃダメってことかな?
保留中だけ <button disabled> ... </button> にしたい、とか

hagiwarahagiwara

Svelte / SvelteKit の練習に TMDB API を使ったサイトをつくってみた

Anime Movie Xplorer
https://animemoviexplorer.vercel.app/

hagiwarahagiwara

あと Tailwind CSS と今どきの(?)CSS レイアウトの練習に

  • Tailwind CSS なるほど便利
  • float レイアウトバリバリ世代の浦島太郎状態なので flexgrid まだまだ難しいけどとても便利
hagiwarahagiwara

Stark WCAG Audit

Violations は色のコントラストが足りないのが2件と、残りはリンクが target=_blank になってるのにユーザーに通知がない

後で対応方法を調べる

hagiwarahagiwara

Todo)

  • 取得するデータに外れ値(?)が時々含まれるからそれを非表示にするような処理を入れる
  • Google Analytics いれる
  • Versel Analytics 入れる
  • 画像をフェードインする
  • 画像をレスポンシブにする
  • WAI-ARIA 対応
  • 機能追加
    • ページング
    • 公開年月で絞り込み
    • タイトルで検索
    • (ダークモード)
hagiwarahagiwara

開発環境を判定する

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 to NODE_ENV or MODE.

Modules • Docs • SvelteKit https://kit.svelte.jp/docs/modules#$app-environment-dev

hagiwarahagiwara

GA トラッキング用のスクリプトを開発環境では読み込まないようにする

+layout.svelte
<script>
  import { dev } from '$app/environment'
</script>

<svelte:head>
  {#if !dev}
    <!-- gtag 用のスクリプト -->
  {/if}
</svelte:head>
hagiwarahagiwara

要素追加時にフェードインして表示する

<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
https://animemoviexplorer.vercel.app/

hagiwarahagiwara

表示するデータが変わる場合(?)は 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
https://animemoviexplorer.vercel.app/release-month?q=2023-11

hagiwarahagiwara

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> の方が楽だったりするのかしら

hagiwarahagiwara

inputpattern 属性を使う場合

<form>
  <input type="month" pattern="[0-9]{4}-[0-9]{2}" />
</form>

このままだと { } が JS の式と見做され(?)、属性値が [0-9]4-[0-9]2 となってしまい正しく動作しない

  • 参照文字を使用する { }&#123; &#125;
  • String.raw() を使用する
<!-- 参照文字 -->
<form>
  <input type="month" pattern="[0-9]&#123;4&#125;-[0-9]&#123;2&#125;" />
</form>
<!-- String.raw() -->
<form>
  <input type="month" pattern={String.raw`[0-9]{4}-[0-9]{2}`} />
</form>
hagiwarahagiwara

リンクの処理を CSR ではなく通常の画面遷移にしたい場合、a 要素に data-sveltekit-reload 属性を付与する
form[method=get] 要素も同様)

<a href="/" data-sveltekit-reload>リンク</a>
<form action="/" method="get" data-sveltekit-reload>
    ...
</form>
hagiwarahagiwara

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> に設定することができ、ルーターの挙動をコントロールすることができます。

hagiwarahagiwara

ある HTML 要素が閲覧中のブラウザで対応してるか確認して出し分けする
(例: <input type="month">

+page.svelte
<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 になる)

+page.js
export const ssr = false
hagiwarahagiwara

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 をクライアントに送信してハイドレーションを行います。もし ssrfalse に設定した場合、代わりに空の 'shell' ページがレンダリングされます。これはページがサーバーでレンダリングできない場合には便利 (例えば document などのブラウザオンリーな globals を使用するなど) ですが、ほとんどの状況では推奨されません (appendix をご参照ください)。

hagiwarahagiwara

ある HTML 要素が閲覧中のブラウザで対応してるか確認して出し分けする Ver.2
(例: <input type="month">)

+page.svelte
<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() でもいいっぽい?
hagiwarahagiwara

+layout.svelte<svelte:head><title> を入れている状態で、
下層の +page.svelte<svelte:head><title> があるページから無いページに遷移した際、タイトルが変わらなかった(あるページのタイトルのまま)

無いページにも入れることで解決した

hagiwarahagiwara

レスポンスのヘッダーを設定する
(例: Cache-Control

export const load = ({ setHeaders }) => {
  setHeaders({
    'cache-control': 'public, s-maxage=3600',
  })

  return hoge
}
hagiwarahagiwara

server load 関数と universal load 関数はどちらも setHeaders 関数にアクセスでき、サーバー上で実行している場合、 レスポンスにヘッダーを設定できます (ブラウザで実行している場合、setHeaders には何の効果もありません)。これは、ページをキャッシュさせる場合に便利です

同じヘッダーを複数回設定することは (たとえ別々の load 関数であっても) エラーとなります。指定したヘッダーを設定できるのは一度だけです。


例えば複数の +page.server.js+layout.server.js で同じ設定をしていると Error: "cache-control" header is already set になった
(ただし、 api/hoge/+server.jsGET() 内だとエラーにならなかった。loadGET だから?)

hagiwarahagiwara

Cache-Controlとは、ブラウザーのキャッシュ動作を管理するHTTPヘッダーのことです。
簡単に言えば、誰かがWebサイトを訪問すると、ブラウザーはキャッシュと呼ばれるストアに画像やWebサイトデータといった特定のリソースを保存します。そのユーザーが同じWebサイトを再度訪問すると、Cache-Controlは、そのユーザーにローカルキャッシュからリソースを読み込ませるか、またはブラウザーがサーバーに新しいリソースを要求する必要があるかを決めるルールを設定します。
Cache-Controlをより深く理解するには、ブラウザキャッシュおよびHTTPヘッダーの基本を理解する必要があります。

hagiwarahagiwara

Vercel にホストしてる本番サイトだと、ドキュメントのレスポンスは Cache-Control: public, max-age=0, must-revalidate になってる
でも Fetch で受け取ってる __data.jsonCache-Control: private, no-store

CSR で表示してるページ(データ)はキャッシュしないってことなのかな? 🤔

hagiwarahagiwara

400, 404, 500 エラーの場合の画面を追加する

hagiwarahagiwara

エラーページをカスタマイズする

  • +error.svelte を追加する

エラーをスローする

import { error } from '@sveltejs/kit'

if (hoge) {
  throw error(400, 'Bad Request')
}
hagiwarahagiwara

DOM の状態を一時的に保持・復元する
(例: 値とスクロール位置)

+page.svelte
<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>
hagiwarahagiwara

例えばサイドバーのスクロールポジションや、<input> 要素の中身などの、一時的な DOM の状態(state)は、あるページから別のページに移動するときに破棄されます。

例えば、ユーザーがフォームに入力し、それを送信する前にリンクをクリックして、それからブラウザの戻るボタンを押した場合、フォームに入力されていた値は失われます。入力内容を保持しておくことが重要な場合、DOM の状態を スナップショット(snapshot) として記録することができ、ユーザーが戻ってきたときに復元することができます。

Snapshots • Docs • SvelteKit https://kit.svelte.jp/docs/snapshots

hagiwarahagiwara

SvelteKit では Snapshot という機能を使用してページの現在の状態を保存できるようになっています。
これを利用すると、無限スクロールで実装された検索ページから他のページに移動したあとで戻るボタンを押してページを戻ってきたときに前回と同じスクロール位置に表示を戻せます。

Svelte で無限スクロールを実装する https://zenn.dev/labbase/articles/6e97ee67c958e0#ページ遷移について