SPA 部署到 Nginx 的路由問題與解決方案

問題描述

啟動容器後發生一個錯誤:一開始是正常的導到 http://localhost:4200/login 並顯示畫面,但重新整理後會變成 404 錯誤。

從瀏覽器 console 中可以看到錯誤訊息:

問題原因

這是一個在單頁應用程式 (SPA) 部署到 Nginx 時常見的問題。發生的原因是:

  1. 首次載入:當第一次載入頁面時,瀏覽器請求根路徑 /,Nginx 正確地提供了 index.html

  2. 前端路由運作:Angular 的路由機制接管了頁面,當你點擊連結導航到 /login 時,Angular 路由器在客戶端處理這個路徑變更,不會向伺服器請求新頁面

  3. 重新整理問題:但當你在 /login 路徑下重新整理頁面時,瀏覽器會直接向伺服器請求 /login 路徑的資源

  4. 404 錯誤:Nginx 找不到實體的 /login 檔案(因為它不存在),所以回傳 404 錯誤

這個問題發生是因為你的 Angular 應用使用前端路由,但 Nginx 預設設定只會尋找實體檔案,而不會將所有請求都導向 index.html。

解決方案

方案一:修改 Dockerfile 和添加自定義 Nginx 設定檔

  1. 修改前端的 Dockerfile,添加自定義 Nginx 設定:

# 建置階段
FROM node:20 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build

# 執行階段
FROM nginx:alpine
# 複製自定義 Nginx 設定
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/voting-system/browser /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
  1. 在前端專案目錄中建立一個 nginx.conf 檔案:

server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;
    
    location / {
        try_files $uri $uri/ /index.html;
    }
}

關鍵設定說明

  • try_files $uri $uri/ /index.html; 這行設定是解決問題的核心

  • 它會嘗試尋找請求的 URI,如果找不到就回退到 index.html

  • 這樣 Angular 的路由器就能正常接管,處理客戶端路由

  1. 重新建置和啟動容器:

docker-compose down
docker-compose up --build

方案二:使用 Docker Hub 官方的 Nginx 設定(透過 docker-compose.yml)

除了修改 Dockerfile 外,也可以直接在 docker-compose.yml 中設定 Nginx:

frontend:
  build:
    context: ./voting-system-fe
  image: voting-system-fe
  container_name: voting_system_fe
  restart: always
  ports:
    - "4200:80"
  depends_on:
    - server
  volumes:
    - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro

使用這種方式時,需要在整合目錄中(與 docker-compose.yml 相同層級)建立 nginx.conf 檔案,內容與前面相同。

技術說明

SPA 應用使用 HTML5 History API 進行路由,與傳統多頁面應用程式有所不同:

  1. 傳統網站:每個 URL 路徑對應到伺服器上的一個實體檔案

  2. SPA:所有路由都由前端 JavaScript 處理,伺服器只需提供唯一的入口檔案(通常是 index.html)

因此,正確設定 Nginx 進行 URL 重寫(URL rewriting)是部署 Angular、React 或 Vue 等 SPA 應用時的必要步驟。

擴展知識

這個 try_files 指令也可以添加其他選項,例如:

location / {
    try_files $uri $uri/ /index.html?$query_string;
}

這會將原始的查詢參數傳遞給 index.html,在某些需要處理 URL 參數的應用中很有用。

對於生產環境,還可以添加快取設定,例如:

location /assets/ {
    expires 1y;
    add_header Cache-Control "public";
}

這會對靜態資源設定較長的快取時間,提高網站性能。

Last updated