跳到主要內容

中間件

中間件允許您在請求完成之前執行程式碼。然後,根據傳入的請求,您可以通過重寫、重定向、修改請求或回應標頭,或直接回應來修改回應。

中間件在快取內容和路由匹配之前執行。請參閱「路徑匹配」以了解更多詳細資訊。

使用案例

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

  • 身份驗證和授權:確保使用者身份並在授予訪問特定頁面或 API 路由之前檢查會話 Cookie。
  • 伺服器端重定向:根據特定條件(例如,語言環境、使用者角色)在伺服器層級重定向使用者。
  • 路徑重寫:通過根據請求屬性動態地將路徑重寫到 API 路由或頁面,來支援 A/B 測試、功能發布或舊版路徑。
  • 機器人偵測:通過偵測和封鎖機器人流量來保護您的資源。
  • 日誌記錄和分析:在頁面或 API 處理之前,捕獲和分析請求數據以獲得洞察。
  • 功能標記:動態地啟用或停用功能,以實現無縫的功能發布或測試。

認識到中間件可能不是最佳方法的狀況也同樣重要。以下是一些需要注意的情境:

  • 複雜的資料獲取和操作:中間件並非設計用於直接資料獲取或操作,這應該在路由處理器或伺服器端工具程式中完成。
  • 繁重的計算任務:中間件應該是輕量級的且能快速回應,否則可能會導致頁面載入延遲。繁重的計算任務或長時間運行的程序應該在專用的路由處理器中完成。
  • 廣泛的會話管理:雖然中間件可以管理基本的會話任務,但廣泛的會話管理應由專用的身份驗證服務或在路由處理器中管理。
  • 直接資料庫操作:不建議在中間件中執行直接資料庫操作。資料庫交互應在路由處理器或伺服器端工具程式中完成。

慣例

在您專案的根目錄中使用檔案 middleware.ts (或 .js) 來定義中間件。例如,與 pagesapp 在同一層級,或在 src 內部 (如果適用)。

注意:雖然每個專案僅支援一個 middleware.ts 檔案,您仍然可以模組化地組織您的中間件邏輯。將中間件功能分解為單獨的 .ts.js 檔案,並將它們匯入到您的主要 middleware.ts 檔案中。這可以更清晰地管理特定路由的中間件,並在 middleware.ts 中聚合以進行集中控制。通過強制使用單個中間件檔案,它可以簡化配置,防止潛在的衝突,並通過避免多個中間件層來優化效能。

範例

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) {
  return NextResponse.redirect(new URL('/home', request.url))
}
 
// See "Matching Paths" below to learn more
export const config = {
  matcher: '/about/:path*',
}

路徑匹配

中間件將會為您專案中的每個路由調用。鑑於此,至關重要的是使用匹配器來精確地定位或排除特定的路由。以下是執行順序:

  1. headersnext.config.js
  2. redirectsnext.config.js
  3. 中間件 (rewrites, redirects, 等等)
  4. beforeFiles (rewrites) 從 next.config.js
  5. 檔案系統路由 (public/, _next/static/, pages/, app/, 等等)
  6. afterFiles (rewrites) 從 next.config.js
  7. 動態路由 (/blog/[slug])
  8. fallback (rewrites) 從 next.config.js

有兩種方法可以定義中間件將在哪些路徑上運行:

  1. 自訂匹配器配置
  2. 條件語句

匹配器

matcher 允許您過濾中間件以在特定路徑上運行。

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

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

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

matcher 配置允許完整的 regex,因此支援像負向先行斷言或字元匹配這樣的匹配。 可以在此處看到一個負向先行斷言的範例,用於匹配除了特定路徑之外的所有路徑。

middleware.js
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
  ],
}

您也可以通過使用 missinghas 陣列,或兩者的組合來繞過某些請求的中間件。

middleware.js
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
 
    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      has: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
 
    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      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 的匹配器將會匹配。

條件語句

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

NextResponse

NextResponse API 允許您:

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

