対象読者
- Reactの基本を修めている方
- 通信回線が弱いユーザーにも高速に表示できるサイトを作りたいエンジニア
- WebブラウザとNode.jsという異なるランタイムをそれぞれキャッチアップするのが辛くなってきたエンジニア
前提環境
筆者の検証環境は以下の通りです。
- macOS Sonoma 14.5
- Node.js 22.4.1
- NPM 10.8.1
- Remix 2.9.2
プロジェクトの構成を確認する
さて、前回の手順でプロジェクトが作成できましたが、通常のRemixプロジェクトとは少し異なる部分があります。簡単におさらいしていきましょう。図1では、前回作成したRemixプロジェクトの構成を示しています。通常のnpm create remix@latestというコマンドで作成したプロジェクトと比べて、追加されたファイルにピンクの下線を引き、大きな変更があったファイルには青い囲みを付けています。
それぞれのファイルの役割や変更点を上から順に簡単に解説します。
app/entry.server.tsx
app/entry.server.tsxは、サーバーサイドレンダリング処理のエントリーポイントです。サーバーが受け取ったリクエストに対して、レスポンスを構築するための入り口となるファイルのことですね。
このファイルには大きな変更がありました。Node.js向けにnpm create remix@latestで作成した場合、このentry.server.tsxのテンプレートは140行程度です。一方、Cloudflare向けのテンプレートでは40行程度に収まっています。これは、RemixサーバーがWeb標準のストリームAPIのインターフェースであるReadableStream型のデータをレスポンスボディとして返しているために起きた差ではないかと思われます。
Cloudflare Workersの処理系は、ブラウザのService WorkerをCDNのエッジサーバーで動かしているため、ブラウザと同じようにストリームAPIを扱うことができます。一方、Node.jsのストリームAPIはv16で初登場し、長らくExperimental(実験的な機能)扱いだったのが、v21でStable(安定版)になりました。
RemixではNode.js v21未満のバージョンもサポートしているため、2024年現在は安定版ではないストリームAPIには頼らない形でentry.server.tsxを実装しているものと思われます。いつか、Node.jsの古いバージョンをサポートしなくなった頃に、Node.js向けのテンプレートもReadableStreamを前提としたものに変わるのかもしれません。
functions/[[path]].ts
functions/[[path]].tsは、Page Functionsのエントリーポイントです。Webサイトにアクセスがあったときに、URLのパスに対応するパスのファイルがfunctions内にあった場合、そのファイルが実行されます。たとえば、/aboutにアクセスがあった場合には、functions/about.tsが実行されるわけですね。
今回のように[[abc]].tsのような[[と]]で囲まれた部分は、ワイルドカードとして扱われ、任意のパスにマッチします。つまり、functions/[[path]].tsは、どんなパスにもマッチするエントリーポイントになるのです。詳しくは、Page Functionsのドキュメントを参照してください。
内容はシンプルで、Remixのハンドラに処理を委譲しているだけです。Remixフレームワーク内の流れとしては、まずはfunctions/[[path]].tsがすべてのリクエストを受け付けて、Remixのハンドラを通じてapp/entry.server.tsxに具体的な処理を委譲する形になっています。
public/_headers
public/_headersはCloudflare Pages固有の設定ファイルです。所定のパスにアクセスがあったときに、デフォルトで付与するヘッダー情報を記述することができます。今回のケースでは、リスト1のように設定されています。
/favicon.ico Cache-Control: public, max-age=3600, s-maxage=3600 /assets/* Cache-Control: public, max-age=31536000, immutable
/favicon.icoのキャッシュを1時間に設定するとともに、/assets/*以下のファイルは1年間キャッシュするように設定されています。/assetsは見慣れないパスかもしれませんが、npm run buildコマンドでビルドしたときに生成される.jsファイルや.cssファイルが格納される、build/client/assetsフォルダを指しています。Cloudflare Pagesには/build/clientフォルダをルートディレクトリとして静的ホスティングするので、デプロイ後にキャッシュ対象とするURLは/assets/*になるわけです。
public/_routes.json
public/_routes.jsonはPage Functions固有の設定ファイルです。リクエストがあったときにfunctions/配下の処理を呼び出すかどうかを設定することができます。リスト2のように設定されています。
{
"version": 1,
"include": ["/*"],
"exclude": ["/favicon.ico", "/assets/*"]
}
静的配信したい(そして_headersでキャッシュしている)ファイルへのアクセスではPage Functionsが呼び出されないようにexcludeを設定しつつ、それ以外のすべてのリクエストに対してPage Functionsを呼び出すようにincludeを設定しています。
load-context.ts
load-context.tsは、Remixのloaderなどでアクセスできるcontextオブジェクトを定義・拡張するためのファイルです。今回のケースでは、context.cloudflareを追加しています。Cloudflareの各種APIを利用するため、TypeScriptの型定義の拡張を行なうことが目的です。
contextをさらに細かく拡張したい場合は、公式ドキュメントを参照して、getLoadContext関数を定義するとよいでしょう。
package.json
package.jsonでは@remix-run/nodeが@remix-run/cloudflareと@remix-run/cloudflare-pagesに置き換えられたのが大きな変更です。また、Cloudflare Workersをはじめとした開発者向けサービスの管理用CLIツールである、Wrangler(ラングラー)も新たにインストールされ、NPM ScriptsにもWranglerを使ったデプロイコマンドが追加されています。
vite.config.ts
vite.config.tsには、小さいようで大きな変更として、@remix-run/devに含まれるcloudflareDevProxyVitePluginというプラグインが設定されています。これは、開発時にローカル環境を立ち上げるタイミングで、Wranglerと連携するためのプラグインです。
本来、Cloudflare Workersの実行環境はNode.jsとは異なるため、ローカル環境では動作確認が困難ですが、WranglerにはMiniflare(ミニフレア)という、ローカル環境でCloudflare Workersをエミュレートするためのツールが内蔵されています。
cloudflareDevProxyVitePluginを通じて、Viteの開発サーバーとMiniflareを連携させることで、ローカル環境でCloudflare PagesやPage Functionsの動作をエミュレートできるようになっているのです。
wrangler.toml
説明の都合で順番が前後しますが、wrangler.tomlについて先に解説させてください。wrangler.tomlは、Wranglerコマンドの設定ファイルでもあり、Cloudflare WorkersやCloudflare Pagesによってのプロジェクト管理ファイルでもあります。リスト3のように設定されています。
#:schema node_modules/wrangler/config-schema.json name = "remix-on-cloudflare-sample" compatibility_date = "2024-06-14" pages_build_output_dir = "./build/client"
nameはプロジェクト名を指しており、このままデプロイすれば、Cloudflare Pagesのプロジェクトがこの名前で作成され、pages.devのサブドメインとして公開されることになります。「remix-on-cloudflare-sample」という名前は本記事で筆者がとってしまったので、読者の皆さんが試すときは別の名前に書き換えてもよいかもしれません。
compatibility_dateは、Wranglerのバージョンとの互換性を指定するためのフィールドです。pages_build_output_dirは、ビルドしたファイルが格納されるディレクトリを指定しています。Cloudflare Pagesは、このディレクトリをルートディレクトリとして静的ホスティングするため、Remixのビルド先のディレクトリを指定しています。
また、wrangler.tomlには、Cloudflare Workers向けの各種サービスを紐づけるための設定を記述する役割もあります。たとえば、SQLiteデータベースのサービスであるCloudflare D1を利用する場合は、リスト4のように設定します。
# (省略) [[d1_databases]] binding = "MY_DB" database_name = "my-database" database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
[[d1_databases]]は、Cloudflare D1のデータベースを指定するためのセクションです。bindingは、WorkersやPage Functionsからアクセスするための変数名を指定します。database_nameは、事前にセットアップ済みのCloudflare D1のデータベース名を指定します。database_idは、database_nameに対応する、Cloudflare D1のデータベースIDを指定します。このように、wrangler.tomlには、Cloudflareの各種APIを利用するための設定を記述することができます。
同様に、ストレージサービスのR2や、Workers AIといった、Cloudflareの各種APIを利用する際には、wrangler.tomlに設定を追加していくことになります。
worker-configuration.d.ts
最後に、worker-configuration.d.tsは、Wranglerによって生成される、Cloudflare Workersの各種APIを利用するためのTypeScript型定義ファイルです。wrangler typesコマンドで生成されます。
リスト4の例で解説してみましょう。wrangler.tomlにbinding = "MY_DB"を書いただけでは、TypeScriptからはどこにAPIがあるのかわかりませんね。そこで、wrangler typesコマンドを実行すると、Cloudflare D1のAPIにアクセスするための型定義ファイルが生成されます。実際にリスト4のように設定して、wrangler typesコマンドを実行したときの結果を見てみましょう(リスト5)。
$ npm run typegen
> typegen
> wrangler types
⛅️ wrangler 3.57.1 (update available 3.60.3)
-------------------------------------------------------
interface Env {
MY_DB: D1Database;
}
NPM Scriptsにtypegenが追加されているので、こちらを実行しました。worker-configuration.d.tsを見ると、MY_DBという変数が追加されており、D1Database型であることがわかります(リスト6)。
// Generated by Wrangler on Sat Jun 15 2024 18:39:59 GMT+0900 (日本標準時)
// by runningwrangler types
interface Env {
MY_DB: D1Database;
}
ここで定義されたEnvインターフェースは、load-context.tsで参照され、contextの型を拡張するために使われます。この設定を終えると、loaderでリスト7のようなコードが書けるようになります。
import { LoaderFunctionArgs } from "@remix-run/cloudflare";
export function loader({ context }: LoaderFunctionArgs) {
const db = context.cloudflare.env.MY_DB;
// (省略)
}
これによって、TypeScriptからCloudflareの各種APIを利用する際にも、型安全なコードを書くことができるようになります。
このように、Create Cloudflare CLIを使うことで、Cloudflareの力を最大限活用するための設定が行われたRemixテンプレートを利用できます。npm create remix@latestで作成したプロジェクトを自分で設定していくのもアリですが、テンプレートの力を借りることで、Cloudflare社のおすすめ設定を取り込めるので、初めての方にはおすすめです。
