中介軟體 (Middleware)

中介軟體 (Middleware) 允許您在請求完成前執行程式碼。然後,根據傳入的請求,您可以透過重寫、重新導向、修改請求或回應標頭,或直接回應來修改回應。

中介軟體會在快取內容和路由匹配之前執行。詳情請參閱 路徑匹配

使用情境

將中介軟體整合到您的應用程式中可以顯著提升效能、安全性和使用者體驗。以下是一些中介軟體特別有效率的常見情境:

  • 身份驗證與授權 (Authentication and Authorization):在授予特定頁面或 API 路由的存取權限前,確保使用者身份並檢查 session cookies。
  • 伺服器端重新導向 (Server-Side Redirects):根據特定條件(例如語言區域、使用者角色)在伺服器層級重新導向使用者。
  • 路徑重寫 (Path Rewriting):根據請求屬性動態重寫 API 路由或頁面的路徑,以支援 A/B 測試、功能發布或舊版路徑。
  • 機器人偵測 (Bot Detection):透過偵測和阻擋機器人流量來保護您的資源。
  • 記錄與分析 (Logging and Analytics):在頁面或 API 處理之前,擷取並分析請求資料以獲得洞察。
  • 功能標記 (Feature Flagging):動態啟用或停用功能,以實現無縫功能發布或測試。

識別中介軟體可能不是最佳解決方案的情境同樣重要。以下是一些需要注意的情境:

  • 複雜的資料擷取與操作 (Complex Data Fetching and Manipulation):中介軟體不適用於直接資料擷取或操作,這應在路由處理程式 (Route Handlers) 或伺服器端工具中完成。
  • 繁重的計算任務 (Heavy Computational Tasks):中介軟體應輕量且快速回應,否則可能導致頁面載入延遲。繁重的計算任務或長時間執行的程序應在專用的路由處理程式中完成。
  • 廣泛的 session 管理 (Extensive Session Management):雖然中介軟體可以管理基本的 session 任務,但廣泛的 session 管理應由專用的身份驗證服務或在路由處理程式中管理。
  • 直接資料庫操作 (Direct Database Operations):不建議在中介軟體中執行直接資料庫操作。資料庫互動應在路由處理程式或伺服器端工具中完成。

慣例

在專案根目錄中使用 middleware.ts (或 .js) 檔案來定義中介軟體。例如,與 pagesapp 同層級,或在適用的情況下放在 src 目錄內。

注意:雖然每個專案僅支援一個 middleware.ts 檔案,但您仍然可以模組化組織中介軟體邏輯。將中介軟體功能拆分到獨立的 .ts.js 檔案中,並將它們匯入到主要的 middleware.ts 檔案中。這樣可以更清晰地管理特定路由的中介軟體,並在 middleware.ts 中集中控制。透過強制使用單一中介軟體檔案,可以簡化配置、避免潛在衝突,並透過避免多層中介軟體來優化效能。

範例

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// 如果內部使用 `await`,此函式可以標記為 `async`
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}

// 詳情請參閱下方的「路徑匹配」
export const config = {
  matcher: '/about/:path*',
}

路徑匹配

中介軟體會針對專案中的每個路由被調用。因此,使用匹配器 (matcher) 來精確定位或排除特定路由至關重要。以下是執行順序:

  1. next.config.js 中的 headers
  2. next.config.js 中的 redirects
  3. 中介軟體 (rewritesredirects 等)
  4. next.config.js 中的 beforeFiles (rewrites)
  5. 檔案系統路由 (public/_next/static/pages/app/ 等)
  6. next.config.js 中的 afterFiles (rewrites)
  7. 動態路由 (/blog/[slug])
  8. next.config.js 中的 fallback (rewrites)

有兩種方式可以定義中介軟體將在哪些路徑上執行:

  1. 自訂匹配器配置
  2. 條件陳述式

匹配器 (Matcher)

matcher 允許您過濾中介軟體以在特定路徑上執行。

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

您可以使用陣列語法匹配單一路徑或多個路徑:

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

