SPA路由与History-fallback
1. 概念与原理
SPA(Single Page Application)只有一个入口 HTML,所有路由由前端 JS 处理。react-router、vue-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:
- 先找
$uri(如/users→/var/www/dist/users) - 再找
$uri/(目录) - 都没有就返回
/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-router 设 basename="/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 炸