Java 项目实战: 外卖平台优化-前端dist部署与Nginx反向代理rewrite

外卖平台部署: 前端 dist 部署与 Nginx 反向代理 rewrite 配置

纲要

「部署前端项目」,两步完成前端上线:

  • 上传 dist 目录 :前端 Webpack 打包产物,整体放到 Nginx 的 html 目录下
  • 修改 nginx.conf :root 指向 html/dist,再加一段 ^~ /api/ 的反向代理
  • rewrite 的必要性 :前端请求带 /api 前缀,后端接口没有,rewrite 负责剥掉它
  • rewrite 正则解析 :^/api/(.*)$ 与 $1 的含义,以及 break 标志
  • 完整请求链路 :点登录 → /api/employee/login → 重写为 /employee/login → 转发到 8080
  • ^~ 前缀匹配 :为什么用它而不是普通 location

Spring Boot :8080 Nginx :80 浏览器 Spring Boot :8080 Nginx :80 浏览器 #mermaid-svg-zufiyFkru5qrFnBo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zufiyFkru5qrFnBo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zufiyFkru5qrFnBo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zufiyFkru5qrFnBo .error-icon{fill:#552222;}#mermaid-svg-zufiyFkru5qrFnBo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zufiyFkru5qrFnBo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zufiyFkru5qrFnBo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zufiyFkru5qrFnBo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zufiyFkru5qrFnBo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zufiyFkru5qrFnBo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zufiyFkru5qrFnBo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zufiyFkru5qrFnBo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zufiyFkru5qrFnBo .marker.cross{stroke:#333333;}#mermaid-svg-zufiyFkru5qrFnBo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zufiyFkru5qrFnBo p{margin:0;}#mermaid-svg-zufiyFkru5qrFnBo .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zufiyFkru5qrFnBo text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-zufiyFkru5qrFnBo .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zufiyFkru5qrFnBo .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-zufiyFkru5qrFnBo .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-zufiyFkru5qrFnBo .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-zufiyFkru5qrFnBo #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-zufiyFkru5qrFnBo .sequenceNumber{fill:white;}#mermaid-svg-zufiyFkru5qrFnBo #sequencenumber{fill:#333;}#mermaid-svg-zufiyFkru5qrFnBo #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-zufiyFkru5qrFnBo .messageText{fill:#333;stroke:none;}#mermaid-svg-zufiyFkru5qrFnBo .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zufiyFkru5qrFnBo .labelText,#mermaid-svg-zufiyFkru5qrFnBo .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-zufiyFkru5qrFnBo .loopText,#mermaid-svg-zufiyFkru5qrFnBo .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-zufiyFkru5qrFnBo .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zufiyFkru5qrFnBo .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-zufiyFkru5qrFnBo .noteText,#mermaid-svg-zufiyFkru5qrFnBo .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-zufiyFkru5qrFnBo .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zufiyFkru5qrFnBo .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zufiyFkru5qrFnBo .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zufiyFkru5qrFnBo .actorPopupMenu{position:absolute;}#mermaid-svg-zufiyFkru5qrFnBo .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-zufiyFkru5qrFnBo .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zufiyFkru5qrFnBo .actor-man circle,#mermaid-svg-zufiyFkru5qrFnBo line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-zufiyFkru5qrFnBo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET / (访问首页) location / 匹配 root html/dist 返回 dist/index.html POST /api/employee/login location ^~ /api/ 匹配 rewrite ^/api/(.*) /1 break /api/employee/login → /employee/login proxy_pass http://192.168.138.101:8080 POST /employee/login R<Employee> JSON 返回响应

一、dist 目录是什么

前端打包产物

前端开发有自己的技术栈,开发完成后同样需要打包。前端用 Webpack 打包,产物就是 dist 目录。

它和后端打包完全不同:

后端 前端
打包工具 Maven Webpack
产物 *.jar / *.war dist/ 目录
内容 字节码 + 依赖 HTML / CSS / JS / 字体 / 图片

dist 目录的内容

txt 复制代码
dist/
├── index.html                          # 首页入口
├── favicon.ico                         # 站点图标
├── static/
│   ├── css/
│   │   └── app.a1b2c3d4.css           # 压缩后的样式
│   ├── js/
│   │   ├── app.e5f6g7h8.js            # 压缩后的业务逻辑
│   │   ├── chunk-vendors.i9j0k1l2.js  # 第三方库
│   │   └── ...
│   ├── fonts/                          # 字体文件
│   └── img/                            # 图片资源
└── ...

几个值得注意的点:

文件名带哈希 。app.a1b2c3d4.css 这种命名是 Webpack 的内容哈希------内容变了文件名就变,从而配合长缓存策略(Cache-Control: max-age=31536000)实现"内容更新时立即生效,未更新时永久缓存"。

