SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Remixを通じてWebを学ぶ

Remixでより高度なWebページを構築しよう──ルートモジュールの設定項目をフル活用する

Remixを通じてWebを学ぶ 第8回

  • X ポスト
  • このエントリーをはてなブックマークに追加

ルートの各種の値を定義するための設定

 過去2回にわたって、ルートモジュールの設定項目を解説してきましたが、残りは4つとなりました。今回解説するのは、ルートの各種の値を定義するための設定です。表2にまとめました。

表2 ルートの各種の値を定義するための設定
名称 種別 概要
headers 関数 HTTPヘッダーを定義してCache-Controlなどを制御する
links 関数 <link>要素を定義してCSSなどを制御する
meta 関数 <title>要素や<meta>要素を定義する
handle オブジェクト パンくずリストなどに載せたい自ページの情報を登録する

 基本的に、HTMLの <head> 要素や、HTTPヘッダーに働きかけるような設定を行うためのものです。それぞれの使い方を見ていきましょう。

headers

 headers はブラウザへのHTTPレスポンスのヘッダーを定義するための関数です。リスト1のように定義します。

[リスト1]app/routes/headers.jsx
export const headers = ({
  // (2)
  actionHeaders,
  errorHeaders,
  loaderHeaders,
  parentHeaders,
}) => ({
  "Cache-Control": "max-age=300", // (1)
});

 headers の戻り値は、(1)のようなHTTPヘッダーのオブジェクトです。何も参考にせずに直接記載しても構いませんし、(2)のように引数で受け取れる各種ヘッダーを参考に組み立てることもできます。引数から受け取れる値は次の通りです。

  • actionHeaders: actionclientAction の戻り値に設定されたヘッダー
  • errorHeaders: loaderaction の実行中に発生したエラーレスポンスに設定されたヘッダー
  • loaderHeaders: loaderclientLoader の戻り値に設定されたヘッダー
  • parentHeaders:親ルートモジュールの headers 関数の戻り値

 Nested Routesを組んでいるときに着目したいのは、 parentHeaders です。これは、親ルートモジュールの headers 関数の戻り値を参照するためのプロパティです。まず、前提条件として、Remix v2では、より深い階層のルートモジュールの設定が優先されます。

 例えば、親ルートモジュールの headers 関数の戻り値に(1)と同様に Cache-Control: max-age=300 というヘッダーが設定されていた場合に、子ルートモジュールの headers 関数の戻り値に Cache-Control: s-maxage=3600 というヘッダーを設定するとしましょう。

 すると、最終的にブラウザへ渡るレスポンスヘッダーは Cache-Control: s-maxage=3600 のみとなります。max-age はクライアント側のキャッシュ時間、 s-maxage はCDNやプロキシなどのキャッシュ時間で、少し役割が違うので、どちらも尊重して残したいところです。そのような場合には、 parentHeaders を使って、親ルートモジュールの設定を参照しながら子ルートモジュールのヘッダーを組み立てることになります。リスト2のように定義します。

[リスト2]app/routes/headers.child.jsx
import { json } from "@remix-run/node";
import parseCacheControl from "parse-cache-control";

export const headers = ({
  loaderHeaders,
  parentHeaders,
}) => {
  // (2)
  const loaderCache = parseCacheControl(
    loaderHeaders.get("Cache-Control")
  );
  const parentCache = parseCacheControl(
    parentHeaders.get("Cache-Control")
  );

  // (3)
  const maxAge = Math.min(
    loaderCache["max-age"],
    parentCache["max-age"]
  );

  // (4)
  const sMaxAge = loaderCache["s-maxage"]

  return {
    // (5)
    "Cache-Control": `s-maxage=${sMaxAge}, max-age=${maxAge}`,
  };
};

