SvelteKitでデータ取得をSPA＋Expressの構成の考え方でやろうとして躓いた話

## はじめに
今までは、SPAで単にブラウザにレンダリングされた後、JavaScriptのaxiosによりREST APIを呼び出していた。SvelteKitになると、SSRになりこのあたりの勝手が違っているが、SAP＋Node.jsのExpressの構成の志向で考えて躓いたりした。

本記事では、その躓きの際に考えたことを忘れないように残す備忘録です。

## 考えたデータ取得の方法

1. SvelteKitの作法にのり、API経由ではなく直にSequelizeを呼ぶ
1. `export const ssr = false;`で+page.jsをブラウザ上でしか実行させないようにして、今までのようにAPIを呼び出す
1. `+page.server.js`内で`fetch`を利用するためにAPIをExpressではなく、API Routesで実装する

## 1. SvelteKitの作法にのり、API経由ではなく直にSequelizeを呼ぶ

```js:src/routes/blog/[slug]/+page.server.js
import * as db from '$lib/server/database';

export async function load({ params }) {
    return {
        post: await db.getPost(params.slug)
    };
}
```

- https://kit.svelte.jp/docs/load#page-data

## 2. `export const ssr = false;`で+page.jsをブラウザ上でしか実行させないようにして、今までのようにAPIを呼び出す

```js:src/routes/+page.js
export const ssr = false;

export async function load({ fetch }) {
	const todo = await fetch('/todo');
	const todojson = await todo.json();
	console.log('todojson', todojson);

	const response = await fetch('/api/healthcheck');
	const healthCheck = await response.text();
	console.log('healthCheck', healthCheck);

	return {
		todos: [],
		healthCheck,
		todojson
	};
}
```

### 問題点

- APIのレスポンスまでに間があると、画面が真っ白になる
  - https://kit.svelte.jp/docs/modules#$app-stores-navigating の`import { navigating } from '$app/stores';`でnavigatingをキャッチできるか？を試したが、初期描画時は不可

```svelte
<script>
	import { page, navigating } from '$app/stores';
</script>

<a href="/" aria-current={$page.url.pathname === '/'}> home </a>
<a href="/about" aria-current={$page.url.pathname === '/about'}> about </a>

{#if $navigating}
	navigating to {$navigating.to.url.pathname}
{/if}

<slot />
```

### 無理やり方法？？①

- onMount()のライフサイクルフック内でAPIを呼び出し、loadingフラグを使って表示を切り替える
  - デメリット：`import { page } from '$app/stores'`を利用して、`$page.data`などのデータを利用できないので、ページ間のデータ共有などで苦労する 

```svelte
<script>
	export let data;
	import { onMount } from 'svelte';
	import { page } from '$app/stores';

	let isLoaded = false;
	let healthCheck = '';

	onMount(async () => {
		const todo = await fetch('/todo');
		const todojson = await todo.json();
		console.log('todojson', todojson);

		const response = await fetch('/api/healthcheck');
		const healthCheckss = await response.text();
		console.log('healthCheck', healthCheckss);

		isLoaded = true;
		healthCheck = healthCheckss;
	});
</script>

{#if !isLoaded}
	Loading...
{:else}
	<div class="centered">
		<h1>HealthCheck</h1>
		{healthCheck}

		<h1>todos</h1>

		<label>
			add a todo:
			<input
				type="text"
				autocomplete="off"
				on:keydown={async (e) => {
				}}
			/>
		</label>

		<ul class="todos">
			{#each data.todos as todo (todo.id)}
				<li>
					<label>
						<input
							type="checkbox"
							checked={todo.done}
							on:change={async (e) => {
							}}
						/>
						<span>{todo.description}</span>
						<button
							aria-label="Mark as complete"
							on:click={async () => {
							}}
						/>
					</label>
				</li>
			{/each}
		</ul>
	</div>
{/if}
```

### 無理やり方法？？②

- Await blocksを使う
  - デメリット：`import { page } from '$app/stores'`を利用して、`$page.data`などのデータを利用できないので、ページ間のデータ共有などで苦労する 

```svelte
<script>
	export let data;
	import { page } from '$app/stores';

	const promise = (async () => {
		const todo = await fetch('/todo');
		const todojson = await todo.json();
		console.log('todojson', todojson);

		const response = await fetch('/api/healthcheck');
		const healthCheck = await response.text();
		console.log('healthCheck', healthCheck);

		return {
			todojson,
			healthCheck
		};
	})();
</script>

{#await promise}
	<p>...waiting</p>
{:then objectData}
	<div class="centered">
		<h1>HealthCheck</h1>
		{objectData.healthCheck}

		<h1>todos</h1>

		...
	</div>
{:catch error}
	<p style="color: red">{error.message}</p>
{/await}
```

## 3. `+page.server.js`内で`fetch`を利用するためにAPIをExpressではなく、API Routesで実装する

```js:src/routes/+page.server.js
export async function load({ fetch }) {
	const todo = await fetch('/todo');
	const todojson = await todo.json();
	console.log('todojson', todojson);

    // ただし、以下のようなExpressで自前で実装したものだと、開発モード時は問題ないが、ビルド後の本番モードだとエラーになる
	// const response = await fetch('/api/healthcheck');
	// const healthCheck = await response.text();
	// console.log('healthCheck', healthCheck);

	return {
		todojson
	};
}
```

### 問題点

- SvelteKitの[API Routes](https://kit.svelte.jp/docs/routing#server)で実装したエンドポイントではエラーにならないが、Expressで実装したエンドポイントだとエラーになる（正確には、ビルド後の本番モードの場合には）
  - [fetch リクエストの作成](https://kit.svelte.jp/docs/load#making-fetch-requests)に以下のような記述がある通り、サーバー側でfetchを呼び出すと、ブラウザのfetchと同じような動作にならないのが原因と思われる

> サーバーで動作している場合、内部リクエスト (例えば +server.js ルート(routes)に対するリクエスト) は直接ハンドラ関数を呼び出すので、HTTP を呼び出すオーバーヘッドがありません

- ※`+page.server.js`内で`axios`を使って自身のサーバーにリクエストを送ることもできるが、HTTPで通信する分非効率？？
  - ただ、DBの方の処理の内容（APIの実装の中の処理）で時間がかかる場合、SvelteKit標準の`fetch`を使おうが、axiosで自分のサーバーにリクエストを送る方法を取ろうが、処理に時間がかかり画面が開くまでに間が空いてしまうというのは同じ話？？

```sh
$ node srv/server.js
listening on port 3000

  🔀 Api routes found:
    - /api/healthcheck:  GET
handleFetch
request Request {
  [Symbol(realm)]: {
    settingsObject: { baseUrl: undefined, origin: [Getter], policyContainer: [Object] }
  },
  [Symbol(state)]: {
    method: 'GET',
    localURLsOnly: false,
    unsafeRequest: false,
    body: null,
    client: { baseUrl: undefined, origin: [Getter], policyContainer: [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 {
      cookies: null,
      [Symbol(headers map)]: Map(0) {},
      [Symbol(headers map sorted)]: null
    },
    urlList: [ [URL] ],
    url: URL {
      href: 'https://192.168.56.5:3000/todo',
      origin: 'https://192.168.56.5:3000',
      protocol: 'https:',
      username: '',
      password: '',
      host: '192.168.56.5:3000',
      hostname: '192.168.56.5',
      port: '3000',
      pathname: '/todo',
      search: '',
      searchParams: URLSearchParams {},
      hash: ''
    }
  },
  [Symbol(signal)]: AbortSignal { aborted: false },
  [Symbol(headers)]: HeadersList {
    cookies: null,
    [Symbol(headers map)]: Map(0) {},
    [Symbol(headers map sorted)]: null
  }
}
todojsontodojson { id: 'test' }
handleFetch
request Request {
  [Symbol(realm)]: {
    settingsObject: { baseUrl: undefined, origin: [Getter], policyContainer: [Object] }
  },
  [Symbol(state)]: {
    method: 'GET',
    localURLsOnly: false,
    unsafeRequest: false,
    body: null,
    client: { baseUrl: undefined, origin: [Getter], policyContainer: [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 {
      cookies: null,
      [Symbol(headers map)]: Map(0) {},
      [Symbol(headers map sorted)]: null
    },
    urlList: [ [URL] ],
    url: URL {
      href: 'https://192.168.56.5:3000/api/healthcheck',
      origin: 'https://192.168.56.5:3000',
      protocol: 'https:',
      username: '',
      password: '',
      host: '192.168.56.5:3000',
      hostname: '192.168.56.5',
      port: '3000',
      pathname: '/api/healthcheck',
      search: '',
      searchParams: URLSearchParams {},
      hash: ''
    }
  },
  [Symbol(signal)]: AbortSignal { aborted: false },
  [Symbol(headers)]: HeadersList {
    cookies: null,
    [Symbol(headers map)]: Map(0) {},
    [Symbol(headers map sorted)]: null
  }
}
TypeError: fetch failed
    at fetch (file:///home/study/workspace/intro-sveltkit-js/build/shims.js:20346:13)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async resolve (file:///home/study/workspace/intro-sveltkit-js/build/server/index.js:4228:14)
    at async respond (file:///home/study/workspace/intro-sveltkit-js/build/server/index.js:4053:22)
    at async fetch (file:///home/study/workspace/intro-sveltkit-js/build/server/index.js:3863:26)
    at async Object.handleFetch (file:///home/study/workspace/intro-sveltkit-js/build/server/chunks/hooks.server-148eac59.js:5:10)
    at async Object.fetch (file:///home/study/workspace/intro-sveltkit-js/build/server/index.js:3803:12)
    at async load (file:///home/study/workspace/intro-sveltkit-js/build/server/chunks/2-d8d031de.js:5:20)
    at async load_server_data (file:///home/study/workspace/intro-sveltkit-js/build/server/index.js:2099:18)
    at async file:///home/study/workspace/intro-sveltkit-js/build/server/index.js:3509:18 {
  cause: [Error: 139657870710720:error:1408F10B:SSL routines:ssl3_get_record:wrong version number:../deps/openssl/openssl/ssl/record/ssl3_record.c:332:
  ] {
    library: 'SSL routines',
    function: 'ssl3_get_record',
    reason: 'wrong version number',
    code: 'ERR_SSL_WRONG_VERSION_NUMBER'
  }
}
healthCheckhealthCheck {"message":"Not Found"}
page {
  error: null,
  params: {},
  route: { id: '/' },
  status: 200,
  url: URL {
    href: 'https://192.168.56.5:3000/',
    origin: 'https://192.168.56.5:3000',
    protocol: 'https:',
    username: '',
    password: '',
    host: '192.168.56.5:3000',
    hostname: '192.168.56.5',
    port: '3000',
    pathname: '/',
    search: '',
    searchParams: URLSearchParams {},
    hash: ''
  },
  data: { todos: [], todojson: { id: 'test' } },
  form: null
}
```

## まとめとして

この備忘録はSvelteKitを触り始めた初期に書いたもので、投稿ができていなかったものでした。懐かしく振り返ると変なアプローチしているな～と思っています。

まあ普通にload関数からサービスなり、サービスを介さずモデル（データベース）へアクセスするような実装をすればいいだけ