文件被压缩 。打开 JS 文件会发现所有代码挤在一行,变量名被缩短成单字母,注释全部删除。这是生产构建的标准处理,能把体积压缩到原来的 1/3 甚至更小。

与源码不同 。打包后的文件名、目录结构与开发时的源码完全对不上------这是打包工具做了合并、压缩、重命名。所以不要试图在服务器上改 dist 里的代码,改源码重新打包才是正道。

二、部署第一步:上传 dist

操作

把课程资料中的 dist 目录整个 上传到 Nginx 的 html 目录:

txt 复制代码
/usr/local/nginx/html/
├── 50x.html
├── index.html          # Nginx 自带的默认首页
└── dist/               # ← 上传到这里
    ├── index.html
    └── static/

用 FinalShell 等工具的上传功能,或 scp:

bash 复制代码
scp -r ./dist root@192.168.138.100:/usr/local/nginx/html/

也可以用 lrzsz(第 4 篇讲过):

bash 复制代码
cd /usr/local/nginx/html
rz          # 选择文件上传(目录需先打包成 tar.gz)
tar -zxvf dist.tar.gz

验证

bash 复制代码
cd /usr/local/nginx/html/dist
ls
# index.html  static  favicon.ico ...

目录结构与本地一致即上传成功。

三、部署第二步:修改 nginx.conf

完整配置

把原有的监听 80、82、8080 的虚拟主机注释掉,改为:

nginx 复制代码
server {
    listen       80;
    server_name  localhost;

    location / {
        root   html/dist;
        index  index.html;
    }

    location ^~ /api/ {
        rewrite ^/api/(.*)$ /$1 break;
        proxy_pass http://192.168.138.101:8080;
    }

    location = /50x.html {
        root   html;
    }
}

为什么要注释掉之前的 server :前面的练习配了监听 80(静态资源)、82(反向代理)、8080(负载均衡)三个虚拟主机。现在正式部署,只需要一个 80 端口的 server 同时承担静态资源与反向代理,多余的会干扰。

配置逐条解释

listen 80 + server_name localhost

监听 80 端口,用户访问时不用写端口号。

location / ------ 静态资源

nginx 复制代码
location / {
    root   html/dist;
    index  index.html;
}

关键变化是 root 从 html 变成了 html/dist。

  • root html; → 根目录是 /usr/local/nginx/html,首页是 Nginx 自带的欢迎页
  • root html/dist; → 根目录是 /usr/local/nginx/html/dist,首页是项目页面

这就是替换默认欢迎页的原理 ------不是删掉 index.html,而是把根指向了 dist。

location = /50x.html ------ 错误页

nginx 复制代码
location = /50x.html {
    root   html;
}

注意这里的 root 是 html 而不是 html/dist------错误页用 Nginx 自带的那个,因为 dist 里没有 50x.html。

= 表示精确匹配,优先级最高。

location ^~ /api/ ------ 反向代理

nginx 复制代码
location ^~ /api/ {
    rewrite ^/api/(.*)$ /$1 break;
    proxy_pass http://192.168.138.101:8080;
}

下一节详细展开。

检查与重启

bash 复制代码
nginx -t
nginx -s reload

访问 http://192.168.138.100,看到的不再是 Welcome to nginx!,而是项目的登录页面------root 生效了。

四、为什么需要 rewrite

前端发出的请求

打开浏览器 F12,点登录按钮,观察请求:

txt 复制代码
POST http://192.168.138.100/api/employee/login

注意三点:

  • IP 是 192.168.138.100------请求发给了 Nginx 自己(同源,无跨域)
  • 端口是 80------默认端口
  • 路径是 /api/employee/login------带了 /api 前缀

后端能处理的路径

后端的 EmployeeController:

java 复制代码
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
    ...
}

类上是 @RequestMapping("/employee"),方法上是 @PostMapping("/login"),所以完整路径是:

txt 复制代码
/employee/login          ← 没有 /api

冲突

路径
前端发的 /api/employee/login
后端要的 /employee/login

如果不做处理直接转发 ,请求到后端变成 /api/employee/login,Spring MVC 找不到对应的 Handler,返回 404。

两种解法

解法一:改后端代码 ,给所有 Controller 加 /api 前缀(加 context-path 或改 @RequestMapping)。

yaml 复制代码
server:
  servlet:
    context-path: /api

解法二:在 Nginx 层用 rewrite 剥掉前缀(课程采用)。

