本篇筆記記錄了單頁面應用程式 (SPA) 在部署至 Vercel 後,於子路徑重新整理或直接輸入網址時遭遇 404 錯誤的根本原因與解決方案。


1. 問題描述

  • 現象
    • 本地開發環境 (npm run dev):無論在任何子路徑(如 /dashboard/finance/api-docs)重新整理頁面、直接輸入 URL 或點擊瀏覽器上一頁,網頁皆能正常載入與切換。
    • 部署至 Vercel 後:在首頁進行點擊導覽皆正常;但在任何子路徑重新整理網頁直接輸入網址,或從外部連結直接進入時,瀏覽器會直接顯示 Vercel 的 404: NOT_FOUND 錯誤頁面。

2. 根本原因分析 (Root Cause Analysis)

此問題的核心在於 單頁面應用程式 (SPA) 的前端路由機制靜態檔案伺服器預設行為 之間的衝突。

1
2
3
4
5
6
graph TD
A[使用者造訪 /dashboard 並重整] --> B{伺服器如何處理?}
B -- Vite 開發伺服器 --> C[自動回退: 將請求重導向至 /index.html]
C --> D[載入 JS/React Router 渲染 Dashboard 頁面]
B -- Vercel 靜態伺服器 --> E[尋找實體檔案 /dashboard 或 /dashboard/index.html]
E -- 檔案不存在 --> F[回傳 404 Not Found 錯誤]

概念一:前端路由 (Client-Side Routing)

在 React SPA(使用 react-router-domBrowserRouter)中,所有的路由切換都是在瀏覽器端由 JavaScript 處理。
當你點擊導覽列的連結時:

  1. JavaScript 攔截了點擊事件。
  2. 使用 HTML5 History API (history.pushState) 改變瀏覽器網址列的 URL,而不向伺服器發送請求
  3. React Router 根據新的網址,動態切換渲染對應的元件。
    這也是為什麼在首頁登入後,點選選單不會出錯的原因。

概念二:重新整理與直接造訪 (Direct Access & Page Reload)

當使用者在子路徑按下了重新整理,或者從瀏覽器直接輸入 https://your-domain.com/dashboard 時,瀏覽器會向 Vercel 伺服器 發送實體 HTTP 請求以取得該路徑的資源。

  • 本地 Vite 伺服器:Vite 內建 historyApiFallback 功能,當找不到實體檔案時,會自動改為提供根目錄的 index.html
  • Vercel 伺服器:預設會去尋找專案建置輸出目錄下是否存在實體的 /dashboard 檔案或 /dashboard/index.html。由於 SPA 打包後只有一個 index.html 放在根目錄,伺服器找不到該檔案,因而直接回傳 404 Not Found

3. 解決方案 (Solution)

為了解決這個問題,我們需要新增一條伺服器端的重寫 (Rewrite) 規則:告訴 Vercel 伺服器,「只要請求的路徑不是 API,通通把請求指向 index.html,讓瀏覽器載入 React SPA 後再由 React Router 來決定渲染什麼頁面」

我們對根目錄底下的 vercel.json 進行了修改:

vercel.json 修改對比

1
2
3
4
5
6
7
 {
- "rewrites": [{ "source": "/api/(.*)", "destination": "/api/$1" }]
+ "rewrites": [
+ { "source": "/api/(.*)", "destination": "/api/$1" },
+ { "source": "/(.*)", "destination": "/index.html" }
+ ]
}

規則說明

  1. 後端 API 排除 (/api/(.*)):
    • 當請求符合 /api/xxx 時,伺服器會將請求轉發至 api 資料夾底下的 Serverless Functions 處理,避免被前端攔截。
  2. 前端路由全攔截 (/(.*)):
    • 當請求符合除 API 以外的任何路徑(例如 /dashboard/sales)時,Vercel 伺服器會在背後將目標資源重寫為 /index.html 回傳給瀏覽器。
    • 瀏覽器收到 index.html 後,載入 JavaScript,此時 React Router 讀取當前的瀏覽器 URL(如 /dashboard),便能渲染出正確的頁面。

4. 驗證與部署步驟

[!IMPORTANT]
該設定僅在 Vercel 環境(或使用 vercel dev 本地開發工具)中生效。Vite 的 npm run dev 仍會使用 Vite 自己的 Fallback 機制,兩者互不干涉。

部署步驟:

  1. 儲存變更:確保本地的 vercel.json 已更新。
  2. 提交與推入 Git
    1
    2
    3
    git add vercel.json
    git commit -m "chore: add SPA rewrite fallback to vercel.json"
    git push
  3. 自動建置:Vercel 將偵測到 Git 提交並自動重新部署。
  4. 驗證測試
    • 前往部署後的網頁子頁面(例如 https://<your-project>.vercel.app/dashboard)。
    • 點選瀏覽器重新整理按鈕,頁面應可成功重新載入,不再出現 404 錯誤。
    • 點選瀏覽器上一頁 / 下一頁,應可正常於歷史軌跡中穿梭。