RemixでETagを使う

Remixアプリを作って実際にサーバにデプロイし始めてから、コンテンツに変更ないほぼ静的なページなのに、ステータスコードが200でレスポンスされていたのでちょっと嫌だなと思って、ETagで304レスポンス返せるようにしてみました。
ETagとはなにか?
まずそもそもとしてETagとはなにか?
ブラウザキャッシュを利用する仕組み。
それを実現するためのHTTPレスポンスHeader。
ブラウザにて、コンテンツに対するHashを持ち、これを最新のコンテンツ要求時に添える。
サーバ側は送られたHashと最新のコンテンツのHashを比較し、更新の有無をクライアントへ返却する。
更新が無ければコンテンツは返送しない。
RemixでETagヘッダを追加して、検証する
- 0から自分で実装する
- remix-etagを使って実装する
0から自分で実装する
0から自分で実装する方法はここでは解説しないです。
この記事にを参考にすれば簡単に実装できるかなと思います。
remix-etagを使って実装する
donavon/remix-etag のREADME.mdにも書いてありますが簡単に実装できます。
まずは、remix-etagを入れます
$ yarn add remix-etag
あとは「apps/entry.server.tsx」を変更していきます。 handleBrowserRequest関数のなかでレスポンスを返しているところにetag関数を挟むだけで簡単に対応できます。
/**
* By default, Remix will handle generating the HTTP Response for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/docs/en/main/file-conventions/entry.server
*/
import { PassThrough } from "node:stream";
import type { EntryContext } from "@remix-run/node";
import { Response } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import { etag } from 'remix-etag';
const ABORT_DELAY = 5_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return isbot(request.headers.get("user-agent"))
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
);
}
function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
console.error(error);
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
// レスポンスを一旦変数に代入する
const response = new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
resolve(
// ここをetagに差し替える
etag({ request, response })
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}