Featured image of post RemixでETagを使う

RemixでETagを使う

Twitter ツイート Hatena Bookmark ブックマーク

Remixアプリを作って実際にサーバにデプロイし始めてから、コンテンツに変更ないほぼ静的なページなのに、ステータスコードが200でレスポンスされていたのでちょっと嫌だなと思って、ETagで304レスポンス返せるようにしてみました。

ETagとはなにか?

まずそもそもとしてETagとはなにか?

ブラウザキャッシュを利用する仕組み。
それを実現するためのHTTPレスポンスHeader。
ブラウザにて、コンテンツに対するHashを持ち、これを最新のコンテンツ要求時に添える。
サーバ側は送られたHashと最新のコンテンツのHashを比較し、更新の有無をクライアントへ返却する。
更新が無ければコンテンツは返送しない。

ETagとは

RemixでETagヘッダを追加して、検証する

  1. 0から自分で実装する
  2. remix-etagを使って実装する

0から自分で実装する

0から自分で実装する方法はここでは解説しないです。
この記事にを参考にすれば簡単に実装できるかなと思います。

Use ETags in Remix

remix-etagを使って実装する

donavon/remix-etag のREADME.mdにも書いてありますが簡単に実装できます。

まずは、remix-etagを入れます

1
$ yarn add remix-etag

あとは「apps/entry.server.tsx」を変更していきます。 handleBrowserRequest関数のなかでレスポンスを返しているところにetag関数を挟むだけで簡単に対応できます。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
 * 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);
  });
}

comments powered by Disqus
Built with Hugo
テーマ StackJimmy によって設計されています。