码上问学问代码之理,学编程之道

第 12 章 · 打包构建与联调

开发能跑,不等于上线能用。这一章解决最后一公里:把几十个 .vue 文件打成浏览器能直接加载的静态文件(npm run build),用环境变量管好「开发 / 生产」两套 API 地址,配好 Nginx 的 try_files 让刷新不 404,最后给你一份前后端联调排错清单。做完这些,你的作品才真正「站」在公网上。

npm run build 到底做了什么

从源码到可部署产物
npm run build:源码 → 依赖打包 → 压缩混淆 → dist 产物源码 src/易读的 .vue/.jsVite 构建· 合并打包成少数文件· 压缩:去空格短变量· Tree-shaking 去死代码· 文件名加哈希指纹dist/index.html(唯一入口)assets/index-a1b2c3.jsassets/index-d4e5f6.css哈希文件名的意义:内容一变名字就变 → 浏览器缓存自动失效,用户总能拿到新版index.html 不加哈希(它必须稳定存在,才能引到新资源)开发时的 node_modules 不会进 dist——产物只有浏览器需要的部分
先讲为什么开发时 npm run dev 起的那个服务,是给你自己用的:它不压缩代码、不做兼容处理、还要靠 WebSocket 热更新——浏览器能跑,但它不适合直接丢到服务器上给用户访问。
npm run build 做的事,就是把「给开发者看」的源码,翻译成「给浏览器用」的产物:合并、压缩、去掉没用到的东西、把 .vue 编译成普通 JS,最后都塞进一个 dist/ 目录。
动手做在项目根目录跑构建,看它输出了什么。
终端bash
npm run build
终端输出命令行执行结果
运行结果: > hivue@0.0.0 build > vite build vite v5.4.8 building for production... transforming... ✓ 612 modules transformed. rendering chunks... computing gzip size... dist/index.html 0.68 kB │ gzip: 0.40 kB dist/assets/ArticleCard-9f3c1a2b.css 1.24 kB │ gzip: 0.68 kB dist/assets/index-2b7e6d91.css 286.41 kB │ gzip: 38.75 kB dist/assets/ArticleCard-4d8a5c11.js 2.10 kB │ gzip: 1.02 kB dist/assets/Home-8c1f2e04.js 4.37 kB │ gzip: 1.86 kB dist/assets/ArticleList-6a0b3f77.js 5.02 kB │ gzip: 2.11 kB dist/assets/ArticleDetail-1e9d4c88.js 5.86 kB │ gzip: 2.32 kB dist/assets/UserCenter-3c7f0a15.js 6.21 kB │ gzip: 2.48 kB dist/assets/Login-2f8b6e33.js 3.15 kB │ gzip: 1.44 kB dist/assets/index-7a5e1b90.js 143.72 kB │ gzip: 51.36 kB ✓ built in 3.42s
讲透:上面这几行输出分别意味着什么
  • 「612 modules transformed」= 编译:Vite 把 .vue、.js、CSS 全部读进来,把 <script setup> 和 <template> 编译成浏览器认识的 JS 渲染函数。
  • 「rendering chunks」= 分块:注意 Home / ArticleList / ArticleDetail 各自成了独立的小文件——这就是第 7 章路由懒加载 () => import(...) 的效果:首屏只加载首页那块,切到列表页才去下载列表的 chunk。
  • 压缩与 tree-shaking:产物是经过压缩的,而且只把你真正引用到的代码打进去。你在 api/article.js 里 import 了 3 个函数,没用到的第 4 个会被自动删掉。
  • gzip 那一列才是用户真正下载的体积。看 index-7a5e1b90.js:压缩前 143 kB,gzip 后只有 51 kB。服务器开启 gzip 后,用户下载的就是 51 kB。
  • 文件名里那串哈希(7a5e1b90)是内容指纹:内容变了文件名才变。这样浏览器可以放心地把 index-7a5e1b90.js 缓存一年——这正是「解决缓存」的正规做法。
