Vue、React前端上线:刷新404处理
项目在本地能运行,上传后却出现首页正常、刷新子页面 404,往往是前端路由与服务器找文件的方式没有配合好。本文以Ubuntu 24.04、系统软件源安装的 Nginx,以及 Vite 构建的 Vue 或 React 单页应用为例,把网站部署在域名根路径。
本例发布的是静态构建产物。Nuxt、Next.js 等需要服务端渲染的项目,不能只上传源码后套用这份配置。服务器应允许建站;已有宝塔等面板接管 Nginx 时,请在对应站点中操作,避免同时维护两套配置。
上传之前,先检查构建产物
在开发电脑的项目目录执行构建。下面适用于使用 npm 且已经提交 package-lock.json的项目;使用 pnpm 或 Yarn 的项目应沿用自己的锁文件和构建命令。
npm ci &&
npm run build
默认产物位于 dist。里面应能找到 index.html和构建后的脚本、样式等文件;如果修改了 build.outDir,以项目实际目录为准。npm run dev和vite preview不作为本例的正式网站进程。
构建前核对生产 API 地址。Vite 的 VITE_变量会进入浏览器代码,不能放数据库密码或服务端密钥;打包后再修改服务器上的 .env,通常不会改变已生成的前端文件。部署到 /console/这类子目录时,构建的 base、前端路由前缀和 Nginx 路径都要一起调整,本例先按根路径部署。
通过SFTP把 dist 内的文件上传到当前账号的 ~/baole-spa-upload/,不要多套一层 dist,也不要上传整个源码仓库、node_modules 或私密配置。先确认上传目录中直接存在 index.html,再建立独立版本目录:
test -f "$HOME/baole-spa-upload/index.html" &&
sudo install -d -m 0755 /srv/baole-spa/releases/release-01 &&
sudo cp -a "$HOME/baole-spa-upload/." /srv/baole-spa/releases/release-01/ &&
sudo chown -R root:root /srv/baole-spa/releases/release-01 &&
sudo chmod -R u=rwX,go=rX /srv/baole-spa/releases/release-01 &&
sudo ln -sT /srv/baole-spa/releases/release-01 /srv/baole-spa/current
命令使用 &&连接,前一步成功才继续;第一条检查失败就先处理上传目录。release-01与current是首次部署示例,已存在时先核对用途,不覆盖正在使用的版本。Nginx 只需读取文件,不需要给网站目录 777 权限。
页面路由回退,静态资源保留真实404
还没有安装 Nginx 时,先完成Nginx 安装教程。新建 /etc/nginx/sites-available/baole-spa,将下面的域名替换为自己的实际域名。不要直接覆盖其他网站的 server 段。
server {
listen 80;
server_name app.example.com;
root /srv/baole-spa/current;
index index.html;
access_log /var/log/nginx/baole-spa.access.log;
error_log /var/log/nginx/baole-spa.error.log;
location = /index.html {
add_header Cache-Control "no-cache" always;
}
location ^~ /assets/ {
try_files $uri =404;
}
location ~* \.(js|mjs|css|png|jpg|jpeg|gif|svg|ico|webp|woff|woff2|ttf|map)$ {
try_files $uri =404;
}
location = /api {
return 404;
}
location ^~ /api/ {
return 404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
这份配置把请求分成三类:真实脚本、样式和图片按文件查找,缺失就返回 404;前端页面地址找不到对应文件时交给 index.html;尚未接入的 API 明确返回 404。这样缺失的 JS 不会被悄悄替换成一份 HTML。
sudo ln -sT /etc/nginx/sites-available/baole-spa /etc/nginx/sites-enabled/baole-spa &&
sudo nginx -t &&
sudo systemctl reload nginx
软链接已经存在时先查看它是否指向正确文件。只有 nginx -t成功才执行重载。安全组及系统防火墙需允许实际使用的 80、443 端口,参照安全组教程处理;前端静态文件不需要再开放 Node 开发端口。
有后端接口时,只替换API这一段
假设后端已在本机 127.0.0.1:8080运行,而且接口本身包含 /api/前缀,把原来的 location ^~ /api/改为:
location ^~ /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
proxy_pass这里没有结尾的斜杠,/api/users会原样传给后端。只有后端实际需要 /users时,才改用带结尾斜杠的 http://127.0.0.1:8080/来去掉匹配前缀。不要同时保留两段相同的 location;如果后端还提供不带末尾斜杠的 /api接口,也要按真实路由处理对应的精确匹配段。
上线验收要查四个地址
先在服务器上绕过 DNS,直接用正确 Host 检查这台 Nginx。下面的 /dashboard替换为项目中确实存在的前端路由:
curl --resolve app.example.com:80:127.0.0.1 -I http://app.example.com/
curl --resolve app.example.com:80:127.0.0.1 -I http://app.example.com/dashboard
curl --resolve app.example.com:80:127.0.0.1 -I http://app.example.com/assets/does-not-exist.js
curl --resolve app.example.com:80:127.0.0.1 -I http://app.example.com/api/does-not-exist
首页和已知前端路由应能返回入口页面;不存在的静态资源应为 404;API 应返回后端自己的响应或明确的 404,不能是前端首页。随后从自己的电脑访问域名,进入子页面再刷新,确认页面、图片与接口一起工作。最后按证书申请与续期教程启用 HTTPS,再检查登录、跳转和 API 地址没有混用 HTTP。
| 现象 | 先检查 |
|---|---|
| 首页正常,刷新子页面404 | 请求是否进入正确 server 段、是否使用 history 路由、页面回退是否生效。 |
| 脚本报MIME类型为text/html | 脚本实际不存在却被回退到首页,或构建 base 与部署位置不一致。 |
| 页面空白,接口返回首页HTML | /api/ 是否被页面回退截获,后端路径是否错误地多了或少了前缀。 |
| 403或只看到Nginx欢迎页 | 前者检查目录可读性和 index.html;后者检查实际域名、Host 与站点匹配。 |
前端还应为未知路由提供自己的 404 页面。纯 SPA 回退通常仍返回 HTTP 200,需要搜索引擎识别真实页面状态时,应结合服务端渲染、静态生成或明确的服务端路由设计。以后更新先上传到新版本目录并核验,再切换;代码分块项目还要保留旧页面可能继续请求的哈希资源,不能把“保留旧目录”误认为旧资源 URL 自动可用。