跳至內容

中介軟體

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

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

使用案例

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

  • 驗證和授權:在授予對特定頁面或 API 路由的存取權限之前,請確認使用者身分並檢查工作階段 Cookie。
  • 伺服器端重新導向:根據特定條件(例如:地區設定、使用者角色)在伺服器層級重新導向使用者。
  • 路徑改寫:根據請求屬性,將路徑動態地改寫到 API 路由或頁面,以支援 A/B 測試、功能推出或舊版路徑。
  • 機器人偵測:透過偵測和封鎖機器人流量來保護您的資源。
  • 記錄和分析:在頁面或 API 處理之前擷取和分析請求數據以取得洞察分析。
  • 功能標記:動態啟用或停用功能,以實現無縫的功能推出或測試。

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

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

慣例

在專案根目錄中使用 `middleware.ts`(或 `.js`)檔案來定義中介軟體。例如,與 `pages` 或 `app` 位於同一層級,或者如果有的話,位於 `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. 中介軟體 (rewrites(重寫), redirects(重新導向)等等)
  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. 條件語句

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

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

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

matcher 設定允許完整的正規表達式,因此支援像是否定先行斷式或字元匹配等匹配方式。這裡可以看到一個使用否定先行斷式來匹配除特定路徑之外所有路徑的範例。

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 讓您可以:

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

  1. 重寫到產生回應的路由(頁面Edge API 路由
  2. 直接返回 NextResponse。請參閱產生回應

使用 Cookie

Cookie 是普通的標頭。在請求中,它們儲存在 Cookie 標頭中。在回應中,它們位於 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
}

注意事項:避免設定過大的標頭,因為這可能會導致 431 請求標頭欄位過大 錯誤,具體取決於您的後端網路伺服器設定。

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 自動新增或移除尾端斜線的重新導向功能。這讓您可以在 middleware 中自訂處理,保留某些路徑的尾端斜線,而移除其他路徑的,使漸進式遷移更加容易。

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
}

執行環境

Middleware 目前僅支援與 Edge 執行環境 相容的 API。Node.js 專屬的 API 不支援

版本歷史記錄

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