🤲

【SvelteKit 入門】データハンドリング(+page.js)

2022/10/15に公開約6,500字

ディレクトリベースのルーティング、+page.svelteによるページ表示、
次はデータ処理に特化したファイルを使った データハンドリングです。

本記事で扱うファイル

+page.svelte +page.js +page.server.js


シリーズまとめ(随時追加・更新)

【SvelteKit 入門】はじめに
【SvelteKit 入門】作業の前に
【SvelteKit 入門】アダプター設定・ホスティング・コンテナ運用
【SvelteKit 入門】ルーティング
【SvelteKit 入門】データハンドリング(+page.js) now reading
SvelteKit + microCMS でブログ構築

+page.svelte内のコード実行タイミング

まず以下の図を見てください

ユーザーからのアクセス    
← ① 画面描画の
ユーザーに画面を表示 ← ② 画面描画の 実行中
← ③ 画面描画の

次に、以下のコード例を見てください。

<script>
    // 【A】HTML要素の元となるデータ
    let items =[ '緒方', '金本', '前田' ]
  
    // 【B】ユーザーアクションで動作する処理
    function alrt(){
        alert('クリックされました')
    }
</script>

<ul>
    {#each items as item}                  // 【A】データを元に描画
        <li on:click={alrt}>{item}</li>    // 【B】on click イベント紐付け
    {/each}
</ul>

このコード内にある処理について、最初の表に追記すると以下のようになります。

ユーザーからのアクセス    
← ① 画面描画の
ユーザーに画面を表示 ← ② 画面描画の 実行中  【A】 
← ③ 画面描画の  【B】 

.svelteファイルに記述し構築していた画面というのは アクセスが回って来てからの話(②,③) であり、その前(①の部分)というのは.svelteファイルにとっては関与できない領域です。

しかし SvelteKit はユーザーからのアクセスを捌くルーティングを行っているため、 ①のタイミングに処理を入れる事が可能です

+page.js, +page.server.js の利用

SvelteKitのルーティングは ディレクトリベース です。

プロジェクト/
    ...
    ├ routes/
    │    ├ xxx/                    <- ディレクトリ名がURLに対応する
    │    │    ├ +page.svelte       <- *必須*
    │    │    ├ +page.js           <- (任意)
    │    │    └ +page.server.js    <- (任意)
    │    ├ ...

表示画面を構築するのはディレクトリ直下の+page.svelte。これは必須ファイル。
必要に応じて データ取得, 処理に特化した+page.js, +page.server.jsを配置 することで、SvelteKit がそれらを連動させ、結果をブラウザに出力します。


データの流れ

+ .jsファイルの特徴まとめ

  • +page.svelteによる画面描画の前、つまり先ほど言及した ①のタイミングで実行される
  • Requestへのアクセス(cookie取得)・パスパラメータの取得・リダイレクト等も可能
  • サーバー実行(固定)もできるので、秘匿データを扱える

認証情報を読み取りログインページにリダイレクトしたり、キーをヘッダーに入れて外部APIにアクセスしたり、.svelteファイルでは出来ない処理を担当します。

+page.js/+page.server.jsの使い分け

違いは 実行場所 です

+page.js サーバー or ブラウザ
+page.server.js サーバーのみ

SvelteKit のレンダリングは SSR と CSR を組み合わせるので、+page.jsの実行場所は状況により サーバーだったりブラウザだったりします。一方で+page.server.jsは常にサーバーで実行されます。

【本題】+page.jsによるデータ取得

+page.jsを使えばデータを+page.svelteに渡せる。レンダリング前に実行できる。

でも+page.svelteで良くない?

そうなんですよね…
そこで、いくつかの実装方法を順番に検証しながら最適解を考えます。

.svelteファイルに全て記述

+page.svelte
<script>
    let players =[]  // データ格納用の変数(空配列で初期化)
    fetch( 'https://jwnr-hono.deno.dev/' )
    .then( x=> x.json() ).then( x=> {
        players =x.players
    })
</script>


{#each players as player}
    <p> {player.name} </p>
{/each}

.svelteファイルの<script>部分にそのまま非同期処理を書いた場合、HTML部分のレンダリングは resolve を待ちません。その際 #eachの対象変数にIterableなデータが入ってないとエラーになるので、初期値は空配列にしてます。

空配列[]のまま初期描画 → fetch完了でデータが入る → 自動で再描画される

という流れ。

{#await}活用

データの取得→描画は出来ましたが、これではfetch完了まで空白になりますね。
Svelte の{#await}を使い、fetchが返すPromiseを適切に処理します。

/src/routes/blog/+page.svelte
<script>
    // getdata には Promise が入り、resolve 後にデータが返る
    let getdata =fetch( 'https://jwnr-hono.deno.dev/' ).then( x=> x.json() )
</script>


{#await getdata}
    <p>取得中</p>
{:then resolveData}
    {#each resolveData.players as player}
        <p> {player.name} </p>
    {/each}
{/await}

(これで全然問題ないじゃん・・・)

+page.jsでデータ処理を引き受ける

+page.svelteと連動してデータを受け渡す場合、load関数を使います。
例を見た方がわかりやすいと思いますので、早速。

+page.js
export function load({ fetch }) {  // <- ここの引数 fetch は後述

    // データ取得の処理
    async function getdata() {
        const x = await fetch('https://jwnr-hono.deno.dev/')
        return    await x.json()
    }
    return getdata()
    // load 関数内で return した値が +page.svelte に渡される。

}
+page.svelte
<script>
  export let data  // +page.js からデータ受け取り
  // ※ fetch の resolve 待ちは +page.js で済んでる
</script>


{#each data as player}
    <p> {player.name} </p>
{/each}

①との大きな違いは、fetch完了までがページ読み込みになる という点ですね。
ページ読み込み・描画 → fetch完了 → 再描画 という流れではなく、
ページ読み込み自体が待機 → fetch完了 → レンダリング開始 となります。

※ load関数 独自のfetch

load({ fetch }){...load関数から受け取る ことで、この関数内では標準の fetch を上書きしています。この load関数独自のfetch は、標準のものにいくつか機能を上乗せしたようなものです。
特定の使用方法だと この上乗せ機能が便利 なので、load関数内で fetch を使う場合は読み込んでおけば安心。今回のケースは標準fetchでいけますけどね。

+page.jsの独特な挙動

return で返すのは基本的にオブジェクトですが、Promise を入れると自動的にawaitしてくれる という便利な特徴があります。

+page.js
// === さっきの書き方(きちんとawaitを書く) ====
export function load({ fetch }){

    async function getdata(){           // async の関数作って
        const x =await fetch( '...' )   // fetch は await で実行
        return   await x.json()         // resolve したものしか返さない
    }
    return getdata()  // +page.svelte へ

}
+page.js
// ==== これでも正常動作する ====
export function load({ fetch }){
    return fetch( '...' ).then( x=> x.json() )  // 自動で await してくれる
}

そしてこの勝手にawaitは return 対象の第1階層まで効くようです。

+page.js
export function load({ fetch }){

    return <Promise>             // await。

    return {
        data: <Promise>          // await。
    }

    return {
        data: {
            players: <Promise>   // Promise のまま +page.svelte へ
        }
    }
}

+page.jsで取得しつつローディング表示

つまり、データ処理を+page.jsに引っ込めつつ、先にレンダリングという荒業も可能。

+page.js
export function load({ fetch }) {
    return {
        level1: {
            level2: fetch('https://jwnr-hono.deno.dev/').then( x=> x.json() )
        }   // 2階層目に Promise を入れる
    }
}
+page.svelte
<script>
    export let data  // Promise はまだ pending 状態
</script>


{#await data.level1.level2}
    <p>取得中</p>
{:then resolveData}
    {#each resolveData.players as player}
        <p> {player.name} </p>
    {/each}
{/await}

(最初から.svelteに書けばよくね・・・?)

結局どの書き方が良いのか

あくまで私の場合ですが・・・
.svelteファイルはアーキテクチャでいう ビュー に専念させる という感覚で分けているので、+page.js,+page.server.jsを使う事が多い気がします。

ただし、アプリケーション開発でコンポーネントに機能を閉じ込めたい場合は、1つの.svelteファイル内に詰め込んだりします。

結局は状況に応じて使い分けることになるので、特に気にせず好きにすれば良いかと思います。

+page.jsを使う最大の目的はload関数の活用です。
公式ドキュメントの該当ページを読んでみてください。
本家 / 日本語版

Discussion

ログインするとコメントできます