matcher 配置允許完整的正規表示式,因此支援如負向預查 (negative lookaheads) 或字元匹配等操作。以下是一個負向預查的範例,用於匹配除特定路徑外的所有路徑:

middleware.js
export const config = {
  matcher: [
    /*
     * 匹配所有請求路徑,除了以下開頭的路徑:
     * - api (API 路由)
     * - _next/static (靜態檔案)
     * - _next/image (圖片優化檔案)
     * - favicon.ico (favicon 檔案)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

您也可以透過使用 missinghas 陣列,或兩者的組合,來繞過中介軟體處理某些請求:

middleware.js
export const config = {
  matcher: [
    /*
     * 匹配所有請求路徑,除了以下開頭的路徑:
     * - api (API 路由)
     * - _next/static (靜態檔案)
     * - _next/image (圖片優化檔案)
     * - favicon.ico (favicon 檔案)
     */
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },

    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      has: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },

    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      has: [{ type: 'header', key: 'x-present' }],
      missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
    },
  ],
}

須知matcher 值必須是常數,以便在構建時進行靜態分析。動態值(如變數)將被忽略。

配置的匹配器:

  1. 必須以 / 開頭
  2. 可以包含命名參數:/about/:path 匹配 /about/a/about/b,但不匹配 /about/a/c
  3. 可以在命名參數上使用修飾符(以 : 開頭):/about/:path* 匹配 /about/a/b/c,因為 * 表示 零個或多個? 表示 零個或一個+ 表示 一個或多個
  4. 可以使用括號包裹的正規表示式:/about/(.*)/about/:path* 相同

詳情請參閱 path-to-regexp 文件。

須知:為了向後兼容,Next.js 始終將 /public 視為 /public/index。因此,/public/:path 的匹配器將會匹配。

條件陳述式

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))
  }
}

NextResponse

NextResponse API 允許您:

  • 將傳入的請求 redirect 到不同的 URL
  • 透過顯示給定的 URL 來 rewrite 回應
  • 為 API 路由、getServerSidePropsrewrite 目的地設定請求標頭
  • 設定回應 cookies
  • 設定回應標頭

要從中介軟體產生回應,您可以:

  1. rewrite 到產生回應的路由 (頁面路由處理程式)
  2. 直接回傳 NextResponse。請參閱 產生回應

使用 Cookies

Cookies 是常規的標頭。在 Request 中,它們儲存在 Cookie 標頭中。在 Response 中,它們儲存在 Set-Cookie 標頭中。Next.js 透過 NextRequestNextResponse 上的 cookies 擴充功能,提供了一種方便的方式來存取和操作這些 cookies。

  1. 對於傳入的請求,cookies 提供以下方法:getgetAllsetdelete cookies。您可以使用 has 檢查 cookie 是否存在,或使用 clear 移除所有 cookies。
  2. 對於傳出的回應,cookies 提供以下方法:getgetAllsetdelete
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // 假設傳入的請求中存在 "Cookie:nextjs=fast" 標頭
  // 使用 `RequestCookies` API 從請求中取得 cookies
  let cookie = request.cookies.get('nextjs')
  console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
  const allCookies = request.cookies.getAll()
  console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]

  request.cookies.has('nextjs') // => true
  request.cookies.delete('nextjs')
  request.cookies.has('nextjs') // => false

  // 使用 `ResponseCookies` API 在回應中設定 cookies
  const response = NextResponse.next()
  response.cookies.set('vercel', 'fast')
  response.cookies.set({
    name: 'vercel',
    value: 'fast',
    path: '/',
  })
  cookie = response.cookies.get('vercel')
  console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
  // 傳出的回應將包含 `Set-Cookie:vercel=fast;path=/` 標頭。

  return response
}

設定標頭

您可以使用 NextResponse API 設定請求和回應標頭(自 Next.js v13.0.0 起支援設定 請求 標頭)。

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // 複製請求標頭並設定新標頭 `x-hello-from-middleware1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')

  // 您也可以在 NextResponse.rewrite 中設定請求標頭
  const response = NextResponse.next({
    request: {
      // 新的請求標頭
      headers: requestHeaders,
    },
  })

  // 設定新的回應標頭 `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}

