跳到內容

中介軟體

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

中介軟體在快取內容和路由比對之前執行。請參閱比對路徑以取得更多詳細資訊。

使用案例

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

  • 身份驗證和授權:確保使用者身份並檢查工作階段 Cookie,然後再授予對特定頁面或 API 路由的存取權。
  • 伺服器端重新導向:根據特定條件(例如,地區設定、使用者角色)在伺服器層級重新導向使用者。
  • 路徑重寫:透過根據請求屬性動態重寫 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. 來自 next.config.jsheaders
  2. 來自 next.config.jsredirects
  3. 中介軟體 (rewritesredirects 等)
  4. 來自 next.config.jsbeforeFiles (rewrites)
  5. 檔案系統路由 (public/_next/static/pages/app/ 等)
  6. 來自 next.config.jsafterFiles (rewrites)
  7. 動態路由 (/blog/[slug])
  8. 來自 next.config.jsfallback (rewrites)

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

  1. 自訂比對器設定
  2. 條件陳述式

Matcher

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
  • rewrite 透過顯示給定的 URL 來重寫回應
  • 設定 API 路由、getServerSidePropsrewrite 目標的請求標頭
  • 設定回應 Cookie
  • 設定回應標頭

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

  1. rewrite 到產生回應的路由 (頁面或 Edge API 路由)
  2. 直接傳回 NextResponse。請參閱產生回應

使用 Cookie

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

  1. 對於傳入的請求,cookies 隨附以下方法:getgetAllsetdelete Cookie。您可以使用 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*',
}

產生回應

您可以透過傳回 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 }
    )
  }
}

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+)/)
  ) {
    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 物件中將執行階段設定為 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)