0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js14でMPAとSPAを混在させてPWAを疎結合する

Posted at

Why such fucking waste?

Next.jsアプリにPWA(Progressive Web Apps)を機能追加した際に、問題がありました。iPhone環境で画面遷移した際にURLバーが出てきてしまい、画面遷移でリロードを行うとダメなのでしょう。

単純に考えてSPA(Single-page application)にすれば解決しそうですが、Next.jsを採用しているため、全体をSPAにするというのは、non senseです。

そのため、Next.jsのApp RouterReact Routerを組み合わせて部分的にSPAにする構成に変更しました。この記事の方法を採用すればMPAとSPAを自由に組み合わせられるのでNext.jsでの自由度が高まりそうです。

環境

  • Vercel CLI 41.1.3
  • Next.js: 14.2.15
  • next-pwa: 5.6.0
  • react-router-dom: 7.1.3
  • iOS: 18.1.1

導入手順

1.諸々インストール

# Next.jsアプリを新規作成
npx create-next-app@latest

# PWAライブラリ
npm install next-pwa

# SPAのルーター
npm install react-router-dom

2.ルーティング

前提として、ルート配下は通常のApp Routerとなります。

クライアントサイドルーティング

SPAのルートパス(今回は/app/pwa)にReact Routerを展開します。

page.tsx
"use client";

import { Route, BrowserRouter as Router, Routes } from "react-router-dom";

// ...

const PWARouter = () => {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  // Hydrationエラーになるのでマウント前は描画しない
  if (!mounted) {
    return null;
  }

  return (
    <Router>
      <Routes>
        <Route path="/pwa/" element={<Menu Header={SPAHeader} />}/>
        <Route path="/pwa/feat1/" element={<Feat1 Header={SPAHeader} />}/>
        <Route path="/pwa/feat2/" element={<Feat2 Header={SPAHeader} />}/>
      </Routes>
    </Router>
  );
};
export default PWARouter;

propsで渡しているHeaderコンポーネントは、SSGではuseRouter、SPAではuseNavigateをそれぞれラップしたコンポーネントです。
Routerが別物となるので、フックを呼び出した時点でエラーを吐きます。MPAとSPAでコードを切り分ける場合はコンポーネントレベルで分離が必要となります。

サーバーサイドルーティング

next.config.mjs
const nextConfig = {
  async rewrites() {
    // ブラウザのURLは変わらず、/pwa/にルーティングされる
    return [{ source: "/pwa/(.*)", destination: "/pwa/" }];
  },
};

/pwa配下の各ページはNext.jsから見ると何も存在しないため、App Routerに対して/pwa/feat1のリクエストが飛んできた際、デフォルトではエラーページを返します。
このため、サーバ側のルーティング設定で/pwa/...以下のリクエストをいったん/pwaに当てる処理が必要となります。

rewritesオプションを付与することで、上記の動きを実現できます。rewritesは、ブラウザから見えるパスとサーバーが返すページを自由に設定するためのオプションです。
サーバーからページコンテンツを受け取った後で、ブラウザ側でURLが評価されてルーティングが実行される流れです。

3.Service Worker

Service Workerはスクリプトをサブスレッドで実行するための仕組みです。プッシュ通知やバックグラウンドの非同期処理など、通常のウェブアプリケーションでは扱えない低レイヤーの処理を実装できるようになります。PWAではキャッシュの管理が主な役割となります。

Service Workerはjsをそのまま書くこともできますが、キャッシュ管理は初心者には難解なため、ライブラリを使うことをおすすめします。今回はnext-pwaを採用しています。

next.config.mjs
/** @type {import('next').NextConfig} */
import nextPWA from "next-pwa";

const withPWA = nextPWA({
  dest: "public",
  register: true,
  skipWaiting: true,
  // ローカルではwarningを吐くのでnext-pwaを無効にします
  disable: process.env.NODE_ENV === "development",
});

const nextConfig = withPWA({
  reactStrictMode: true,
  trailingSlash: true,
  async rewrites() {
    return [{ source: "/pwa/(.*)", destination: "/pwa/" }];
  },
});

export default nextConfig;

next-pwaがビルド毎にjsを生成するのでignoreしておきます。

.gitignore
# next-pwa
**/public/sw.js
**/public/sw.js.map
**/public/workbox-*.js
**/public/workbox-*.js.map

4.その他PWAの設定

manifest.json

  • name: ホーム画面に表示されるアプリの名前です
  • start_url: PWAで初期設定されるURL
  • icons: 配列中の一番近いサイズが自動で使われます
  • display: standaloneに設定するとウインドウがブラウザと別になり、URLバーも非表示になります
  • theme_color: URLバーの色指定
  • background_color: コンテンツ背景の色指定
manifest.json
{
  "name": "sample-app",
  "short_name": "sample-app",
  "start_url": "/pwa/", 
  "icons": [
    {
      "src": "/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "theme_color": "#2563eb",
  "background_color": "#2563eb",
  "display": "standalone"
}

headタグ

以下のmetaタグが重要です。iPhoneなど一部のプラットフォームでPWAとしてインストールしたアプリをフルスクリーンで開かせるには、manifestの設定だけでは不十分です。

layout.tsx
      <head>
        <link rel="manifest" href="/manifest.json" />
        {/*iOS用*/}
        <meta name="apple-mobile-web-app-capable" content="yes" />
        {/*Android用*/}
        <meta name="mobile-web-app-capable" content="yes" />
      </head>

キャッシュコントロール

Next.jsのmiddlewareという機能を使います。middlewareとは、Next.jsにリクエストが処理される前に実行されるレイヤーです。プロキシに似た役割ですね。
next.config.mjsでも静的なキャッシュ管理は可能ですが、動的なキャッシュ管理をしたい場合はmiddlewareが適切です。

middleware.ts
import { NextResponse } from "next/server";

import type { NextRequest } from "next/server";

export function middleware(_request: NextRequest) {
  const res = NextResponse.next();
  res.headers.set("Cache-Control", "public, max-age=31536000, immutable");
  return res;
}

export const config = {
  matcher: ["/pwa/:path*", "/favicon.ico"],
};

デプロイ

最後にVercelにデプロイしてください。
ローカルでは基本的に動かないと思います。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?