🌛

middlewareの再入門(matcher、条件分岐、rewrite、redirect)

2023/08/12に公開

1. はじめに

  • Next.js の middleware で処理対象となるパスの指定方法(matcher と条件分岐)について解説します。
  • NextResponse の rewriteredirect の違いについて解説します。

2. 結論

  • middleware とは、リクエストが完了する前にコードを実行できる仕組みです。
  • middleware.tsは、app あるいは pages ディレクトリと同じ階層に配置します。
  • matcher とは、middleware を適用するための URL のパターンを正規表現で指定します。
  • 条件分岐 は、middleware を適用するための URL のパターンを if 構文などで指定します。
  • rewrite は、リクエストを別の URL に書き換えます。ユーザーには書き換えた URL が見えません。
  • redirect は、リクエストを別の URL にリダイレクトします。ユーザーにはリダイレクト先の URL が見えます。

3. middlewareとは❓

middleware を利用すると、リクエストが完了する前にコードを実行できます。

https://nextjs.org/docs/app/building-your-application/routing/middleware

4. middlewareの配置位置

app あるいは pages ディレクトリと同じ階層に、middleware.ts(あるいは middleware.js)を配置します。

.
└── src
    └── app
    └── middleware.ts
.
└── src
    └── pages
    └── middleware.ts

5. middlewareが実行されるパス

middleware が実行される対象パスは以下の2つで指定します。

  • matcher の利用
  • 条件分岐

5.1. matcher を利用

matcher を利用することで、実行対象となる特定 URL パスを正規表現で指定できます。

正規表現は文字列のパターンマッチングに用いられる強力なツールです。正規表現を用いることで、特定の文字列のパターンを検索したり、置換したり、抽出したりできます。

5.1.1. 特定のURLパターンをマッチさせる

middleware.ts
export const config = {
  matcher: '/about/:path*',
}

この例では、特定の URL パスをマッチさせるためのパターンを示しています。ここでのパターンは、/about/ に続く任意のパスを表しています。

要素 説明
/about/ 文字列がこのパターンで始まる必要があります。
:path コロン(:)はプレースホルダーを示しており、ここでは任意の文字列を受け取ることができます。
* アスタリスク(*)は、プレースホルダーに対応する部分が0回以上繰り返されることを示します。

5.1.2. 複数のURLパターンをマッチさせる

middleware.ts
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

このコードは、配列を用いて 2 つのパスのパターンを定義しています。それぞれのパターンは、特定のルートにマッチするためのテンプレートを表しています。正規表現は先程と内容が同じのため説明を省略します。

5.2. 条件分岐

matcher を利用しない場合は、URL のパスを利用し、条件に応じて処理を分岐させます。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }
 
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}

6. NextResponse

middleware での処理の結果を返すために、NextResponse を利用します。

NextResponse で以下を実施できます。

  • redirect は、リクエストを別の URL にリダイレクトします。相手側にはリダイレクト先の URL が見えます。
  • rewrite は、リクエストを別の URL に書き換えます。相手側にはリダイレクト先の URL は見えません。
  • ヘッダーを設定します。
  • リクエストとレスポンスのクッキーを設定します。

7. 実装して確認

実装して確認して行きます。

7.1. Next.jsプロジェクトの新規作成し環境構築

作業するプロジェクトを新規に作成していきます。

長いので、折り畳んでおきます。

新規プロジェクト作成と初期環境構築の手順詳細
$ pnpm create next-app@latest nextjs-middleware-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd nextjs-middleware-sample

以下の通り不要な設定を削除し、プロジェクトの初期環境を構築します。

$ mkdir src/styles
$ mv src/app/globals.css src/styles/globals.css
src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
src/app/page.tsx
export default function Home() {
  return (
    <main className="text-lg">
      テストページ
    </main>
  )
}
src/app/layout.tsx
import '@/styles/globals.css'

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body className="">{children}</body>
    </html>
  );
}
tailwind.config.js
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  plugins: [],
};
export default config;
tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
+   "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

コミットします。

$ pnpm build
$ git add .
$ git commit -m "新規にプロジェクトを作成し, 作業環境を構築"

7.2. middlewareでmatcherを利用

/about/:path* にアクセスした場合、/home へリダイレクトする middleware を作成します。

$ touch src/middleware.ts
src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
  console.log(request.nextUrl.href);
  return NextResponse.redirect(new URL("/", request.url));
}

// See "Matching Paths" below to learn more
export const config = {
  matcher: "/about/:path*",
};

ページを作成します。

$ mkdir -p src/app/about/section/subsection
$ touch    src/app/about/page.tsx \
           src/app/about/section/page.tsx \
           src/app/about/section/subsection/page.tsx
$ mkdir -p src/app/dashboard/user/settings
$ touch    src/app/dashboard/page.tsx \
           src/app/dashboard/user/page.tsx \
           src/app/dashboard/user/settings/page.tsx

実装部分は長いので折り畳んで起きます。

ページ作成
src/app/about/page.tsx
export default function Home() {
  return (
    <main className="text-lg">
      /about
    </main>
  )
}
src/app/about/section/page.tsx
export default function Home() {
  return (
    <main className="text-lg">
      /about/section
    </main>
  )
}
src/app/about/section/subsection/page.tsx
export default function Home() {
  return (
    <main className="text-lg">
      /about/section/subsection
    </main>
  )
}
src/app/dashboard/page.tsx
export default function Home() {
  return (
    <main className="text-lg">
      /dashboard
    </main>
  )
}
src/app/dashboard/user/page.tsx
export default function Home() {
  return (
    <main className="text-lg">
      /dashboard/user
    </main>
  )
}
src/app/dashboard/user/settings/page.tsx
export default function Home() {
  return (
    <main className="text-lg">
      /dashboard/user/settings
    </main>
  )
}

開発サーバーを起動します。

$ pnpm dev

以下の URL にアクセスすると、http://localhost:3000 にリダイレクトされます。ブラウザに表示される URL はリダイレクト先の http://localhost:3000 となっています。

以下の URL にアクセスすると、リダイレクトは発生しません。

サーバログは以下の通り書き出されています。

http://localhost:3000/about
http://localhost:3000/about/section
http://localhost:3000/about/section/subsection

コミットします。

$ pnpm build
$ git add .
$ git commit -m "matcherの利用を確認"

7.3. middlewareで条件分岐を利用

1 つ前の middleware はバックアップしておき、新規に middleware を作成します。

$ mv src/middleware.ts src/middleware-1.ts
$ touch src/middleware.ts
src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about/section', request.url))
  }
 
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}

開発サーバーを起動します。

$ pnpm dev

http://localhost:3000/about にアクセスすると、URL は変更されず、http://localhost:3000/about/section が表示されます。

http://localhost:3000/dashboard にアクセスすると、URL は変更されず、http://localhost:3000/dashboard/user が表示されます。

それ以外のパスは、リダイレクトされません。

コミットします。

$ pnpm build
$ git add .
$ git commit -m "条件分岐とrewriteの利用を確認"

8. まとめ

  • Next.js の middleware で処理対象となるパスの指定方法(matcher と条件分岐)について解説しました。

  • NextResponse の rewriteredirect の違いについて解説しました。

  • 作業コードは以下です。
    https://github.com/hayato94087/nextjs-middleware-sample

9. 参考

Discussion