export const loader = () => {
  return json({
    message: "Hello, I'm loader!",
  }, {
    headers: {
      // (1)
      "Cache-Control": "max-age=500, s-maxage=3600",
    },
  });
};

 リスト2である /headers/childloader で、(1)のように Cache-Control: max-age=500, s-maxage=3600 というヘッダーを設定しています。もし headers を使わない場合には、このヘッダーがそのままブラウザに渡されることになりますが、今回は親ルートモジュールの headers と組み合わせて、より適切なヘッダーを設定することを考えてみましょう。

 まずは(2)のように、 loaderHeadersparentHeaders から Cache-Control ヘッダーを取得します。ヘッダーの情報をパースしてオブジェクトとして扱うために parse-cache-control というライブラリを使用しましたが、他のものでも構いません。

 次に、(3)のように、 loaderparent の両方に設定されている max-age の値を取り出し、より小さい方を採用します。

 そして、(4)のように、 s-maxageloader に設定されている値をそのまま採用します。

 最後に、(5)のように、 Cache-Control ヘッダーを組み立てて返すことになります。最終的には図2のように、 Cache-Control: max-age=300, s-maxage=3600 というレスポンスヘッダーが設定されることになります。

図1:子ルートモジュールのheadersの戻り値がレスポンスヘッダーになる
図1:子ルートモジュールのheadersの戻り値がレスポンスヘッダーになる

 このように、 parentHeadersloaderHeaders の間で折り合いをつけることで、より適切なヘッダーを設定することができます。

links

 links は、 <link> 要素を定義するための関数です。リスト3のように定義します。

[リスト3]linksの簡単な例
export const links = () => [
  // <link rel="stylesheet" href="/styles.css"> を定義する
  { rel: "stylesheet", href: "/styles.css" }, 
  // <link rel="icon" href="/favicon.ico"> を定義する
  { rel: "icon", href: "/favicon.ico" },
];

 このルートで使用したいCSSファイルやfaviconを定義するために使います。戻り値は、 <link> 要素の属性を定義したオブジェクトの配列です。この配列の要素がそのまま <link> 要素として出力されます。href のパスを / から始めているので、 public フォルダ直下のファイルを参照する形になりますね。

 href にはファイルの絶対パスを書くこともできますが、相対パスでファイルを参照することもできます。たとえば、図2のように、 app/styles フォルダにCSSファイルを、 app/images フォルダにアイコンを配置した場合のことを考えてみましょう。

図2:route.jsxとリソースを同じフォルダに配置する
図2:route.jsxとリソースを同じフォルダに配置する

 このとき、リスト4のように styles.cssfavicon.ico を指定できます。

[リスト4]app/routes/links/route.jsx
// CSSファイルやfaviconをインポートする
import styles from '../styles/styles.css';
import favicon from '../images/favicon.ico';

export const links = () => [
  // hrefでCSSファイルやfaviconを指定する
  { rel: "stylesheet", href: styles }, 
  { rel: "icon", href: favicon },
];

 このとき、CSSファイルやアイコンは、ビルド時に適切なフォルダ(/build/_assets など)に移動され、そのパスが <link> 要素に埋め込まれます。実際に開発者ツールで確認してみると、図3のように、CSSファイルやアイコンが正しく読み込まれていることが確認できます。

図3:ファイルをインポートする形でもCSSやアイコンが適用される
図3:ファイルをインポートする形でもCSSやアイコンが適用される

 このように、links を使うことで、 <link> 要素を柔軟に構築できます。詳細は公式ドキュメントを参照してください。

meta

 meta は、 <title> 要素や <meta> 要素を定義するための関数です。リスト5のように定義します。

[リスト5]app/routes/meta.jsx
export const meta = ({
  // (3)
  location,
  matches,
  data,
  params,
  error,
}) => [
  // <title>要素を定義する
  { title: "Remix meta function!" }, // (1)
  // <meta name="description" content="Remix is ..." /> を定義する
  { name: "description", content: "Remix is ..." }, // (2)
];

 このルートで使用したい <title> 要素や <meta> 要素を定義するために使います。<title> 要素だけは(1)のように特別なデータ構造を持っていますが、それ以外は基本的に(2)のように <meta> 要素のための name 属性と content 属性を定義したオブジェクトを記述することになります。リスト5の設定で実際に画面を表示すると、図4のようになります。