推荐第二种 ------后端接口路径不应被前端约定绑架。而且 /api 只是前端区分"动态请求"的标记,让 Nginx 用它做路由判断更合理:/api/** 走后端,其他走静态资源。

这也是实际项目中的主流做法 :前端统一加前缀,Nginx 靠前缀区分动静态,同时剥掉前缀转发。好处是后端接口路径保持干净,Nginx 配置一处即可。

五、rewrite 正则解析

配置

nginx 复制代码
rewrite ^/api/(.*)$ /$1 break;

语法

复制代码
rewrite  <正则表达式>  <替换后的路径>  [标志];

逐段拆解

^/api/(.*)$ ------ 匹配规则

片段 含义
^ 以......开始
/api/ 字面量 /api/
(.*) 捕获组:任意字符,任意长度
$ 以......结束

/$1 ------ 替换结果

$1 引用第一个捕获组(即 (.*) 匹配到的内容),前面加 /。

break ------ 标志

表示重写完成后停止后续 rewrite 规则的处理,直接用新路径继续。

实战推演

以 /api/employee/login 为例:

txt 复制代码
原始路径:/api/employee/login

正则匹配:^/api/(.*)$
          ├──┬──┘├─┬─┘
          │      └─ (.*) 捕获到 "employee/login"
          └─ 字面量 /api/

$1 = "employee/login"

替换结果:/$1 = /employee/login

即:

txt 复制代码
/api/employee/login  →  /employee/login

再配合 proxy_pass http://192.168.138.101:8080;,最终请求:

txt 复制代码
http://192.168.138.101:8080/employee/login

正好是后端能处理的路径。

更多例子

原始请求 重写后 转发到后端
/api/employee/login /employee/login 8080/employee/login
/api/employee/logout /employee/logout 8080/employee/logout
/api/dish/page?page=1 /dish/page?page=1 8080/dish/page?page=1
/api/category /category 8080/category
/api/common/upload /common/upload 8080/common/upload

Query 参数不受影响 ------rewrite 只处理路径部分,?page=1 这类查询串会自动附加。

break 与 last 的区别

标志 行为
break 重写后停止 rewrite 阶段,用当前 location 继续处理
last 重写后重新发起一轮 location 匹配
redirect 返回 302 临时重定向,浏览器地址栏变化
permanent 返回 301 永久重定向

这里用 break 是对的------重写完直接交给同 location 的 proxy_pass。

如果用 last ,重写后的 /employee/login 会重新走一遍 location 匹配,可能被 location / 捕获而变成请求静态资源,导致 404 。这是 rewrite 的高频坑。

六、^~ 前缀匹配的意义

nginx 复制代码
location ^~ /api/ { ... }

^~ 是 location 的修饰符,表示前缀匹配且优先于正则。

回顾第 66 篇讲的优先级:

写法 类型 优先级
location = /path 精确匹配 最高
location ^~ /prefix/ 优先前缀匹配 次高(不再检查正则)
location ~ /regex/ 正则匹配 中
location /prefix/ 普通前缀匹配 低(会被正则覆盖)
location / 通用匹配 最低

为什么这里必须用 ^~:

如果写成普通的 location /api/,它的优先级低于正则 location。当配置里有其他正则 location(比如前面第 66 篇演示过的 location ~* \.(jpg|png)$ 匹配图片)时,/api/common/upload 这种带图片语义的路径可能被正则抢走,导致转发失败。

用 ^~ /api/ 后,一旦匹配到 /api/ 前缀就立即确定用它,不再看任何正则,行为确定可控。

建议 :凡是做反向代理的 location,统一用 ^~。

七、完整配置与验证

最终 nginx.conf(相关部分)

nginx 复制代码
server {
    listen       80;
    server_name  localhost;

    # 前端静态资源:dist 目录
    location / {
        root   html/dist;
        index  index.html;
    }

    # 动态请求:剥掉 /api 前缀后转发到后端
    location ^~ /api/ {
        rewrite ^/api/(.*)$ /$1 break;
        proxy_pass http://192.168.138.101:8080;

        # 透传客户端信息
        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;

        proxy_connect_timeout 60s;
        proxy_read_timeout    60s;
    }

    # 错误页(用 Nginx 自带的)
    location = /50x.html {
        root   html;
    }
}

SPA 路由回退

如果前端用了 Vue Router 的 history 模式,还需加 try_files:

nginx 复制代码
location / {
    root   html/dist;
    index  index.html;
    try_files $uri $uri/ /index.html;
}

否则刷新 /dish/list 这样的前端路由会 404。

验证步骤

第一步:静态资源

访问 http://192.168.138.100,能看到项目登录页------root html/dist 生效。

第二步:反向代理(后端未启动时)

点登录,请求 /api/employee/login,此时后端还没部署,会返回 502 Bad Gateway 或连接失败。

这不是配置错误,只是后端服务还没起来。第 76 篇部署后端后即可打通。

第三步:后端启动后

再点登录,请求经 Nginx 转发到 8080,返回登录结果,页面跳转------全链路贯通。

验证技巧 :在 Nginx 机器上直接 curl 后端,排除后端问题:

bash 复制代码
curl -X POST http://192.168.138.101:8080/employee/login \
     -H "Content-Type: application/json" \
     -d '{"username":"admin","password":"123456"}'

能返回结果说明后端没问题,问题在 Nginx 配置;不能则返回后端排查。

八、常见问题

现象 原因 解决
访问 80 还是 Nginx 欢迎页 root 没改成 html/dist 检查 location / 的 root
页面能开但样式全无 dist 上传不完整 检查 static/ 目录是否存在
点登录报 404 rewrite 未生效或 proxy_pass 地址错 检查 rewrite 正则与后端端口
点登录报 502 后端未启动 先 curl 验证后端
刷新子页面 404 未配 try_files 加 try_files $uri $uri/ /index.html;
/api 请求被静态资源 location 吃掉 未用 ^~ 改为 location ^~ /api/
用 last 后请求失败 重写后重新匹配到 location / 改用 break

API 速览

指令 作用
root html/dist 静态资源根目录指向 dist
index index.html 默认首页
try_files $uri $uri/ /index.html SPA 路由回退,避免刷新 404
location ^~ /api/ 优先前缀匹配,不再检查正则
rewrite <regex> <replacement> <flag> URL 重写
^/api/(.*)$ 正则:以 /api/ 开头,捕获其后全部内容
$1 引用第一个捕获组
break 重写后停止 rewrite 阶段,用当前 location 继续
last 重写后重新匹配 location(此处会导致问题)
proxy_pass http://ip:port 反向代理目标(无尾斜杠,保留重写后的完整路径)
scp -r ./dist root@host:/path 上传目录到服务器
nginx -t / nginx -s reload 检查配置 / 重载

官方文档

总结

前端部署就两步 :把 dist 目录整个传到 Nginx 的 html 下;把 nginx.conf 里 location / 的 root 从 html 改成 html/dist。访问 80 端口看到的就不再是 Nginx 欢迎页而是项目页面------不是删了默认首页,而是换了根路径。

rewrite 解决的是"前端带 /api 前缀、后端不带"的路径不匹配问题 。后端接口路径不应该被前端约定绑架,所以在 Nginx 层剥掉前缀更合理。正则 ^/api/(.*)$ 捕获 /api/ 之后的所有内容存入 $1,替换成 /$1 即得到 /employee/login。

break 与 last 的选择是关键 。用 break 重写后直接在当前 location 继续(交给 proxy_pass);用 last 会重新走 location 匹配,很可能被 location / 捕获变成请求静态资源,导致 404。这是 rewrite 最高频的坑。

反向代理的 location 统一用 ^~ 。普通 location /api/ 优先级低于正则 location,可能被图片之类的正则规则抢走;^~ 一旦匹配就不再检查正则,行为确定可控。

502 不等于配置错误 ------后端还没部署时就是这个表现。排查时先在 Nginx 机器上 curl 后端,能通说明后端正常、问题在 Nginx;不能通则先修后端。

下一篇部署后端:在服务器 B 上用 git clone 拉代码、上传 reggieStart.sh 脚本一键完成"拉代码 → Maven 打包 → java -jar 启动",并解决图片展示不出来这个最后一公里问题。

相关推荐
专业程序开发源42 分钟前
springboot生命故事书制作小程序65290-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·mysql·小程序·课程设计
鬓戈1 小时前
Claude Code frontend-design 插件调研 与 Vue 旧系统风格一致性方案
前端·vue.js·人工智能
IT_陈寒1 小时前
SpringBoot自动配置的坑我帮你踩过了
前端·人工智能·后端
代码山河1 小时前
Java的前世今生:从Oak语言到Java 23的发展史
java·学习·架构·教程·面向对象·项目
vx_Biye_Design1 小时前
springboot角色扮演服务平台65161-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·python·spring·课程设计
李游Leo1 小时前
HarmonyOS 7 QuickDock 闪控窗开发实录 02:floatView × floatingBall:形态切换、位置恢复与单一状态源【鸿蒙心迹】
java·华为·harmonyos
企业数字化笔记1 小时前
视频局部合成怎么做得自然?掩膜修复、边缘羽化、时序稳定与背景替换
java·javascript·音视频
仍然.1 小时前
Redis---主从复制
java·数据库·redis
vx_Biye_Design1 小时前
springboot咖啡厅顾客点单管理系统12080-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·课程设计·express