24
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

静的ホストでNext.jsの動的ルートをSPAにフォールバックする話

Last updated at Posted at 2020-12-11

Next.js Advent Calendar 2020の12日目の記事です。

2021/03/30 追記

Next.js 10.0.8から_resolveHrefメソッドがresolveDynamicRouteメソッドに変更されています。また、この関数はexportされなくなったのでアプリ側から叩く方法がなくなってしまいました。しかし、resolveDynamicRouteメソッドで使用しているNextのメソッドは全てexportされているため、アプリ側にresolveDynamicRouteを定義すれば今まで通り実装可能です。

はじめに

Next.jsは3の頃からルートを事前にレンダリングし、静的ファイルとしてエクスポートする機能があります。

SSRをした方がブログなど動的に追加されるコンテンツではパフォーマンスやOGPなどでメリットがあります。ただ、SSRをするインフラを構築して運用するのはなかなか大変なものです。

例えばログインしてからでないと動的ルートを使用しない場合などはOGPとか考えなくて良いので、静的ファイルとして吐き出し、初回ロード以外はSPAとして動作させてしまえばHTTPサーバーを立てるだけでアプリを公開できます。楽!

さらに、Next.js 9からファイルパスベースで動的ルートの定義ができるようになりました。

今まではnext.config.jsに静的に吐き出したいパスを定義する必要がありましたが、9からはpages/path/[id].jsxのようなファイルを作成することで/path/1といったルーティングを自動的に生成できるようになりました。すごい!

さあ満を持して、サーバーに静的ファイルをアップロードした!アクセスした!表示される!やったー!そしておもむろに/path/1をアドレスバーに打ち込んだ!

そこに表示されたのは…404!

そう、動的ルートは/path/[id].htmlとして吐き出されていたのでした。

本記事ではこれを解決しましょう。

解決案

ここでは考えうる解決策を比較しましょう。

サーバーでリライトする

この方法は既に @mottox2 さんの Next.jsのDynamic Routing + Static HTML exportを組み合わせて使う で解説されている通り、動的ルートにアクセスがきた場合サーバー側で対応するファイルにリダイレクトするものです。

Pros

  • サーバー設定だけでいいのでNext.js側の対応は不要

Cons

  • ルートが増える度に設定の書き換えが必要

動的ルートはSPAでルーティングする

この方法はnuxt.jsなどが採用しています。( https://ja.nuxtjs.org/docs/2.x/configuration-glossary/configuration-generate/#fallback )

動的ルートにアクセスされた時、カスタム404ページでフォールバック用ページを返します。

フォールバック用ページはSPAでルーティング可能なルートであればそのページに移動し、不可能なルートであれば404をレンダーします。

Pros

  • SPAにルーティングがあるのでルートが増えても自動対応できる

Cons

  • サーバーは404を返しているけど、正常にページがレンダリングされるので??ってなる

で、どっちにするん?

タイトルの通り、「動的ルートはSPAでルーティングする」案でいきます。諸事情で頻繁にサーバー設定が弄れない環境だったんです。

でも、そんな機能Next.jsにはないんですよ。

実装

じゃあ、作りましょう。なお、対象とするNext.jsは10.0.3とします。

下調べ

この方法のミソは「フォールバック用ページはSPAでルーティング可能なルートであればそのページに移動し、不可能なルートであれば404をレンダーします。」ってところです。

そのためには、今のパスとマッチするルートは何かという情報をクライアントで取得する必要があります。Next.jsがクライアントでそのような処理をしてそうなところの目星を付けます。

Next.jsの9.5.3以降ではLinkコンポーネントのasを指定しなくても自動的に動的ルートのページを推定してくれるようになりました。
つまり、pathnameからルートを判断する仕組みがありそうです。

Linkコンポーネントを追っていくとL221-229付近resolveHrefというメソッドを呼んでいますね。hrefからasを推定していそうな匂いがします。

あとは、これをページから呼べれば完璧です。

resolveHrefpackages/next/next-server/lib/router/router.tsで定義されているのでみてみるとパッケージとしてexportされていないので叩けなさそうです。
しかし、L1185-L1209付近にクライアントからも叩けるRouterクラスに_resolveHrefメソッドが定義されているではありませんか。

では叩いてみましょう。この_resolveHrefの第2引数にルート一覧を渡しますが幸運なことにL700付近でpageLoader.getPageListメソッドで取得できることがわかります。

pages/path/[id].jsx
import Router from "next/router";
import { useEffect, useState } from "react";

export default function Path() {
  const [resolvedRoute, setResolvedRoute] = useState(undefined);
  const [asPath, setAsPath] = useState("");
  useEffect(() => {
    (async () => {
      const pages = await Router.router.pageLoader.getPageList();
      setResolvedRoute(
        Router.router._resolveHref({ pathname: Router.asPath }, pages)
      );
    })();
  }, []);

  return <div>{JSON.stringify(resolvedRoute)}</div>;
}

やったー!ルートが取得できました。

スクリーンショット 2020-12-11 17.55.53.png

フォールバックページの実装

無事マッチするルートが取得できたので、カスタムエラーページを作成して、リダイレクトの処理を書いていきましょう。

_resolveHrefは存在しないルートでもとりあえず値を返すので、ルート一覧に存在する場合はリダイレクト、存在しない場合は404表示と切り分けています。

pages/404.jsx
import Router from "next/router";
import { useEffect, useState } from "react";
import Error from "next/error";

export default function NotFoundPage() {
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    (async () => {
      const pages = await Router.router.pageLoader.getPageList();
      const resolvedRoute = Router.router._resolveHref(
        { pathname: Router.asPath },
        pages
      );

      if (pages.includes(resolvedRoute.pathname)) {
        Router.replace(resolvedRoute.pathname, Router.asPath);
      } else {
        setIsError(true);
      }
    })();
  }, []);

  return <>{isError && <Error statusCode={404} />}</>;
};

動作テスト

ビルドします。

npm run build && ./node_modules/.bin/next export

nginxで生成したファイルをホスティングしてアクセスしてみます。404の時は404/index.htmlを返すようにします。

default.conf
server {
    listen       80;

    location / {
        root   /var/www;
        index  index.html index.htm;
        try_files $uri $uri/index.html =404;
    }
    error_page   404 /404/index.html
}

まずは動的ルートにアクセスして、リロードしてみます。

ezgif-7-c6628c821281.gif

次に存在しない動的ルートにアクセスしてみます。

スクリーンショット 2020-12-11 18.22.57.png

完璧!

おわりに

Next.jsの非公開APIを叩いてたり、Next.jsのいいところを無視している構成な気がしますが、同じ悩みを抱える誰かの助けになると嬉しいです。

あとbasePathの対応はしてないです。

ソースコードはこちら→https://github.com/ainehanta/fallback-spa-nextjs

24
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?