まくろぐ
更新: / 作成:

Web サイトに検索用のテキストフィールドなどを配置する場合、入力内容と URL のクエリパラメーターを同期させると、ブラウザのブックマークなどで入力内容を保存することができます。

/p/uy89x6s/img-001.gif
図: input 要素とクエリパラメーターの同期

Svelte での実装例

下記は input 要素を 1 つだけ持つシンプルな Svelte ページコンポーネントの例です。

src/routes/+page.svelte
<script lang="ts">
	import { onMount } from 'svelte';
	import { page } from '$app/stores';
	import { replaceState } from '$app/navigation';

	/** 入力フィールドのテキスト */
	let query = '';

	// マウント時に URL のクエリパラメーターを取得し、入力フィールドの内容 (query) として反映する。
	onMount(() => {
		const params = $page.url.searchParams;
		query = params.get('q') ?? '';
	});

	/** 入力フィールドの内容 (query) に合わせて URL のクエリパラメーターを変更する。 */
	function updateUrl() {
		const url = $page.url; // new URL(window.location.href); でも可
		if (query) {
			url.searchParams.set('q', query);
		} else {
			url.searchParams.delete('q');
		}
		replaceState(url, {});
	}
</script>

<input type="text" bind:value={query} on:input={updateUrl} />

onMount() でセットしたコールバック関数は、このコンポーネントがブラウザ上で表示されたときに呼び出されます。 このタイミングで、URL のクエリパラメーター(?q=ABC)を参照し、テキストフィールドの値(query 変数)に反映しています。

input 要素の on:input プロパティには、ユーザーがテキスト入力したときに呼び出されるイベントハンドラーを設定します。 このタイミングで URL 末尾のクエリパラメーターを同期させています。 ページ遷移を発生させずに URL だけ書き換えるには、$app/navigation が提供する pushState()replaceState() 関数を使用できます。 pushState() を使うと呼び出しごとにブラウザの履歴に積まれてしまうので、今回のケースでは replaceState() を使った方がよさそうです。

これら実装により、ブックマークやリンクの URL に検索文字列を含めることができるようになります。 もちろん、ブラウザ上で URL を直接入力した場合も、input 要素に検索文字列が入力された状態でページを開くことができます。

便利! ٩(๑❛ᴗ❛๑)۶ わーぃ

関連記事

更新: / 作成:
/p/t6f8b4s/img-001.gif
図: Svelte によるチャットボット用 UI の実装例

Svelte で ChatGPT のようなチャットボット UI を作るときの実装例です。 UI ライブラリなどを使わずに、プレーンな Svelte (+SvelteKit) だけで実装しています。 シンプルなチャットボットを作りたいときに参考になるかもしれません。

実際にはボットサーバーは存在しないので、ユーザーの入力したテキストを 1 秒後にオウム返しする関数を作っています。 下記のトップページ (+page.svelte) で呼び出している sendMessageToBot() 関数がそれです。

src/routes/+page.svelte
<script lang="ts">
	import './global.css';
	import { sendMessageToBot } from '$lib/utils';
	import Chat, { type ChatBubbleData } from '$lib/Chat/Chat.svelte';

	/** 表示する会話の内容(ユーザーと Bot のメッセージのリスト) */
	let bubbles: ChatBubbleData[] = [];

	/**
	 * ユーザーがメッセージを入力し終わったときに呼ばれるコールバック関数です。
	 *
	 * ユーザーの入力内容は直ちにチャットバブルとして表示し、ボットに入力内容を送ります。
	 * ボットからの応答が返ってきたときに、その内容をチャットバブルとして表示します。
	 */
	async function handleSend(userMessage: string): Promise<void> {
		bubbles = [...bubbles, { name: 'You', content: userMessage }];
		const botMessage = await sendMessageToBot(userMessage);
		bubbles = [...bubbles, { name: 'Bot', content: botMessage }];
	}
</script>

<Chat {bubbles} onSend={handleSend} />