図4:meta関数でタイトルとdescriptionが設定された
図4:meta関数でタイトルとdescriptionが設定された

 <title> 要素や <meta> 要素が構築されていますね。また、(3)のように <meta> 要素を動的に構築するための引数を受け取ることもできます。受け取れる値は次の通りです。

  • location:現在のルートのLocationを表すデータ
  • matches:親ルートモジュールの情報などが格納されている
  • data:loader の戻り値
  • params:URLパスのパラメーター
  • error:ErrorBoundary でキャッチしたエラー

 locationparams を使えばURLに付与された各種パラメータを利用できますし、 data を使えば loader で取得したデータを利用できるので、ページの内容に合わせた <meta> 要素を動的に構築することができます。また、Nested Routesを組んである場合は matches を使用することで親ルートモジュールの情報を参照することもできます。詳細は公式ドキュメントを参照してください。

handle

 最後に、 handle について解説します。これは、少し特殊な項目で、Nested Routesの各階層の情報を useMatches フックで集約するためのものです。リスト6のように定義します。

[リスト6]app/routes/handle.child.grandchild.jsx
export const handle = {
  breadcrumb: () => 
    <Link to="/handle/child/grandchild">Handle Grand Child</Link>,
};

 このような情報をNested Routesの各階層に設置しておくと、 useMatches フックの戻り値として、そのとき表示している(=マッチしている)ルートモジュールの、すべてのhandleを取得することができます。この仕組みを活用すると、公式ドキュメントのBreadcrumbs Guideのように、パンくずリストを容易に作成することができます。

 簡単な例を示してみましょう。日本の都道府県を、地方区分ごとに表示するページを作成するとします。図5〜7のように、地方区分のリンクからドリルダウンしていく形で、各都道府県のページに遷移するイメージです。

図5:/handle のトップページ
図5:/handle のトップページ
図6:/handle/$region の地方区分のページ
図6:/handle/$region の地方区分のページ
図7:/handle/$region/$pref の都道府県のページ
図7:/handle/$region/$pref の都道府県のページ

 これらを実現するために、次の5つのルートファイルを作成しました。ファイル名とURLパスの関係については、第5回を参照してください。

  • app/routes/handle.jsx:パンくずリストを表示するためのレイアウトを定義する
  • app/routes/handle._index.jsx:トップページで地方区分一覧を表示する
  • app/routes/handle.$region.jsx:地方区分ひとつ分のページのためのレイアウトを定義する
  • app/routes/handle.$region._index.jsx:地方区分ひとつ分の都道府県一覧を表示する
  • app/routes/handle.$region.$pref.jsx:都道府県ひとつ分のページ

 これらのうち、リスト6のような形式の handle を定義してあるファイルは次の3つです。

  • app/routes/handle.jsx
  • app/routes/handle.$region.jsx
  • app/routes/handle.$region.$pref.jsx

 それぞれ、どんな定義をしているか見てみましょう。まずは、 handle.jsx です(リスト7)。

[リスト7]app/routes/handle.jsx
export const handle = {
  breadcrumb: () => <Link to="/handle">トップページ</Link>,
};

 このファイルは、自分自身(のインデックスページ)であるパンくずリストのトップページへのリンクを定義しています。次に、 handle.$region.jsx です(リスト8)。

[リスト8]app/routes/handle.$region.jsx
export const loader = async ({ params }) => {
  // $region で渡された地方区分のキーを取得
  const regionKey = params.region;

  // 地方区分の詳細データを取得
  const region = japan.find((item) => item.key === regionKey);

  if (!region) {
    return json({
      status: 404,
      message: 'Not Found',
    }, {
      status: 404,
    });
  }

  return json({
    region,
  });
};

export const handle = {
  // (1) loaderの戻り値を参照してパンくずリストを表示する
  breadcrumb: ({ data }) =>
    <Link to={`/handle/${data.region.key}`}>{data.region.name}</Link>,
};

 こちらも、 /handle/tohoku のような形でアクセスされたときに、その地方区分のページへのリンクを生成するために handle を定義しています。

 注目すべきは(1)で引数に data を受け取っていることです。これは、 loader の戻り値を参照するためのもので、 loader で取得したデータを使ってパンくずリストを生成することができます。これによって、URLのパスからはキーだけを受け取りつつ、パンくずリストにはデータの名前を表示する、といった処理が可能です。これはフレームワークのデフォルトの挙動ではなく、上位の階層でパンくずリストを構成するときの工夫によって実現されています。リスト10で解説するので、今は「こういうこともできる」ということだけ覚えておいてください。

 最後に、 handle.$region.$pref.jsx です(リスト9)。

