这个问题困扰过无数前端开发者。想象一下:你花了两周时间开发的Vue应用,在本地测试时一切正常,部署到Nginx服务器后也能正常访问。但当用户点击浏览器刷新按钮时,页面突然变成了冷冰冰的404 Not Found或者403 Forbidden错误页面。这种体验就像精心准备的晚餐,客人刚要动筷子,桌子突然塌了。
根本原因在于SPA(单页面应用)的特殊路由机制与传统Web服务器的处理方式存在冲突。在SPA中,像/user/profile这样的路由实际上并不对应服务器上的物理文件,而是由前端路由库(如vue-router或react-router)在浏览器端解析和渲染的。但当你刷新页面时,浏览器会直接向服务器请求这个路径,而Nginx默认会把它当作一个真实的文件路径去查找,自然就找不到对应的文件了。
更让人头疼的是403错误。这种情况通常发生在你的前端代码中使用了路由守卫(如vue-router的beforeEach),在页面刷新时触发了某些重定向逻辑。由于Nginx没有正确配置,这些重定向请求会被拒绝,导致403错误。我曾在项目中因为这个错误调试到凌晨3点,最后发现是Nginx配置中少了一个简单的rewrite规则。
try_files是Nginx中一个极其强大的指令,它的工作方式就像是一个文件查找的"备胎链"。当用户请求一个路径时,Nginx会按照你指定的顺序尝试不同的文件或路径,直到找到可用的为止。如果所有尝试都失败,最后会fallback到你指定的默认文件。
对于SPA应用,最关键的配置就是:
nginx复制location / {
try_files $uri $uri/ /index.html;
}
这个配置的意思是:先尝试直接访问请求的URI对应的文件($uri),如果不存在就尝试把它当作目录访问($uri/),如果还不存在,最后返回/index.html。这样,无论用户请求什么路径,最终都会由index.html接管,再由前端路由处理实际的页面渲染。
我在实际项目中发现,很多开发者会忽略$uri/这部分。其实它很重要,特别是当你的应用中有使用到相对路径的资源文件时。有一次我们的CSS文件加载失败,就是因为缺少了这个配置。
如果你的SPA使用了哈希模式的路由(如http://example.com/#/user/profile),配置会更简单一些:
nginx复制location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
expires max;
add_header Cache-Control public;
}
这个配置做了两件事:一是处理所有非静态文件请求,都返回index.html;二是对静态资源设置长期缓存。注意哈希路由虽然配置简单,但对SEO不友好,现在主流SPA应用大多使用history模式。
403错误通常比404更棘手,因为它涉及到权限和重定向逻辑。最常见的情况是:你的前端路由守卫中设置了某些重定向逻辑,比如未登录用户访问/admin时重定向到/login。当用户刷新页面时,这个重定向请求会被发送到Nginx,而Nginx没有对应的配置来处理,就会返回403。
解决这个问题的关键在于正确配置rewrite规则。下面是我在一个电商项目中实际使用的配置:
nginx复制server {
listen 80;
server_name example.com;
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# 处理前端路由的重定向
location ~ ^/(login|admin|dashboard) {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
expires 1y;
add_header Cache-Control "public";
}
}
rewrite是Nginx中处理URL重写的强大工具。根据不同的场景,我们可以使用不同的rewrite策略:
nginx复制rewrite ^/old-path$ /new-path permanent;
nginx复制rewrite ^/api/(.*) /new-api/$1 last;
nginx复制if ($arg_utm_source = "wechat") {
rewrite ^/(.*)$ /wechat/$1 last;
}
nginx复制rewrite ^/([0-9]{4})/([0-9]{2})/(.*)$ /archive/$1-$2-$3 last;
在实际项目中,我建议尽量使用last标志而非break,因为last会重新发起一个新的location匹配,更符合大多数SPA应用的需求。曾经因为用错了这个标志,导致我们的CDN缓存策略完全失效。
下面是一个经过实战检验的Nginx配置模板,适用于大多数Vue/React SPA应用:
nginx复制server {
listen 80;
server_name yourdomain.com;
# 开启gzip压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# 设置根目录
root /var/www/your-app;
index index.html;
# 主location块
location / {
try_files $uri $uri/ /index.html;
# 禁止访问隐藏文件
location ~ /\. {
deny all;
}
}
# 静态资源缓存
location ~* \.(?:ico|css|js|gif|jpe?g|png|svg|woff2?|eot|ttf|otf)$ {
expires 1y;
add_header Cache-Control "public";
# 防止因为缓存导致的问题
add_header ETag "";
}
# API代理设置(如果有后端API)
location /api/ {
proxy_pass http://backend-server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 错误页面处理
error_page 404 /404.html;
location = /404.html {
internal;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
internal;
}
}
除了解决404/403问题外,生产环境还需要考虑以下优化点:
http2即可:nginx复制listen 443 ssl http2;
nginx复制location = /index.html {
expires 5m;
add_header Cache-Control "public, must-revalidate";
}
nginx复制add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
nginx复制add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com; img-src 'self' data: https://*.example.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; frame-src 'none'; object-src 'none'";
nginx复制access_log /var/log/nginx/access.log main buffer=32k flush=5m;
error_log /var/log/nginx/error.log warn;
在实际部署中,我发现很多团队会忽略静态资源的缓存策略。有一次我们的应用更新后,用户反映看到的还是旧版本,就是因为没有正确处理index.html的缓存。后来我们采用了在文件名中添加哈希值的策略,并设置HTML文件不缓存,彻底解决了这个问题。