関連記事

更新: / 作成:

Chunked Transfer Encoding (Coding) とは

HTTP/1.1 では Transfer-Encoding: chunked というレスポンスヘッダーを返すことにより、本文(ペイロード)部分を少しずつ返すことができるようになっています。 この仕組みを使うと、クライアントが最初のデータを受信するまでの時間 (TTFB: Time To First Byte) を短くすることができます。

例えば、次の HTTP レスポンスの例では、HelloWorld というテキストを分けて送っています(これらをチャンクと呼んでいます)。 各行末は \r\n で終わっていると考えてください。

Transfer-Encoding: chunked のレスポンス例
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

5
Hello
5
World
0
 

各チャンクは、サイズ(16進数文字列)とデータが <サイズ>\r\n<データ>\r\n というペアの形で送られます。 そして、最後はサイズ 0 のチャンク (0\r\n\r\n) を送ることで、すべてのデータの送信が完了したことを示します。

HTTP/1.1 のレスポンスには、この Transfer-Encoding: chunked ヘッダーか、Content-Length ヘッダーのいずれか一方を含む必要があります。 本文全体を一度に送る場合は、Content-Length ヘッダーで本文全体のサイズ(オクテット数)を示します。

☝️ なぜサイズ情報が必要なのか?

Transfer-Encoding: chunkedContent-Length のどちらを使う場合も、本文のデータサイズを送ることが前提となっています。 これは、クライアント側で一連のデータの受信を完了したことを判断できるようにするためです。

HTTP 接続では半二重通信が行われるため、1 つのリクエストのやりとりがすべて完了するまで、次のリクエストを送ることができません。 クライアントでデータの受信完了タイミングを判断できれば、次のリクエスト用にその接続を使いまわすことができます (Connection: keep-alive)。

もちろん、サーバー側から接続を切ることでもデータの送受信を止めることができますが、次のリクエストのために TCP 接続を使いまわすことができなくなってしまいます。 新しく TCP 接続を確立するには、3-way ハンドシェイクという 3 回の通信を行わなければいけないため、できれば TCP 接続は使いまわすような仕組みの方が効率がよいのです。

Chunked Transfer Encoding なレスポンスを返すサーバー

Transfer-Encoding: chunked なレスポンスを返すサーバーの Deno での実装例が公開されています。

このサーバーは Deno Deploy 上で公開されているので、Web ブラウザーか curl コマンドで次のようにアクセスすると、1 秒おきに文字列を受信できます(現在時刻を表す文字列になっています)。 curl-i オプションを付けてレスポンスヘッダーを見ると、transfer-encoding: chunked が含まれていることを確認できます。

curl コマンドでの HTTP stream 受信
$ curl -i https://example-streaming.deno.dev
HTTP/1.1 200 OK
content-type: text/plain
x-content-type-options: nosniff
vary: Accept-Encoding
transfer-encoding: chunked
date: Fri, 08 Mar 2024 07:28:13 GMT
via: http/1.1 edgeproxy-h
server: deno/gcp-asia-northeast1

It is 2024-03-08T07:28:14.180Z
It is 2024-03-08T07:28:15.181Z
It is 2024-03-08T07:28:16.182Z
...

デフォルトでは、上記のようにチャンクのデータ部分しか表示されませんが、curl--raw オプションを付けると、チャンクサイズの部分も表示してくれます。

$ curl --raw https://example-streaming.deno.dev
1F
It is 2024-03-08T07:28:14.180Z

1F
It is 2024-03-08T07:28:15.181Z

1F
It is 2024-03-08T07:28:16.182Z

1F というサイズは 16 進数表記なので、10 進数だと 31 オクテット(≒ 31bytes)ということですね。 上記テキストの場合は「30 文字+改行(\n)」でちょうど 31 オクテットになっています。

理解したっ ٩(๑❛ᴗ❛๑)۶ わーぃ

関連記事

メニュー

まくろぐ
サイトマップまくへのメッセージ