[リスト9]app/routes/handle.$region.$pref.jsx
export const loader = async ({ params }) => {
  const regionKey = params.region;
  const prefKey = params.pref;

  const region = japan.find((item) => item.key === regionKey);
  const pref = region?.prefs.find((item) => item.key === prefKey);

  if (!region || !pref) {
    return json({
      status: 404,
      message: 'Not Found',
    }, {
      status: 404,
    });
  }

  return json({
    region,
    pref,
  });
};

export const handle = {
  breadcrumb: ({ data }) =>
    <Link to={`/handle/${data.region.key}/${data.pref.key}`}>
      {data.pref.name}
    </Link>,
};

 リスト8とほぼ同等の内容になっていますね。

 これらのデータを取りまとめて、パンくずリストを作成しているのが、最上位の handle.jsx です。パンくずリストを作成している部分をリスト10で見てみましょう。

[リスト10]app/routes/handle.jsx
import { Link, Outlet, useMatches } from "@remix-run/react";
// (省略)
export default function HandleLayout() {
  // (1) マッチしたルートモジュールの情報を取得
  const matches = useMatches();

  // (2) パンくずリスト用のデータがあるルート情報のみを抽出
  const breadcrumbMatches = matches.filter((match) =>
    match.handle && match.handle.breadcrumb);

  return (
    <div>
      <h1>日本の都道府県</h1>
      <nav className="breadcrumb">
        <ul>
        {/* (3) */}
        {breadcrumbMatches.map((match, index) => (
          <li key={index}>
            {match.handle.breadcrumb(match)}{/* (4) */}
          </li>
        ))}
        </ul>
      </nav>
      <hr />
      <Outlet />
    </div>
  )
}

 まず、(1)で useMatches フックを使って、現在表示中のURLパスにマッチしたルート(route)モジュールの情報を配列を取得しています。ここに含まれる情報については公式ドキュメントを参照してください。

 (2)では、パンくずリスト用のデータがあるルート情報のみを抽出しています。これは、 breadcrumb プロパティを含む handle オブジェクトがルートモジュールに定義されている情報、つまりパンくずリストの表示対象だけを取り出していることになります。

 (3)で実際にパンくずリストを構築します。(4)の match.handle.breadcrumb() 関数を実行することで、各ルートモジュールの handle に定義した <Link> 要素が展開されることになり、画面に各階層のリンクが表示されるのです。

 ここで注目してほしいのが、 breadcrumb(match) のように、引数に match を渡しているところです。この match には、各ルートモジュールで loader 関数が実行された結果が格納されています。ここで引数にデータを渡していることによって、リスト8のように loader で取得したデータを使ってパンくずリストを構築することができるわけです。

 Nested Routesを活かした、強力な仕組みだとは思うのですが、まだ公式側もパンくずリストくらいしか活用方法を例示できていないので、今後の展開に期待したいところです。

まとめ

 3回にわたって、ルートモジュールの設定項目を解説してきました。フレームワークのAPIとしてはすっきりした形になりつつも、古き良きブラウザとサーバーの関係を踏襲して、HTTPやHTMLの設定項目を柔軟に扱えるようになっているRemixの特徴が出ていたと思います。これらの設定項目を活用して、より高度なWebページを構築していきましょう。

 次回は、実際のインフラにデプロイしながら、Remixで作ったアプリケーションを公開する方法について解説します。

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
Remixを通じてWebを学ぶ連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 中川幸哉(ナカガワユキヤ)

WINGSプロジェクトについて> 有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS Twitter: @yyamada(公式)、@yyamada/wings(メンバーリスト) Facebook

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるReact実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/19528 2024/05/29 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング