紀錄工作經驗、相關知識,解決技術相關問題。

Javascript, Next, React, 工作雜談, 新手 Jamp Holes, 網站相關

Next.js Pages Router 載入順序解析:middleware、_app 與 _document 差異與作用

Next.js Pages Router 載入順序解析:middleware、_app 與 _document 差異與作用 特色圖片

掌握 Next.js 載入順序是每位前端開發者在進入 Pages Router 架構時必經的基礎核心。在 Next.js 的生態系中,middleware.js、_app.js、_document.js 與 index.js 等特殊檔案各自在請求生命週期的不同階段扮演關鍵角色。理解這些檔案的執行先後順序與運作機制,能幫助你在正確的層級處理權限驗證、全域狀態、HTML 結構與頁面渲染,大幅提升應用程式的穩定度與架構可維護性。

⚡ 快速跳轉:

整體執行順序

Next.js 的執行順序與載入機制需要分兩種情境來看:

⚠️ 架構防雷提醒:本文探討的 _app.js 與 _document.js 是 Next.js 傳統 Pages Router 架構(位於 pages/ 目錄)的專屬核心檔案。如果你的專案是採用 Next.js 13+ 預設的 App Router 架構(位於 app/ 目錄),這兩個檔案已被全新設計的 layout.js 與 template.js 取代,請務必確認專案目錄結構再進行設定。

1. 當用戶首次發出 HTTP 請求(例如訪問 https://example.com/)時的執行順序:

  1. middleware.js:位於專案根目錄或 src 目錄,負責在請求抵達路由之前進行攔截、重新導向或修改 headers。
  2. _app.js:負責管理全域狀態與共用 CSS 樣式,包裹所有渲染的頁面元件。
  3. _document.js:只在伺服器端執行,用來擴展與客製化 HTML 文件的基本結構(如 <html>、<head> 與 <body>)。
  4. index.js:實際對應路由並渲染頁面內容的目標元件。
首次 HTTP 請求的執行資料流向
圖:首次 HTTP 請求的執行資料流向

2. 客戶端路由切換(Client-Side Transition)時的執行順序:

當用戶已經在網站內,點擊連結切換到其他頁面時:

  1. middleware.js 不會執行(除非透過 Middleware 進行 API 路由攔截,一般頁面切換由前端 Router 處理)。
  2. _app.js 仍然存在並持續運行(不會重新掛載,僅切換內部的 Component)。
  3. _document.js 不會重新載入(HTML 骨架已經在首次請求時建立)。
  4. 目標頁面元件(如 index.js 或其他頁面)會透過非同步載入並重新渲染。
客戶端路由切換的資料流向與生命週期反應
圖:客戶端路由切換的資料流向與生命週期反應

3. 生命週期執行階段與行為對照表

檔案名稱首次 SSR 請求執行狀態客戶端路由切換執行狀態主要職責與運行環境
middleware.js每次請求最先執行預設不執行(除非觸發特定 API 路由)請求攔截、權限驗證、邊緣運算 (Edge) 重新導向
_app.js伺服器端與客戶端皆初始化持續運行(不重新掛載,僅切換元件)全域 Layout 包裹、Context Provider、狀態維持
_document.js僅在伺服器端執行一次不執行客製化 HTML 骨架、<head> 屬性注入、SSR 結構
目標頁面 (index.js)伺服器端渲染後傳送至客戶端客戶端非同步載入與渲染頁面專屬 UI 與資料抓取 (Data Fetching)

middleware.js 說明與實作

middleware.js 主要用來在請求完成前進行攔截與邏輯判斷,例如權限驗證、地理位置偵測或動態重新導向。

Middleware 模組架構與執行生命週期
圖:Middleware 模組架構與執行生命週期

簡單解釋:
這就像是機場的安檢站,在乘客登機前(進入應用程式)先檢查護照與行李,決定是否讓乘客順利通關、轉搭其他航班(重新導向),或是直接拒絕訪問。

載入時機:
在每個 HTTP 請求進入應用程式時最先執行,並且在路由匹配與伺服器處理之前運行。

middleware.js 基本程式碼範例

import { NextResponse } from 'next/server';

export function middleware(request) {
  const token = request.cookies.get('token');

  // 如果沒有 token 且嘗試訪問保護頁面,則重定向到登入頁
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

// 設定只有特定路徑會觸發此 Middleware
export const config = {
  matcher: ['/dashboard/:path*'],
};

_document.js 說明與實作

_document.js 用來擴展 Pages Router 中的 HTML 文件的基本結構,例如自訂 <html> 標籤的語系、載入外部字型或注入第三方指令碼。

方案 2:客戶端導航與 SSR 執行條件判斷
圖:方案 2:客戶端導航與 SSR 執行條件判斷

簡單解釋:
想像它是一個 HTML 模板,讓你可以在所有頁面共用底層的標籤與結構。這就像是飛機的機身設計,決定了所有乘客座位與逃生出口的基礎框架,但不負責具體的航班資訊與旅客服務。

載入時機:
只在伺服器端渲染(SSR)時執行一次,用來生成完整的 HTML 基礎骨架,在瀏覽器端進行客戶端導航時不會重新執行。

_document.js 基本程式碼範例

import Document, { Html, Head, Main, NextScript } from 'next/document';

export default function MyDocument() {
  return (
    <Html lang="zh-Hant">
      <Head>
        {/* 可在此處載入全域字型或共用 Meta 標籤 */}
        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto" />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

_app.js 說明與實作

_app.js 是 Pages Router 中控制全域頁面初始化的最外層元件。所有頁面的切換與共用版面配置都會通過這裡。

生命週期角色分工與邊界條件判斷
圖:生命週期角色分工與邊界條件判斷

簡單解釋:
這就像是登機閘口,確保每位乘客在進入登機橋前都經過必要的統一流程,例如登機證核對(全域狀態與權限控管)、行李統一規範(共用樣式與佈局包裝)。

載入時機:
在每個頁面加載時執行,非常適合用來放置共用的導覽列、Footer、狀態管理(如 Redux Provider)或錯誤邊界(Error Boundary)。

_app.js 基本程式碼範例

import '../styles/globals.css';

export default function MyApp({ Component, pageProps }) {
  // 可以在這裡包裹全域 Layout、Context Provider 或 Redux Store
  return (
    <div className="app-container">
      <Component {...pageProps} />
    </div>
  );
}

根目錄 index.js 說明

定義網站首頁的實際內容與檢視邏輯。

根路徑訪問條件與重定向防呆分流
圖:根路徑訪問條件與重定向防呆分流

簡單解釋:
預設的首頁元件,如同你在瀏覽器網址列輸入 https://example.com/ 時,在沒有其他重定向設定的前提下,最終導向並渲染的目標頁面。

載入時機:
只有在用戶訪問根路徑 / 時才會執行,負責首頁專屬的資料抓取(如 getStaticProps 或 getServerSideProps)與 UI 渲染。


實務常見踩坑與排錯心法

在實務開發中,工程師在處理 Next.js Pages Router 的生命週期與這幾個核心檔案時,最常踩到以下幾大常見情境,以下整理對應的排錯案例與避坑心法:

1. 在 _document.js 裡面寫業務邏輯或綁定事件監聽(如 onClick)

  • 踩坑情境:
    初學者常誤以為 _document.js 是全域頁面的包裝器,因而嘗試在 <body> 或 <Head> 裡面加入 useState、useEffect,或是綁定按鈕點擊事件。
  • 為什麼會出錯:
    _document.js 只在伺服器端渲染 (SSR) 時執行,用來建立最外層的 HTML 骨架。當 HTML 傳送到客戶端進行 Hydration 之後,_document 內部的程式碼完全不會被當成 React 元件重新運作。
  • 排錯與解決方法:
    症狀:主控台噴出 window is not defined、事件完全沒有反應,或是水合錯誤(Hydration Mismatch)。
    解法:任何與瀏覽器互動、狀態管理、UI 互動邏輯,一律必須放在 _app.js 或具體的頁面元件(如 index.js)中。
方案 2:錯誤陷阱 vs 解決方法的邊界條件與防呆分流
圖:方案 2:錯誤陷阱 vs 解決方法的邊界條件與防呆分流

2. 誤用 middleware.js 進行龐大的資料庫查詢或 CPU 密集運算

  • 踩坑情境:
    有些開發者為了貪快,直接在 middleware.js 中引入大型 ORM(如 Prisma)或直接連線 MySQL / MongoDB,想在路由攔截時順便撈取整筆使用者資料。
  • 為什麼會出錯:
    多數 Next.js 部署平台(如 Vercel)的 Middleware 是運行在 Edge Runtime 上的(基於 V8 Workers,而非完整 Node.js 環境)。Edge Runtime 不支援許多 Node.js 原生模組(例如 fs、crypto 的部分功能),且執行時間極短(通常有嚴格的逾時限制)。
  • 排錯與解決方法:
    症狀:部署到雲端平台時直接噴出編譯錯誤,或線上環境頻繁發生 504 Gateway Timeout。
    解法:Middleware 應只用來做輕量級的字串判斷(如檢查 JWT 的 Cookie 存在與否、格式是否合法、簡易重新導向)。若需複雜的資料庫驗證,應交由 API 路由或頁面本身的 getServerSideProps 處理。
方案 2:邊界條件判斷與架構防呆解法分流
圖:方案 2:邊界條件判斷與架構防呆解法分流

3. 在 _app.js 內無條件執行會噴錯的瀏覽器 API

  • 踩坑情境:
    在 _app.js 的頂層直接讀取 window.localStorage 或 document.cookie,用來初始化全域狀態。
  • 為什麼會出錯:
    _app.js 在首次請求時會先在伺服器端(SSR)執行一次。此時伺服器環境根本沒有 window 物件。
  • 排錯與解決方法:
    症狀:ReferenceError: window is not defined(常見於本機測試正常,但一上線執行 SSR 就崩潰)。
    解法:若要在 _app.js 中使用瀏覽器專屬 API,必須加上安全檢查,或包在 useEffect 內:
import { useEffect } from 'react';

export default function MyApp({ Component, pageProps }) {
  useEffect(() => {
    // 確保只在客戶端執行
    const theme = window.localStorage.getItem('theme');
  }, []);

return <Component {...pageProps} />;
}
載入階段職責劃分與邊界條件防呆
圖:載入階段職責劃分與邊界條件防呆

4. 搞混 _app.js 與 _document.js 的樣式注入順序導致樣式閃爍 (FOUC)

  • 踩坑情境:
    使用 CSS-in-JS 框架(如 Styled Components 舊版)或 Material-UI (MUI) 時,沒有正確在 _document.js 收集伺服器端的樣式並注入 <Head>。
  • 排錯與解決方法:
    症狀:畫面載入瞬間呈現無樣式的原始 HTML,0.5 秒後才突然變成有設計感的介面(Flash of Unstyled Content)。
    解法:必須嚴格依照對應 UI 框架的官方文件,在 _document.js 的 getInitialProps 中攔截並收集樣式,確保 SSR 輸出的 HTML 已經帶有完整的 CSS。
方案 2:錯誤配置與正確防呆解法的條件分流
圖:方案 2:錯誤配置與正確防呆解法的條件分流

💡 工程師避坑要點:何時該用哪個檔案?

  • middleware.js:管「能不能進來」(路由攔截、Edge 重新導向)。
  • _document.js:管「外殼長怎樣」(全域 HTML 結構、字型、伺服器端靜態標籤)。
  • _app.js:管「穿什麼衣服、帶什麼裝備」(全域狀態、Layout、Context Provider)。
  • index.js:管「裡面賣什麼」(頁面專屬 UI 與資料)。
功能定位與防呆決策樹
圖:功能定位與防呆決策樹

結論

掌握 Next.js 的載入順序與生命週期架構,能讓你在開發時清楚知道程式碼究竟是在伺服器端(如 _document.js 與 middleware.js)還是客戶端運行。妥善運用這四個核心檔案的職責劃分,可以有效避免權限漏洞、減少不必要的重複渲染,並打造出結構清晰且高效能的 React 應用程式。

參考文件

參考資料與延伸閱讀

發表迴響