跳到主要内容

SPA路由与History-fallback

1. 概念与原理

SPA(Single Page Application)只有一个入口 HTML,所有路由由前端 JS 处理。react-routervue-router 默认有两种模式:

  • hash 模式example.com/#/users,URL 里 # 后面的不发到服务器,无需服务器配合
  • history 模式example.com/users,URL 干净,但用户直接访问或刷新页面时服务器需返回 index.html,否则 404

history 模式的 fallback 是 Nginx 上 SPA 部署的核心配置。

2. 基础 fallback 配置

server {
listen 80;
server_name app.example.com;
root /var/www/dist;
index index.html;

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

try_files

  1. 先找 $uri(如 /users/var/www/dist/users
  2. 再找 $uri/(目录)
  3. 都没有就返回 /index.html

这样:

  • /app.abc.js → 真存在 → 返回文件
  • /users → 不存在 → 返回 index.html,前端路由接管

3. 进阶:区分静态资源 404 和路由 404

直接 try_files $uri /index.html 的问题:用户输错 /app.abcd.js(hash 错),Nginx 返回 index.html + 200,浏览器把 HTML 当 JS 执行报错。

更严格的做法:

# 静态资源精确匹配,找不到直接 404
location ~* \.(js|css|woff2?|png|jpg|svg|ico|map)$ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}

# 其他都 fallback 到 index.html(路由)
location / {
try_files $uri $uri/ /index.html;
}

4. 子目录部署

把 SPA 部署到 /admin/ 而非根域:

location /admin/ {
alias /var/www/admin/;
try_files $uri $uri/ /admin/index.html;
}

# 或用 root + 不同子路径
location /admin {
root /var/www; # 实际找 /var/www/admin/
try_files $uri $uri/ /admin/index.html;
}

前端构建时 publicPath: '/admin/'react-routerbasename="/admin"

5. HTML 不缓存 + 静态资源长缓存

SPA 标准缓存策略:

# JS / CSS / 资源:hash 文件名 + 1 年缓存
location ~* \.(js|css|woff2?|png|jpg|jpeg|webp|svg|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}

# index.html 永不缓存(确保用户拿到新版本)
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
expires 0;
}

# 路由 fallback
location / {
add_header Cache-Control "no-cache";
try_files $uri $uri/ /index.html;
}

6. SSR / Next.js 部署

Next.js / Nuxt 有 SSR 时,路径分两种:

upstream node_app {
server 127.0.0.1:3000;
keepalive 32;
}

server {
listen 443 ssl http2;
server_name app.example.com;

# Next.js 静态资源(含 hash)
location /_next/static/ {
alias /var/www/app/.next/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}

# public 文件夹
location /public/ {
alias /var/www/app/public/;
expires 7d;
}

# 其余走 SSR
location / {
proxy_pass http://node_app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

7. 多个 SPA 共存

一个域名下多个独立 SPA 应用:

server {
listen 443 ssl http2;
server_name console.example.com;
root /var/www;

# 主控制台(默认)
location / {
try_files $uri $uri/ /console/index.html;
}
location /console/ {
alias /var/www/console/;
try_files $uri $uri/ /console/index.html;
}

# 子应用:data
location /data/ {
alias /var/www/data/;
try_files $uri $uri/ /data/index.html;
}

# 子应用:admin
location /admin/ {
alias /var/www/admin/;
try_files $uri $uri/ /admin/index.html;
}
}

每个子应用构建时 publicPath 指定自己的路径前缀。

8. 微前端(qiankun / micro-app)部署

主应用 + 子应用各自打包,部署同一域名或子域:

# 主应用
server {
server_name portal.example.com;
root /var/www/portal;
location / {
try_files $uri $uri/ /index.html;
}
}

# 子应用:用 CORS 让主应用能 fetch 它的 HTML
server {
server_name sub-app.example.com;
root /var/www/sub-app;

location / {
try_files $uri $uri/ /index.html;
# 允许主应用跨域加载
add_header Access-Control-Allow-Origin "https://portal.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
}
}

9. PWA / Service Worker

Service Worker 注册时浏览器会请求 /sw.js

# Service Worker 必须从 site root 提供
location = /sw.js {
add_header Cache-Control "no-cache, no-store";
add_header Service-Worker-Allowed "/";
expires 0;
try_files $uri =404;
}

# manifest
location = /manifest.json {
add_header Cache-Control "no-cache";
}

Service Worker 不能 fallback 到 index.html,否则浏览器报错。

10. 错误页定制

error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;

location = /404.html {
root /var/www/error-pages;
internal;
}

# SPA 通常不需要服务端 404 页(前端路由接管)
# 但 API 的 404 可能要返 JSON
location /api/ {
error_page 404 = @api_404;
proxy_pass http://backend;
}

location @api_404 {
add_header Content-Type application/json;
return 404 '{"code":404,"message":"Not Found"}';
}

11. 故障排查

11.1 刷新页面 404

最经典的 SPA 部署问题。原因:没配 try_files fallback。

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

11.2 路由 fallback 把所有 404 都变成 200

用户访问任何不存在的资源都返回 HTML,浏览器解析报错。

# 资源类型显式 404
location ~* \.(js|css|png|jpg|svg|woff2?)$ {
try_files $uri =404;
}

11.3 子目录刷新白屏

构建时 publicPath 配置错。React 用 homepage、Vite 用 base、Next.js 用 basePath、Vue CLI 用 publicPath

// vite.config.ts
export default { base: '/admin/' }

// next.config.js
module.exports = { basePath: '/admin' }

11.4 静态资源 mime 错

JS 文件被当成 HTML 加载,浏览器报:

Refused to execute script from 'app.js' because its MIME type ('text/html') is not executable

= Nginx 把 app.js fallback 到 index.html 了。看 location 优先级和 try_files 配置。

11.5 浏览器拿到旧 HTML

发版后用户还看老版本:

  • HTML 被强缓存了(必须 no-cache)
  • CDN 没刷新 HTML
  • Service Worker 缓存了老版本

修复:HTML 永不强缓存 + Service Worker 在新版本主动 skipWaiting + clients.claim。

12. 常见反模式

  • 不配 try_files:history 模式刷新 404
  • try_files $uri /index.html:静态资源 404 也变 HTML,浏览器解析报错
  • HTML 配 1 年缓存:发版用户永远看不到新版
  • JS/CSS 不带 hash 还配 1 年缓存:发版后老 JS + 新 HTML 不兼容,白屏
  • location / 之后还想用 location /static:注意 location 优先级(精确 → 前缀长 → 正则 → 默认 /
  • alias 结尾漏斜杠:路径拼接出错
  • 子目录部署没改 publicPath:所有静态资源 404
  • Service Worker 文件被 fallback:注册失败
  • API 走 SPA fallback:API 错误返回 HTML,前端 JSON.parse 炸

13. 延伸阅读