要從中間件產生回應,您可以:

  1. rewrite 到產生回應的路由(頁面路由處理器
  2. 直接返回 NextResponse。請參閱「產生回應

使用 Cookie

Cookie 是常規標頭。在 Request 中,它們儲存在 Cookie 標頭中。在 Response 中,它們在 Set-Cookie 標頭中。Next.js 提供了一種方便的方法,通過 NextRequestNextResponse 上的 cookies 擴展來訪問和操作這些 Cookie。

  1. 對於傳入的請求,cookies 具有以下方法:getgetAllsetdelete cookies。您可以使用 has 檢查 Cookie 是否存在,或使用 clear 移除所有 Cookie。
  2. 對於傳出的回應,cookies 具有以下方法:getgetAllsetdelete
middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // Assume a "Cookie:nextjs=fast" header to be present on the incoming request
  // Getting cookies from the request using the `RequestCookies` API
  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
 
  // Setting cookies on the response using the `ResponseCookies` API
  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: '/' }
  // The outgoing response will have a `Set-Cookie:vercel=fast;path=/` header.
 
  return response
}

設定標頭

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

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // Clone the request headers and set a new header `x-hello-from-middleware1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')
 
  // You can also set request headers in NextResponse.next
  const response = NextResponse.next({
    request: {
      // New request headers
      headers: requestHeaders,
    },
  })
 
  // Set a new response header `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}

须知:避免設定大型標頭,因為根據您的後端 Web 伺服器配置,這可能會導致 431 Request Header Fields Too Large 錯誤。

CORS

您可以在中間件中設定 CORS 標頭,以允許跨來源請求,包括簡單請求預檢請求

middleware.ts
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*',
}

须知:您可以在路由處理器中為個別路由配置 CORS 標頭。

產生回應

您可以通過返回 ResponseNextResponse 實例直接從中間件回應。(自 Next.js v13.1.0 起可用)

middleware.ts
import type { 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 }
    )
  }
}

waitUntil 和 NextFetchEvent

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+)/)
  ) {
    return NextResponse.redirect(
      new URL(`${req.nextUrl.pathname}/`, 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
}

單元測試 (實驗性)

從 Next.js 15.1 開始,next/experimental/testing/server 套件包含用於協助單元測試中間件檔案的工具程式。單元測試中間件可以幫助確保它僅在所需的路徑上運行,並且自訂路由邏輯在程式碼到達生產環境之前按預期工作。

unstable_doesMiddlewareMatch 函式可用於斷言中間件是否將針對提供的 URL、標頭和 Cookie 運行。

import { unstable_doesMiddlewareMatch } from 'next/experimental/testing/server'
 
expect(
  unstable_doesMiddlewareMatch({
    config,
    nextConfig,
    url: '/test',
  })
).toEqual(false)

整個中間件函式也可以進行測試。

import { isRewrite, getRewrittenUrl } from 'next/experimental/testing/server'
 
const request = new NextRequest('https://nextjs.dev.org.tw/docs')
const response = await middleware(request)
expect(isRewrite(response)).toEqual(true)
expect(getRewrittenUrl(response)).toEqual('https://other-domain.com/docs')
// getRedirectUrl could also be used if the response were a redirect

執行階段

中間件預設使用 Edge 執行階段。從 v15.2(canary 版)開始,我們實驗性地支援使用 Node.js 執行階段。要啟用,請將標誌添加到您的 next.config 檔案中

next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  experimental: {
    nodeMiddleware: true,
  },
}
 
export default nextConfig

然後在您的中間件檔案中,將 config 物件中的 runtime 設定為 nodejs

middleware.ts
export const config = {
  runtime: 'nodejs',
}

注意:此功能尚不建議用於生產環境。因此,除非您使用的是 next@canary 版本而不是穩定版本,否則 Next.js 將拋出錯誤。

版本歷史

版本變更
v15.2.0中間件現在可以使用 Node.js 執行階段(實驗性)
v13.1.0添加了進階中間件標誌
v13.0.0中間件可以修改請求標頭、回應標頭和發送回應
v12.2.0中間件已穩定,請參閱升級指南
v12.0.9在 Edge 執行階段中強制執行絕對 URL (PR)
v12.0.0添加了中間件(Beta 版)