須知:避免設定過大的標頭,因為根據後端網頁伺服器的配置,可能會導致 431 請求標頭欄位過大 錯誤。

CORS

您可以在中介軟體 (Middleware) 中設定 CORS 標頭來允許跨來源請求,包括簡單請求預檢請求

import { NextRequest, NextResponse } from 'next/server'

const allowedOrigins = ['https://acme.com', 'https://my-app.org']

const corsOptions = {
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}

export function middleware(request: NextRequest) {
  // Check the origin from the request
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)

  // Handle preflighted requests
  const isPreflight = request.method === 'OPTIONS'

  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }

  // Handle simple requests
  const response = NextResponse.next()

  if (isAllowedOrigin) {
    response.headers.set('Access-Control-Allow-Origin', origin)
  }

  Object.entries(corsOptions).forEach(([key, value]) => {
    response.headers.set(key, value)
  })

  return response
}

export const config = {
  matcher: '/api/:path*',
}

小知識: 您可以在路由處理程式 (Route Handlers) 中為個別路由設定 CORS 標頭。

產生回應

您可以直接從中介軟體返回 ResponseNextResponse 實例來產生回應。(此功能自 Next.js v13.1.0 起可用)

import { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'

// Limit the middleware to paths starting with `/api/`
export const config = {
  matcher: '/api/:function*',
}

export function middleware(request: NextRequest) {
  // Call our authentication function to check the request
  if (!isAuthenticated(request)) {
    // Respond with JSON indicating an error message
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 }
    )
  }
}

waitUntilNextFetchEvent

NextFetchEvent 物件繼承了原生的 FetchEvent 物件,並包含 waitUntil() 方法。

waitUntil() 方法接受一個 promise 作為參數,並延長中介軟體的生命週期直到該 promise 完成。這對於在背景執行工作非常有用。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'

export function middleware(req: NextRequest, event: NextFetchEvent) {
  event.waitUntil(
    fetch('https://my-analytics-platform.com', {
      method: 'POST',
      body: JSON.stringify({ pathname: req.nextUrl.pathname }),
    })
  )

  return NextResponse.next()
}

進階中介軟體標記

在 Next.js v13.1 版本中,新增了兩個中介軟體標記 skipMiddlewareUrlNormalizeskipTrailingSlashRedirect 來處理進階使用案例。

skipTrailingSlashRedirect 停用 Next.js 用於新增或移除尾部斜線的重新導向。這允許在中介軟體中自訂處理方式,為某些路徑保留尾部斜線而不為其他路徑保留,這可以使漸進式遷移更加容易。

next.config.js
module.exports = {
  skipTrailingSlashRedirect: true,
}
middleware.js
const legacyPrefixes = ['/docs', '/blog']

export default async function middleware(req) {
  const { pathname } = req.nextUrl

  if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
    return NextResponse.next()
  }

  // apply trailing slash handling
  if (
    !pathname.endsWith('/') &&
    !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
  ) {
    req.nextUrl.pathname += '/'
    return NextResponse.redirect(req.nextUrl)
  }
}

skipMiddlewareUrlNormalize 允許停用 Next.js 中的 URL 標準化,使直接訪問和客戶端轉換的處理方式相同。在某些進階案例中,此選項透過使用原始 URL 提供完整控制。

next.config.js
module.exports = {
  skipMiddlewareUrlNormalize: true,
}
middleware.js
export default async function middleware(req) {
  const { pathname } = req.nextUrl

  // GET /_next/data/build-id/hello.json

  console.log(pathname)
  // with the flag this now /_next/data/build-id/hello.json
  // without the flag this would be normalized to /hello
}

執行環境

中介軟體目前僅支援 Edge 執行環境,無法使用 Node.js 執行環境。

版本歷史

版本變更
v13.1.0新增進階中介軟體標記
v13.0.0中介軟體可修改請求標頭、回應標頭和傳送回應
v12.2.0中介軟體功能穩定,請參閱升級指南
v12.0.9在 Edge 執行環境中強制使用絕對 URL (PR)
v12.0.0新增中介軟體 (Beta)

On this page