Vue 或 React 项目部署到 VPS,首页能打开,从菜单进入 /dashboard 也正常,但刷新就出现 Nginx 404?通常是浏览器路由和服务器文件路径没有对上。另一个常见结果是刷新不报 404 了,JS 却收到 HTML,页面变成白屏。
这篇针对构建后由 Nginx 托管的静态单页应用。先查请求、文件和匹配规则,再给页面路由加回退;API 与静态资源要分开处理。Next.js、Nuxt 的服务端渲染项目不能直接套用这份静态站配置。
打开浏览器开发者工具的 Network,刷新故障页面,分别看文档请求、JS/CSS 和 API。不要只根据页面上显示的错误文字改配置。
| 请求结果 | 优先检查 | 应有的处理 |
|---|---|---|
/dashboard 文档 404,站内跳转正常 | History 路由回退 | 返回入口 HTML,由前端识别路由 |
/assets/app-xxx.js 404 | 构建产物、资源路径、旧缓存 | 找到正确文件,不能返回首页冒充 JS |
| JS 请求 200,但正文是 HTML | 回退覆盖静态资源 | 缺失文件返回真实 404 |
/api/users 返回首页 | API 被页面规则接管 | 恢复独立 API 路由或反向代理 |
Vue Router 的 History 模式说明解释了这个区别:站内跳转可以由浏览器处理,直接访问深层 URL 时,服务器仍需要交付应用入口。返回入口不等于对应前端路由一定存在。
以 Vite 默认输出为例,先运行项目已有的构建命令,检查 dist/index.html 及它引用的资源是否完整上传。Vite 部署文档默认输出目录为 dist,也说明 vite preview 用于本地预览,不能直接当生产服务器。
下面假设产物放在 /srv/www/app/dist,域名是 app.example.com。先看实际生效配置和目录:
sudo nginx -T
ls -l /srv/www/app/dist/index.html
ls -l /srv/www/app/dist/assets/
检查请求是否命中正确的 server_name、80/443 站点和 root。配置可能由宝塔、1Panel 或其他面板生成,别改完一个文件,又被面板覆盖。nginx -T 的输出可能含内部地址,分享前应脱敏。
路径大小写也要一致:本地能打开的 Logo.png,在 Linux 上不一定能用 logo.png 访问。若 Nginx 在容器里,必须检查容器实际看到的文件,宿主机有文件不代表挂载正确;可以接着看 Docker 重建后数据与挂载排查。
下面是独立域名、根路径部署、无后端 API的 HTTP 示例。HTTPS 站点应将相关规则合入原有站点块并保留证书设置;已有 API 时保留其真实代理规则,不能用这里的占位 404 覆盖。
server {
listen 80;
server_name app.example.com;
root /srv/www/app/dist;
index index.html;
location = /index.html {
try_files $uri =404;
add_header Cache-Control "no-cache" always;
}
location ^~ /assets/ {
try_files $uri =404;
}
location ^~ /static/ {
try_files $uri =404;
}
location = /api {
return 404;
}
location ^~ /api/ {
return 404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
/dashboard 找不到真实文件时会内部转到 /index.html。单独处理入口文件,是为了在入口也缺失时返回 404,避免反复内部跳转。no-cache 允许缓存存储,但要求使用前重新验证,并不等于彻底禁止缓存。
/assets/ 和 /static/ 是常见资源目录,若你的构建使用其他目录,也要给它加独立规则。否则一个缺失的 JS 文件仍可能落进页面回退。不要用全站 error_page 404 /index.html 掩盖文件、API 和真实路由错误。
try_files 会按文件映射检查是否存在,最后一个 URI 才是回退目标;root 则把 URI 拼到目录后面,详见 Nginx 指令文档。例如 root /srv/www/app/dist 下的 /assets/app.js 对应 /srv/www/app/dist/assets/app.js。
如果用了 alias,它替换匹配到的 location 前缀,路径算法不同。别为了消掉 404,把 root 和 alias 随意互换;先在错误日志中看 Nginx 究竟尝试打开哪个文件。Permission denied 属于读取权限问题,不是再加一条回退就能解决。
遇到 “Expected a JavaScript module … MIME type text/html” 或 Unexpected token '<',先打开失败请求的 Response。如果开头是 <!doctype html>,通常拿到的是首页或 HTML 错误页。
确认路径、文件和状态码之后,再检查 Nginx 的 http 上下文是否加载安装环境对应的 mime.types。不要把全站默认类型强制改成 JavaScript;那会让 HTML、CSS 和其他文件也用错类型。
更新发布时还要检查旧 HTML 是否引用了已经删除的旧哈希资源。部署产物应作为完整版本切换,按需要保留一段时间的旧资源,并核对浏览器、Service Worker 和 CDN 的缓存。单纯改响应头,不能凭空补出缺失文件。
若域名经过 CDN,先比较源站和边缘返回的状态、Content-Type 与响应正文。只有源站正确、边缘仍返回旧结果时,再针对相关 URL 清缓存;不要把所有 404 都归因于 CDN,也不要用跳过证书校验来掩盖 HTTPS 配置问题。
如果实际地址是 https://example.com/app/,不能只把 Nginx 的 location 换成 /app/。Vite 的资源 base、前端路由的基路径、服务器文件布局和入口回退都必须对应这个前缀。
例如页面在 /app/,却请求 /assets/app.js,服务器可能去主站根目录找文件。应按构建工具与路由库的部署说明设置基路径,重新构建,再核对生成 HTML 中的资源 URL。本文根路径示例不能原样用于子目录。
单页应用收到未知页面 URL 后,服务器仍可能返回 200;前端还应有自己的未找到页面。若需要搜索引擎看到准确的 HTTP 404,应考虑能在服务器判断路由的方案,而不是把所有 URL 都当成有效页面。
先备份原配置,语法检查通过后再重载。检查失败时保留运行中的旧配置,参考 Nginx 配置与 reload 失败排查。
sudo nginx -t && sudo systemctl reload nginx
curl -i https://app.example.com/dashboard
curl -i https://app.example.com/assets/not-a-real-file.js
curl -i https://app.example.com/api/not-a-real-endpoint
预期是深层页面拿到入口 HTML,缺失 JS 返回 404;真实 API 则按后端规则返回 JSON、鉴权错误或接口 404,不该收到前端首页。还要在浏览器刷新深层页面,确认脚本执行、登录与数据加载正常。
本文用现有 Nginx 1.19.6 在隔离回环服务中完成 10 项检查:深层页面返回 200,JS/CSS 类型正确,缺失资源与 API 占位路径返回 404,删除入口文件后也返回 404 而非内部跳转循环。这个本机版本仅用于验证,不作为生产安装建议。测试不覆盖真实前端脚本执行、TLS、CDN、Service Worker 或业务登录流程。