package.json(scripts 部分)json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}
响应内容接口返回数据
运行结果: # dev → 开发服务器,带热更新,不要用于生产 # build → 产出 dist/,可部署 # preview → 本地起一个静态服务器来预览 dist/
避坑开发时一切正常、build 之后一堆报错,绝大多数是这三类:① 模板里用了没定义的变量——开发时 Vue 只给警告,构建时严格校验(有的项目开了 eslint-plugin-vue 直接失败);② 大小写写错,import ArticleCard from '@/components/articleCard.vue'——macOS 文件系统不区分大小写所以开发能跑,Linux 服务器区分大小写,部署后立刻 404;③ 用了 process.env.XXX,那是 Node 的写法,浏览器里没有 process,要用 import.meta.env(下一节讲)。

npm run preview:先本地验货

别拿线上环境试错
先讲为什么「构建成功」和「页面正常」是两件事。构建只保证代码能被打包,打包后的页面能不能跑、路由刷新会不会 404、资源路径对不对,只有真正访问一次才知道。npm run preview 就是让你在本地用一个静态服务器,先把 dist/ 里那套产物跑起来看看——发现问题时还在自己电脑上,改起来最便宜。
动手做构建完直接预览。
终端bash
npm run preview
终端输出命令行执行结果
运行结果: > hivue@0.0.0 preview > vite preview ➜ Local: http://localhost:4173/ ➜ Network: use --host to expose ➜ press h + enter to show help

打开 http://localhost:4173/,你会发现一个关键区别:preview 用的是 .env.production,不是开发环境那套代理。

讲透:preview 和 dev 的三个不同
  • 端口不同:dev 是 5173,preview 是 4173(Vite 默认)。
  • 没有热更新:preview 读的是已经打好的 dist/,改源码不会自动生效,得重新 build。
  • 没有 server.proxy:vite.config.js 里的代理只在 dev 生效。这就是为什么 preview 时接口请求会失败——它需要真正的后端地址,或者反向代理。这一点务必记牢,联调排错时它出现频率极高。
避坑用 preview 测试时接口 404 / 跨域报错,先别怀疑代码。打开 F12 Network 看请求地址:如果还是 http://localhost:4173/api/article/list,说明 .env.production 里的 VITE_API_BASE 还是相对路径 /api,而 preview 服务器不会把 /api 转发到 Flask。想让 preview 也能连后端,最简单的办法是本地开一个 Nginx(下一节配),或者用 http-server 之类的工具配合手写代理。

环境变量 .env 与环境模式

开发 / 生产两套配置
先讲为什么同一个项目,本地开发时后端跑在 127.0.0.1:5000,上线后接口在 https://你的域名/api。如果这个地址硬写在代码里,每次上线都要改代码、重新打包——这显然不对。
Vite 的答案是环境变量:把地址写进 .env 文件,构建时按当前模式自动挑一份注入代码。代码不动,配置变。
动手做在项目根目录建两个 env 文件。只有 VITE_ 开头的变量才能被前端代码读到,这是 Vite 的硬性规定。
.env.development(dev / preview 都不算它,dev 专用)bash
# 开发环境:走相对路径 /api,由 vite.config.js 的 server.proxy 转发给 Flask
VITE_API_BASE=/api
VITE_APP_TITLE=古典文学网(开发)
终端输出命令行执行结果
运行结果: # npm run dev 时被加载 # 页面里 import.meta.env.VITE_API_BASE === '/api' # 请求 → http://localhost:5173/api/... → 代理转发 → http://127.0.0.1:5000/api/...
.env.productionbash
# 生产环境:同样用相对路径 /api,由 Nginx 反向代理到 Flask
# 注意:这里绝不写死 http://127.0.0.1:5000 或某个内网 IP
VITE_API_BASE=/api
VITE_APP_TITLE=古典文学网
终端输出命令行执行结果
运行结果: # npm run build 时被加载 # 产物里的接口地址就固定为 /api/xxx,跟当前域名走 # 无论部署到哪个域名/IP,都不用重新打包
src/utils/request.js 与组件里怎么读js
// ① 在业务代码里读:只能读 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 { /* ... */ }
})
页面效果浏览器运行结果
运行结果: # npm run dev → mode = 'development',控制台打印 [dev] API Base = /api # npm run build → mode = 'production',产物内联为 /api # 若把变量写成 API_BASE(不带 VITE_ 前缀)→ import.meta.env.API_BASE 为 undefined
讲透
  • 加载优先级:.env(通用)< .env.local(本地覆盖,要写进 .gitignore)< .env.[mode] < .env.[mode].local。后加载的覆盖先加载的。
  • 模式(mode)和命令名不是一回事:dev 默认 mode 是 development,build 默认是 production,但你可以用 vite build --mode staging 指定读 .env.staging——多环境部署就靠这个。
  • 三个内置变量不用自己定义:import.meta.env.DEV(是否开发模式)、.PROD、.MODE。
避坑.env 里的内容会被原样打进前端产物,用户按 F12 就能看到。所以数据库密码、JWT 密钥、第三方服务的 secret key 绝对不能写在这里——那些必须放在后端。前端环境变量只适合放「公开也无所谓」的配置,比如接口地址、标题、是否开启某个开关。

生产环境 API 地址怎么配

相对路径 + 反向代理
先讲为什么上线后有两条路:① 把 VITE_API_BASE 写成后端的完整地址,比如 http://47.98.x.x:5000/api;② 写成相对路径 /api,让 Nginx 把 /api 开头的请求转发给 Flask。
一定要选 ②。原因很实在:写死 IP 之后,只要后端换了服务器、加了域名、开了 HTTPS,前端就得重新打包;而且写死 IP 会触发跨域(前端在 80 端口、后端在 5000 端口),你还得在后端配 CORS。用相对路径,前后端同源,跨域问题直接消失。
动手做对比一下两种写法,看清楚差别在哪。
两种配法对比text
【错误】写死 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 证书
效果预览运行结果
运行结果: # 方案二下,后端的 Flask 可以只监听 127.0.0.1:5000,外网根本访问不到 # 用户看到的所有请求都指向同一个域名,Cookie / Token / 缓存策略都更简单
讲透:「同源策略」这件事值得单独记住
  • 同源 = 协议 + 域名 + 端口 三者全相同。只要有一个不同,就是跨域,浏览器就会拦。
  • 跨域是浏览器的限制,不是服务器的。所以用 Postman 测永远没问题,一放到浏览器就报 blocked by CORS policy——这个区别能救你很多次。
  • 开发时 Vite 的 server.proxy 和生产的 Nginx 做的是同一件事:把 /api 转到 Flask。理解了这一点,你就理解了整个前后端分离的部署模型。

dist 里到底有什么

一个入口 + 一堆带哈希的资源
动手做把 dist/ 目录整个摊开看一遍。部署时要上传的就是这个目录,源码不用传。
dist/ 目录结构text
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 打开后是一行压缩过的 HTML,通过 <script type="module" src="/assets/index-xxx.js"> 加载 # 所有 .vue 源文件都消失了 —— 它们已经被编译进 assets 里的 js # 上传部署 = 把 dist/ 里的东西原样传到服务器目录,不需要 Node.js

看一眼 index.html 更好理解:

dist/index.htmlhtml
<!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>
页面效果浏览器渲染结果
运行结果: # 你写的 index.html 里那一长串 <script src="/src/main.js"> 被替换成了指向产物的一行 # <div id="app"></div> 保留 —— 挂载点不变 # 资源路径以 / 开头,所以部署时整个站点要放在域名根目录(放子目录要另外配 base)
讲透:为什么产物里面没有 .vue 文件了
  • 浏览器不认识 .vue,也不认识 import 语法里的裸模块名('vue'、'element-plus')。Vite 在构建时把这一切都翻译成浏览器原生能跑的 ESM 代码。
  • dist/ 是纯静态的:一堆 .html / .js / .css / 图片。服务器上不需要装 Node.js、不需要跑 npm run dev,任何一个能发文件的 Web 服务器(Nginx / Apache / CDN)都能托管它。
  • 如果你要把站点部署到子路径(比如 https://域名/hiwen/),必须在 vite.config.js 里设 base: '/hiwen/',否则资源路径全是 /assets/...,加载不到。

部署到 Nginx

try_files 是这一节的主角
Nginx try_files:先找文件,找不到就把路由交给 index.html用户访问/article/3/user/favoritestry_files 逐个试1. 有这个真实文件吗?2. 有这个目录吗?3. 都没有 → /index.htmljs/css 图片前端路由直接返回文件回 index.htmlVue 路由接管不配 try_files 会怎样:刷新 /article/3 → Nginx 找不到这个文件 → 404try_files $uri $uri/ /index.html; —— 一行配置,SPA 刷新白屏的终极解药/api 开头的请求例外:转发给 Flask(location /api 块)
先讲为什么先说那个让无数人踩坑的现象:部署后访问首页正常,点进详情页也正常,但按 F5 刷新一下就 404。
原因是:你的项目用的是 history 模式路由(createWebHistory()),地址栏里的 /article/1 只是前端「演出来」的——服务器上根本没有 /article/1 这个文件。点链接时是 JS 接管的,不经过服务器;一刷新,浏览器直接去找服务器要 /article/1,Nginx 找不到文件就回 404。
动手做写一份完整的 Nginx 站点配置:静态文件 + history 路由兜底 + /api 反代 + 静态资源缓存。
/etc/nginx/conf.d/hiwen.confnginx
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;
}
效果预览运行结果
运行结果: # nginx -t → syntax is ok / test is successful # systemctl reload nginx # 访问 http://hiwen.example.com/ → 首页正常 # 访问 http://hiwen.example.com/article/1 → 详情页正常 # 在详情页按 F5 刷新 → 仍然正常(不再 404)

再对比一下「不写 try_files」会发生什么,把这个现象刻进脑子里。

不写 try_files 的后果text
【配置缺失】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
                      → 渲染详情页 → 用户看到的内容和刷新前一模一样
效果预览运行结果
运行结果: # 缺失配置:直接输入 https://你的域名/article/1 也一样 404(分享出去的链接全打不开) # 正确配置:任意前端路由地址直接打开、刷新、分享,都能正常显示
避坑配了 try_files ... /index.html 之后,所有「真的不存在」的路径也会返回 200 + 首页内容。如果后端接口 404 也被这条规则兜住,就会出现「接口挂了却返回一段 HTML」的诡异现象——前端 JSON.parse 直接报 Unexpected token <。
所以 location /api/ 必须写在单独一个块里,且不要加 try_files,让它老老实实把 404 透传给前端。另外,location /api/ 结尾的那个斜杠别丢——proxy_pass http://127.0.0.1:5000 不带路径时,请求 /api/article/list 会原样转发到 /api/article/list,正好和后端蓝图的 url_prefix="/api/article" 对上。

联调排错清单

照着查,不要瞎猜
动手做把下面这张表打印出来贴在显示器边上。遇到问题先打开 F12 → Network,看清楚「请求发出去了没有、发到哪、回来什么」,再对照现象找原因。
联调排错清单text
现象                        最可能的原因                       怎么确认 / 怎么修
--------------------------  --------------------------------  --------------------------------------
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= 空字符串被发出去        请求前删掉空值参数
效果预览运行结果
运行结果: # 这张表覆盖了本项目联调 90% 的问题 # 排查顺序永远是:① 请求发了没 ② 发到哪 ③ 带了什么 ④ 回来什么 ⑤ 页面怎么处理

再给你两条命令行工具,比在浏览器里反复点更快:

用 curl 直接验证后端与代理bash
# ① 后端本身通不通(绕开浏览器,排除跨域干扰)
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
终端输出命令行执行结果
运行结果: # ① HTTP/1.1 200 OK … {"code": 200, "msg": "success", "data": {"list": [...]}} # ② HTTP/1.1 200 OK … 与 ① 内容一致 → 代理配置正确 # 若 ② 返回 404 / HTML → location /api/ 没配对,或漏了结尾斜杠 # ③ HTTP/1.1 200 OK … {"code":200,"msg":"success","data":{"id":3,...}} # 若返回 {"code":401,...} → token 无效或过期 # ④ HTTP/1.1 200 OK → try_files 兜底生效;返回 404 → 配置没写对
避坑浏览器缓存是排错的头号干扰项。你明明改了代码、重新 build、也重启了 Nginx,看到的却还是老页面——很可能只是浏览器把旧的 index.html 缓存了。做法:F12 → Network 勾上 Disable cache,或者用无痕窗口验证。同时在生产配置里,index.html 千万别跟着 /assets/ 一起设一年缓存,否则新版本永远推不出去。

衔接综合实战

从这里走向「真正上线」
先讲为什么到这里,课程三的三门课都走完了:课程一建库建表、写 SQL;课程二用 Flask 实现 18 个接口;课程三用 Vue 3 把这些接口变成四大模块的页面。
但你现在手里还是两台机器:一边 python app.py 跑在 5000,一边 npm run dev 跑在 5173。「把它们合成一个能给别人用的网站」这件事,就是综合实战要做的。
开发期 → 上线的完整流程text
阶段一 · 本地开发(你现在最熟悉的)
  终端 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)+ 接口文档 + 部署说明
效果预览运行结果
运行结果: # 阶段三 ⑤ 之后:同一台服务器、同一个域名,前端页面与后端接口终于同源 # 手机 / 同学 / 老师用各自设备打开域名,看到的是同一个运营中的网站
讲透:本章这四件事,其实是一件事
  • build 把源码变成产物;env 让同一份产物能适配不同环境;Nginx 决定产物怎么被访问、请求怎么被转发;排错清单保证出问题时你能定位到具体哪一环。
  • 它们共同回答一个问题:「我本地跑通的东西,怎么变成别人能访问的东西?」——这就是「开发」和「上线」之间那道墙。
  • 带着这份能力进入综合实战,你要做的不再是「学语法」,而是把三门课的成果装进一台服务器、配一个域名、让真实用户能用。从那一刻起,你写的就不再是练习,而是产品。
动手练习
1跑一次 npm run build,记录产物的文件名与大小。然后打开 Chrome DevTools 的 Network 面板(勾上 gzip),访问首页,看看主包实际下载了多少 KB。
2只改 .env.production 里的 VITE_API_BASE(改成 /api/v1),重新 build,验证产物里的接口地址真的变了。想一想:这个变量能不能改成后端服务器的真实 IP?为什么?
3迁移练习:「红影赏析」要部署到一台只有 80 端口的服务器上,后端 Flask 跑在 5000。请写出它的 Nginx 配置,并说明为什么不能把 VITE_API_BASE 写成 http://服务器IP:5000/api。
参考答案
练习答案:验证构建 + Nginx 配置nginx
# 第 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;
    }
}
效果预览运行结果
运行结果: # 第 1 题:主包 index-xxxx.js 压缩前约 143 kB,Network 里 gzip 后约 51 kB # 第 2 题:改 .env.production 后重新 build,产物内联地址变为 /api/v1 # → 证明环境变量是「构建期注入」,改了必须重新 build # 第 3 题:配置套用后 # curl -i http://movie.example.com/api/movie/list → 200 + JSON(代理生效) # curl -i http://movie.example.com/movie/12 → 200 + index.html(兜底生效) # 手机打开域名 → 影评、点赞、收藏全链路正常

第 2 题的关键词是「构建期注入」:环境变量不是在运行时读的,而是在 build 那一刻被字面替换进产物。所以改了 .env 一定要重新 build,热更新不会帮你重读它。

本章小结