开发能跑,不等于上线能用。这一章解决最后一公里:把几十个 .vue 文件打成浏览器能直接加载的静态文件(npm run build),用环境变量管好「开发 / 生产」两套 API 地址,配好 Nginx 的 try_files 让刷新不 404,最后给你一份前后端联调排错清单。做完这些,你的作品才真正「站」在公网上。
npm run build
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
npm run preview
打开 http://localhost:4173/,你会发现一个关键区别:preview 用的是 .env.production,不是开发环境那套代理。
# 开发环境:走相对路径 /api,由 vite.config.js 的 server.proxy 转发给 Flask
VITE_API_BASE=/api
VITE_APP_TITLE=古典文学网(开发)
# 生产环境:同样用相对路径 /api,由 Nginx 反向代理到 Flask
# 注意:这里绝不写死 http://127.0.0.1:5000 或某个内网 IP
VITE_API_BASE=/api
VITE_APP_TITLE=古典文学网
// ① 在业务代码里读:只能读 VITE_ 开头的变量
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE, // '/api'
timeout: 10000,
})
// ② 想按环境做点小差别(比如开发时打印日志)
if (import.meta.env.DEV) {
console.log('[dev] API Base =', import.meta.env.VITE_API_BASE)
}
// ③ 在 vite.config.js 里读:用 loadEnv,模式要显式传进去
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
console.log('当前模式:', mode, ',API:', env.VITE_API_BASE)
return { /* ... */ }
})
【错误】写死 IP / 端口
VITE_API_BASE=http://47.98.123.45:5000/api
浏览器地址 http://47.98.123.45/
接口地址 http://47.98.123.45:5000/api/article/list
↑ 端口不同(80 vs 5000)→ 属于跨域 → 浏览器先发 OPTIONS 预检
后果:① 后端必须配 CORS ② 换成域名或上 HTTPS 就要重新打包
③ IP 暴露在产物里,被扫到就是攻击入口
【正确】相对路径 + 反向代理
VITE_API_BASE=/api
浏览器地址 http://47.98.123.45/
接口地址 http://47.98.123.45/api/article/list
↑ 同源(同协议、同域名、同端口)→ 没有跨域,不需要 CORS
由 Nginx 在服务器内部把 /api 转发给 127.0.0.1:5000
好处:① 前端打包一次,部署到哪都能用 ② 后端端口不对外暴露 ③ 上 HTTPS 只需配 Nginx 证书
dist/
├── index.html # 唯一入口,里面的 <script> 指向 assets 里的 js
├── favicon.ico
└── assets/
├── index-7a5e1b90.js # 主包:Vue 运行时 + Router + Pinia + axios 等公共代码
├── index-2b7e6d91.css # 主样式:Element Plus 按需引入后剩下的样式
├── Home-8c1f2e04.js # 首页 chunk(懒加载)
├── ArticleList-6a0b3f77.js # 列表页 chunk
├── ArticleDetail-1e9d4c88.js # 详情页 chunk
├── UserCenter-3c7f0a15.js # 用户中心 chunk
├── Login-2f8b6e33.js # 登录页 chunk
└── ArticleCard-4d8a5c11.js # 被多处复用的公共组件,单独成块
看一眼 index.html 更好理解:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>古典文学网</title>
<script type="module" crossorigin src="/assets/index-7a5e1b90.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-2b7e6d91.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
server {
listen 80;
server_name hiwen.example.com;
# ① 前端静态产物:把 dist/ 里的东西放到这个目录
root /var/www/hiwen/dist;
index index.html;
# ② 核心一行:history 路由的兜底
# 先找有没有同名文件 → 再找有没有同名目录 → 都没有就返回 index.html
# 这样 /article/1 刷新时也交给前端 JS 去路由,而不是 404
location / {
try_files $uri $uri/ /index.html;
}
# ③ 带哈希的静态资源可以长期缓存(内容变了文件名就变了)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# ④ 接口反向代理:把 /api 转发给本机的 Flask
location /api/ {
proxy_pass http://127.0.0.1:5000;
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;
}
# ⑤ 后端返回的图片(头像 / 封面)也要能访问
location /static/ {
proxy_pass http://127.0.0.1:5000;
}
# ⑥ 开启 gzip,JS/CSS 体积能再小 60%+
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
}
再对比一下「不写 try_files」会发生什么,把这个现象刻进脑子里。
【配置缺失】location / { root /var/www/hiwen/dist; }
用户点击「静夜思」 → 地址变成 /article/1(JS 渲染,不经过服务器)→ 正常显示
用户按 F5 → 浏览器向服务器请求 /article/1
→ 服务器在 dist/ 里找 article/1 这个文件
→ 找不到 → 返回 Nginx 的 404 页面
【配置正确】location / { try_files $uri $uri/ /index.html; }
用户按 F5 → 服务器找不到 /article/1 文件
→ 兜底返回 index.html(HTTP 200)
→ index.html 加载 JS,Vue Router 看到路径是 /article/1
→ 渲染详情页 → 用户看到的内容和刷新前一模一样
现象 最可能的原因 怎么确认 / 怎么修
-------------------------- -------------------------------- --------------------------------------
CORS 报错 前后端不同源,且后端没配 CORS Network 里看请求域名与页面域名是否一致
blocked by CORS policy 生产环境不该出现(用 Nginx 反代) 开发用 server.proxy;生产用 location /api/
401 请先登录 没带 token / token 过期 Network → Headers → 有没有 Authorization
上传接口最容易漏(不走拦截器) el-upload 的 :headers 要手动传
404 接口不存在 路径写错 / 连到了前端服务器 看请求 URL,是不是 http://localhost:5173/api/...
代理没生效(preview 模式) Vite 代理只在 dev 生效;preview 需要真后端
404 刷新页面白屏 history 路由缺 try_files dist 部署目录的 Nginx 配置加 try_files
build 后资源 404 部署到了子目录但没设 base vite.config.js 加 base: '/子目录/'
或 assets 没上传完整 对比 dist/ 与服务器目录的文件清单
页面还是旧版本 浏览器/浏览器缓存了旧的 index.html 强刷 Ctrl+Shift+R;确认 index.html 别长缓存
或 CDN 缓存未刷新 只给 /assets/ 加长缓存,index.html 要短缓存
接口返回但页面空白 拦截器直接 return res.data 成功分支里必须判 code === 200
页面拿到的是 {code,msg} 不是数据 F12 Console 看有没有 undefined 报错
图片不显示 src 是 /static/xxx,没配代理 Nginx 加 location /static/ 反代到 Flask
avatar 字段是相对路径 或前端给图片 URL 拼上 VITE_API_BASE
请求发了两次 watch 和 @change 同时触发 二选一,别都写
分页出现重复/漏条 ORDER BY 没有次关键字 id 后端排序加 , id DESC;前端改每页数归 1
空数组 / undefined category_id= 空字符串被发出去 请求前删掉空值参数
再给你两条命令行工具,比在浏览器里反复点更快:
# ① 后端本身通不通(绕开浏览器,排除跨域干扰)
curl -i http://127.0.0.1:5000/api/article/list?page=1&page_size=3
# ② 走 Nginx 代理通不通(验证 location /api/ 配对了没有)
curl -i http://127.0.0.1/api/article/list?page=1&page_size=3
# ③ 带 token 请求受保护接口(验证鉴权链路)
curl -i -H "Authorization: Bearer $TOKEN" http://127.0.0.1/api/user/profile
# ④ 检查 history 路由兜底是否生效(应返回 200 + index.html,而非 404)
curl -i http://127.0.0.1/article/1 | head -n 1
阶段一 · 本地开发(你现在最熟悉的)
终端 A: python app.py # Flask 跑在 127.0.0.1:5000
终端 B: npm run dev # Vite 跑在 localhost:5173,proxy 转发 /api
打开 http://localhost:5173/ # 热更新,改代码自动刷新
阶段二 · 本地验收(部署前必做)
npm run build # 产出 dist/
# 用本地 Nginx 或 preview 访问 dist/,完整走一遍 M5 七步闭环
阶段三 · 部署上线(综合实战)
① 服务器准备:MySQL 8 + Python 3.10+ + Nginx
② 数据库:导入 hiwen.sql
③ 后端:上传 hiflask/ → 建虚拟环境 → pip install -r requirements.txt
→ 用 gunicorn 常驻(不要用 flask run)
gunicorn -w 4 -b 127.0.0.1:5000 app:app
④ 前端:本地 npm run build → 把 dist/ 整个上传到 /var/www/hiwen/dist
⑤ Nginx:套用上一节的配置,root 指向 dist,/api/ 反代 5000
⑥ 验证:curl 一遍 ① ② ③ ④,再用手机浏览器打开域名走一遍闭环
⑦ 收尾:配 HTTPS(证书 + 80 跳 443)、配 systemd 守护 gunicorn、开日志
阶段四 · 交付
域名 + 可访问的网站 + 完整源码(hiflask + hivue)+ 接口文档 + 部署说明
# 第 2 题结论
# 不能改成真实 IP。原因有三:
# ① 写死 IP 会变成跨域(前端 80 端口、后端 5000 端口),必须让后端配 CORS
# ② 后端换服务器 / 上 HTTPS / 换域名,都得重新打包前端
# ③ IP 与端口会出现在产物里,被扫描到就是攻击面
# 正确做法:VITE_API_BASE=/api,由 Nginx 反代。
# 第 3 题:红影赏析的 Nginx 配置
server {
listen 80;
server_name movie.example.com;
root /var/www/hongying/dist;
index index.html;
# history 路由兜底:刷新 /movie/12 不再 404
location / {
try_files $uri $uri/ /index.html;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 接口反代到本机 Flask
location /api/ {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 图片等后端静态资源
location /static/ {
proxy_pass http://127.0.0.1:5000;
}
}
第 2 题的关键词是「构建期注入」:环境变量不是在运行时读的,而是在 build 那一刻被字面替换进产物。所以改了 .env 一定要重新 build,热更新不会帮你重读它。