Open5

Remix学習メモ

へいぼんへいぼん

Cloudflareにデプロイしようと思うので、Cloudflare用のテンプレートを使用してプロジェクトを作成します。パッケージマネージャーはBunを使います。VS Codeで任意のディレクトリを開いて、以下のコマンドを実行します。

bunx create-remix@latest --template remix-run/remix/templates/cloudflare

プロジェクト名、gitで管理するかどうか(Yesが推奨のようですが自分はNoを選択)、依存関係をインストールするかを聞かれます。一通り終わったら、指定したプロジェクト名のディレクトリに移動します。以下のコマンドを実行します。

bun run dev

通常、localhost:5173でWebアプリが起動します。デフォルトのページが表示されることを確認して、準備が整いました。

https://remix.run/docs/en/main/guides/vite#cloudflare

へいぼんへいぼん

Cloudflareにデプロイできることを確認します。CLI(wranglerコマンド)でCloudflareの認証を行った後に、CLIからデプロイします。

bunx wrangler logout # 他のアカウントで既に認証済みの場合は実行しておくのが吉
bunx wrangler login

この後、ブラウザが起動してCloudflareへのログインが求められます。メールアドレスとパスワードを入力してログイン後、アクセス許否を聞かれるので、「許可」を選択します。

CLIからWebアプリのビルド、そしてデプロイをします。

bun run build
bun run deploy

デプロイの時に、プロジェクト名とブランチ名の入力を求められます。自分は、プロジェクト名はディレクトリ名と同一、ブランチ名は元から入力されていたproductionをそのまま使いました(ブランチ名は、GitHubと連動してデプロイする時に使用するもの?)

コマンドの実行が終わったら、公開先のURLが表示されるのでアクセスしてみます。ページが問題なく表示されたら完了です。反映まで少し時間がかかることもあるようです。

へいぼんへいぼん

WebアプリはPWAにしたいので、Remix PWAというライブラリを使用してプロジェクトをビルドしたいと思います。
https://remix-pwa.run/

Service Workerは利用せず、アプリのようにインストールできるようにします。

bun i --save-dev @remix-pwa/dev
# 2024-05-20時点で公式サイトのQuick Startに記載はないが必要
bun i --save-dev @remix-pwa/sw 
bun i --save @remix-pwa/worker-runtime

PWAに必要なファイル群を生成します。

bunx remix-pwa sw 
bunx remix-pwa manifest # WebManifestファイルを作成

https://remix-pwa.run/docs/main/web-manifest

この状態でビルドした後、デプロイを試みるとエラーが発生します。

✘ [ERROR] Could not resolve "stream"

    ../node_modules/stream-slice/index.js:3:24:
      3 │ var Transform = require('stream').Transform;
        ╵                         ~~~~~~~~

  The package "stream" wasn't found on the file system but is built into node.
  Add the "nodejs_compat" compatibility flag to your Pages project and make sure to prefix the module name with "node:" to enable Node.js compatibility.


✘ [ERROR] Build failed with 2 errors:

  ../node_modules/@remix-run/node/node_modules/cookie-signature/index.js:5:21: ERROR: Could not
  resolve "crypto"
  ../node_modules/stream-slice/index.js:3:24: ERROR: Could not resolve "stream"

Cloudflareのテンプレートを使ってプロジェクトを作ると、自動生成されたmanifest[.webmanifest].tsファイルの参照が合わないので見直します。

/app/routes/manifest[.webmanifest].ts
import type { WebAppManifest } from '@remix-pwa/dev';
-import { json } from '@remix-run/node';
+import { json } from "@remix-run/cloudflare";

export const loader = () => {
  return json(
    {
      short_name: 'PWA',
      name: 'Remix PWA',
      start_url: '/',
      display: 'standalone',
      background_color: '#d3d7dd',
      theme_color: '#c34138',
    } as WebAppManifest,
    {
      headers: {
        'Cache-Control': 'public, max-age=600',
        'Content-Type': 'application/manifest+json',
      },
    }
  );
};

root.tsxにmanifestを登録するコードを追加します。

/app/root.tsx
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";
+import { ManifestLink } from "@remix-pwa/sw";

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
+        <ManifestLink />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}

この後、bun run buildそしてbun run deployでデプロイして、スマホでページにアクセスした後にアプリを「ホームに追加」すると、PWAとして登録できます。

へいぼんへいぼん

/interestsにアクセスした時に、興味があることをカテゴリ別に表示したいと思います。

/app/routes/interests.tsx
import { json, type MetaFunction } from "@remix-run/cloudflare";
import { useLoaderData } from "@remix-run/react";

export const meta: MetaFunction = () => {
  return [
    { title: "New Remix App | Interests" },
    {
      name: "description",
      content: "My Interests",
    },
  ];
};

export function loader() {
  const interests = [
    {
      key: "勉強",
      value: ["Java", "TypeScript", "Rust"],
    },
    {
      key: "生活",
      value: ["料理", "掃除", "選択"],
    },
  ];
  return json({ interests });
}

export default function Interests() {
  const { interests } = useLoaderData<typeof loader>();
  return (
    <dl>
      {interests.map((interest, i) => (
        <>
          <dt key={i}>{interest.key}</dt>
          {interest.value.map((e, j) => (
            <dd key={j}>{e}</dd>
          ))}
        </>
      ))}
    </dl>
  );
}

loader関数内で表示したいデータを用意してreturn json({データ})で返します。実際に表示するコンポーネントでは、useLoaderDataフックを用いて、loader関数で返した値を扱うことができます。