ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

若依前后端分离项目部署避坑:Nginx与Tomcat白屏404排查

若依前后端分离项目部署避坑:Nginx与Tomcat白屏404排查

简介:面向使用若依框架进行前后端分离项目落地的开发者,这份部署文档围绕 Linux+Nginx 与 Windows+Tomcat 两套环境展开,并补充第三方 API 调用时的跨域处理及部署后页面 404 的排查思路。资源包仅含 1 个 docx 文件,大小约 223KB,内容以操作步骤、关键配置片段和问题定位说明为主,适合需要把 Vue+Spring Boot 项目从本地运行推进到服务器部署的初中级开发者参考。已有 7766 人学习或下载,说明其部署路径具有一定通用性。读者可据此梳理前后端打包、Nginx 代理、Tomcat 路径配置、Redis 缓存服务启动以及 CORS/代理配置等环节,对照排查登录后空白页、刷新 404、菜单打不开等常见问题,形成较完整的部署与排错思路。

1. 场景:本地跑得好好的,扔上服务器第一眼就是白屏

上周又被叫去看一个若依前后端分离项目部署现场:开发在自己机器上npm run dev一切正常,账号能登、菜单能点;打成包传到服务器,浏览器敲 IP 回车,页面白了,F12 里一堆静态资源 404,登录接口挂在/prod-api/上根本没转发出去。这类翻车在若依项目里太常见了——框架本身没问题,问题几乎全在「打包产物的落位」和「代理转发的那一行配置」上。若依后端是 Spring Boot + Spring Security + Redis + Jwt,前端是 Vue + Element UI,两套东西分开部署,任何一个路径前缀对不上,表现就是白屏、菜单打不开、刷新 404。这篇笔记按 Linux + Nginx 和 Windows + Tomcat 两条线走,把版本、命令、配置、排错都摊开写,适合第一次给若依做生产部署的人照着抄。

2. 环境清单:先把版本对齐,再谈部署

2.1 一张表定版本,别在版本上赌运气

若依前后端分离版的部署是「两套运行时 + 两套中间件」的组合:后端要 JDK 和 Maven(打包时用),前端要 Node(打包时用),服务器上要 Redis 存缓存和 token,Nginx 或 Tomcat 负责把静态资源和接口拼到一起。下表是这套组合里我会固定下来的版本范围,不是越新越好,是踩坑概率最低:

组件建议版本装在哪台机器关键说明
JDK1.8应用服务器后端基于 Spring Boot 2.x,JDK 8 最稳,JDK 17 会碰到模块化相关报错
Maven3.6.x打包机(开发机即可)只用于mvn clean package,服务器上不必装
Node.js14.x 或 16.x打包机老项目里的 sass-loader / node-sass 对高版本 Node 不友好,装 18 以上经常编译失败
MySQL5.7 或 8.0数据库服务器8.0 注意驱动版本和连接串里的时区参数
Redis5.x / 6.x应用服务器或独立缓存机若依用它存登录 token、字典缓存、验证码
Nginx1.18 以上前端机或与应用同机静态资源托管 + 后端接口反向代理
Tomcat8.5 或 9.0Windows 服务器只在「不用 Nginx」的 Windows 方案里出现

这里有个容易被忽略的点:项目正文的部署清单里只写了 Redis 和 Nginx,没提 MySQL。但若依的application-druid.yml一启动就要连库,库没建、账号密码没改,jar 包连日志都不会给你留几行就直接退出。所以顺序上,永远先备好库和缓存,再启后端。

2.2 打包机和服务器分离时,先做三项检查

很多人的部署环境是「Windows 上打包、Linux 上运行」,两边环境不一致就是各种玄学的来源。上传之前,我会在三处各跑一遍命令确认:

# 打包机:确认 JDK 和 Node 版本与目标环境一致 java -version mvn -v node -v npm -v
# 服务器:确认端口没被占、JDK 版本对、防火墙放行了 80 java -version ss -lntp | grep -E '80|8080|6379|3306' firewall-cmd --list-ports

第一段在开发机上跑,重点看node -v。如果npm install阶段就报node-sass编译失败,别去折腾环境变量,直接换 Node 14/16 重装依赖,比修编译工具链快得多。第二段在服务器上跑,重点看 8080 是否被别的进程占着——jar 启动失败时日志里那句Port 8080 was already in use经常被后面的异常盖住,一眼扫不到。firewall-cmd --list-ports没看到 80/tcp 就先加上,否则 Nginx 起得再对,浏览器也是转圈。

第三项检查是字符集和时区。若依的菜单名、日志时间都受这个影响,服务器时区不对会出现「登录时间差 8 小时」这种看似业务 bug 的问题:

timedatectl set-timezone Asia/Shanghai java -XshowSettings:properties -version 2>&1 | grep -E 'file.encoding|user.timezone'

file.encoding输出不是 UTF-8 的话,后面启动 jar 时补一个-Dfile.encoding=UTF-8,省得中文菜单在页面上变成问号再去怀疑前端编码。

3. Linux + Nginx:jar 包和 dist 目录各自落到哪

3.1 后端打包:mvn clean package 与 nohup 启动

后端在 IDEA 的 Terminal 里执行打包,产物在ruoyi-admin/target/下:

# 在项目根目录执行,跳过测试类打包更快,也更少受测试环境影响 mvn clean package -Dmaven.test.skip=true # 产物确认 ls -lh ruoyi-admin/target/*.jar

clean会清掉上一次的 target,避免拿到旧包;-Dmaven.test.skip=true是跳过测试编译和执行,生产打包一般不需要在打包阶段跑单测,尤其是连不上数据库的环境,测试类会把整个构建卡死。如果你的项目模块多,注意ruoyi-admin才是可执行模块,其余模块打出来的是普通依赖 jar,别传错了。

上传到服务器后,启动命令不要直接java -jar占着终端,用nohup放后台:

# 后台启动,日志落到当前目录的 msg.log nohup java -jar -Xms512m -Xmx1024m \ -Dfile.encoding=UTF-8 \ ruoyi-admin.jar > msg.log 2>&1 & # 确认进程和启动日志 ps -ef | grep ruoyi-admin.jar | grep -v grep tail -f msg.log

-Xms和-Xmx给小内存机器一个上限,1G 堆对一般业务够用,机器只有 2G 内存时堆别超过 1G,否则系统 OOM Killer 会挑你的 Java 进程下手。2>&1是把标准错误也重定向进日志文件,少了这一段,启动失败时日志里只有正常输出,真正的异常全丢了,排查起来就是个黑匣子。&让它后台运行,终端断开也不影响。

启动成功的标志是日志尾部出现Started RuoYiApplication in xxx seconds。如果进程起来几秒就没了,直接看msg.log里第一个Caused by,九成是数据库连不上或者 Redis 没起。

3.2 前端打包:npm run build:prod --report 与 dist 上传

前端打包在生产环境用build:prod这个脚本,--report会在打包完成后生成一份体积分析报告,方便看哪个包特别大:

# 首次部署先装依赖,网络慢就挂国内源 npm install --registry=https://registry.npmmirror.com # 生产打包,产物在当前目录的 dist 下 npm run build:prod --report # 看下产物大小 du -sh dist ls dist

打包完成后dist目录里是index.html、static/等静态文件,整个目录传到服务器,比如/usr/local/ruoyi/dist。注意是传目录里的内容,不是把 dist 再套一层——Nginx 的root指向哪一层,决定浏览器能不能找到index.html。

打包前务必确认.env.production里的接口前缀:

# 前端根目录下 .env.production VUE_APP_BASE_API = '/prod-api'

这个值必须和 Nginx 里的代理前缀一致,也和后面 Tomcat 方案里的 war 包名一致。改了这个值不重新打包,等于没改。

3.3 nginx.conf:server 段的两条 location 决定成败

Nginx 的配置核心就两段:一段把静态资源指到 dist,一段把/prod-api/转发到后端 jar。编辑conf/nginx.conf,在http块里加一个server:

server { listen 80; server_name localhost; # 前端静态资源:dist 上传到哪个目录,root 就指到哪 location / { root /usr/local/ruoyi/dist; try_files $uri $uri/ /index.html; index index.html index.htm; } # 后端接口:注意 proxy_pass 末尾的这个斜杠 location /prod-api/ { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8080/; } }

try_files $uri $uri/ /index.html是给 Vue 的 history 路由兜底:用户直接访问/system/user这种地址时,磁盘上没有对应文件,就回退到index.html交给前端路由处理,不写这行刷新必 404。

proxy_pass http://127.0.0.1:8080/末尾的斜杠是整套配置里最容易写错的一个字符。带斜杠,Nginx 会把请求路径里的/prod-api前缀去掉再转发,后端收到的是/system/user/list;不带斜杠,后端收到的是/prod-api/system/user/list,接口直接 404。因为若依后端的接口路径本身没有/prod-api这一段,所以这里必须是带斜杠的写法。

改完配置先测语法再重载,别直接 restart:

nginx -t nginx -s reload

nginx -t会告诉你第几行少了分号或括号不匹配,比改完重启失败再回头找要快。另外文件上传功能常见的 413 报错,是 Nginx 默认请求体限制太小,在server块里补一行client_max_body_size 50m;即可。

4. Windows + Tomcat:war 包、Context 与 404 兜底

4.1 pom.xml 改 war 与重命名为 prod-api

没有 Nginx 的 Windows 环境,靠 Tomcat 一个容器同时托管前端静态文件和后端接口。第一步是把打包方式从 jar 改成 war,在ruoyi-admin模块的pom.xml里改:

<!-- 原来通常是 <packaging>jar</packaging> --> <packaging>war</packaging> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <!-- 外置 Tomcat 部署时必须 provided,否则内嵌容器和外置容器打架 --> <scope>provided</scope> </dependency>

provided的意思是编译时需要、打包时不带进 war,因为运行时容器自己提供 Servlet 实现。不加这一条,war 里塞了一份内嵌 Tomcat,部署到外置 Tomcat 上会出现类冲突,表现是启动日志里一堆重复的 Servlet 容器告警。

另外 war 包能被外置 Tomcat 拉起,前提是启动类继承了SpringBootServletInitializer。若依 admin 模块自带这个类,一般不用动;但如果之前裁过模块或改过启动类,这一处要确认,不然 war 扔进去 Tomcat 启动了、应用却没初始化,页面永远是 404。

打包完成后重命名:

ruoyi-admin.war → %TOMCAT_HOME%\webapps\prod-api.war

Tomcat 会自动解压成prod-api目录,应用的 context path 就是/prod-api,正好和前端.env.production里的VUE_APP_BASE_API = '/prod-api'对上。这里的机制和 Nginx 完全不同:Nginx 是纯转发、需要手动剥前缀;Tomcat 是按 context path 匹配,容器会自动把/prod-api从请求路径里去掉,所以不需要再去改前端前缀,也不要多此一举地在 Tomcat 里配 rewrite。

4.2 server.xml:端口、Host 与 Context 三处改动

前端dist目录整个拷到webapps下,然后改conf/server.xml。原生 Tomcat 对外是 8080 端口,如果希望浏览器直接敲 IP 不带端口,把 Connector 的端口改成 80:

<Connector port="80" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

接着在<Host>节点内部加一行 Context,把根路径指到 dist:

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Context path="/" docBase="D:/tomcat/webapps/dist" reloadable="true" crossContext="true"></Context> </Host>

path="/"表示访问根路径时用这个应用;docBase写 dist 的绝对路径最稳,写相对路径是相对于webapps的,容易因为多套了一层目录而失效;reloadable="true"是改了类文件自动重载,调试期方便,生产环境可以关掉省资源。

这一行不加的典型后果是:登录页能打开、账号密码也能提交成功,登录之后一片空白。原因是前端路由跳转后地址变成IP:端口/路由的形式,根路径下没有对应的静态资源入口,页面自然渲染不出来。这个现象我见过不止一次,加不加 Context 就是一句话的差别。

4.3 dist 下补 WEB-INF/web.xml,刷新不再 404

上一步解决的是首屏白屏,接着会遇到第二个问题:登录进去之后随便按 F5 刷新,直接 404。原因是 Tomcat 找不到/system/user这种路径对应的物理文件,也不会像 Nginx 那样自动回退到index.html。

解法是在 dist 目录里建WEB-INF文件夹,放一个web.xml,把 404 统一指向index.html:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1" metadata-complete="true"> <display-name>Router for Tomcat</display-name> <error-page> <error-code>404</error-code> <location>/index.html</location> </error-page> </web-app>

metadata-complete="true"表示不扫描注解,纯静态站点用不上注解扫描,打开反而拖慢启动。error-page把 404 状态码重定向到index.html,本质和 Nginx 的try_files是一个思路,只是 Tomcat 得靠这个描述文件才能实现。

改完重启 Tomcat,访问http://IP即可。Windows 下如果 80 端口被系统服务占用,把 Connector 改回 8080 用http://IP:8080访问,别硬抢端口。

5. 跨域与上线排查:第三方接口报错、菜单打不开的现场记录

5.1 跨域的三条走法:devServer 代理、全局 CORS、Nginx 反代

调用第三方服务(地图、短信、对象存储这类)时的跨域,处理位置不同,适用场景也不同。第一条是开发期用的 devServer 代理,只在npm run dev生效,打包后无效:

// vue.config.js devServer: { port: 80, proxy: { [process.env.VUE_APP_BASE_API]: { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { ['^' + process.env.VUE_APP_BASE_API]: '' } } } }

changeOrigin: true会把请求头里的 Host 改成目标地址,绕过服务端的 Host 校验;pathRewrite负责把/prod-api前缀去掉。这套配置只解决本地开发,生产环境不生效,别拿它当线上方案。

第二条是后端全局 CORS,适合「前端直连后端、后端再转发第三方」的结构:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") // 生产环境建议写死域名 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

allowedOriginPatterns配*加allowCredentials(true)在 Spring Boot 2.4 之后是允许的,老版本用allowedOrigins("*")会直接抛异常,这是升级后常见的翻车点。maxAge是预检结果的缓存秒数,能减少 OPTIONS 请求次数。

第三条是 Nginx 反向代理,适合第三方接口不能改、又不想在后端写转发代码的情况:

location /third-api/ { proxy_set_header Host third.example.com; proxy_pass https://third.example.com/; proxy_ssl_server_name on; }

前端改成请求同域的/third-api/xxx,浏览器层面就不存在跨域了,跨域问题被挪到服务端之间,而服务端之间不受同源策略限制。三种走法里,我一般优先选第三条,前端不用改代码,后端不用加配置,问题收敛在 Nginx 一处。

5.2 五条踩坑记录:从现象到解决

第一条:改完菜单重新部署,菜单点不开、页面空白。现象是登录正常,左侧菜单渲染出来了,点进去内容区空白或者直接跳回首页。原因是vue.config.js里的publicPath被打成了相对路径./,改成绝对路径/后重新打包即可。相对路径在首页能加载资源,一旦路由跳到二级页面,相对路径的基准就变了,静态资源全部 404。这个坑的隐蔽之处在于:本地 dev 模式永远正常,只有打包后多层路由才暴露。

第二条:Tomcat 部署登录成功,进去是白屏。现象如上文所说,登录接口通了,跳转后空白。原因是server.xml里没配<Context path="/" docBase="...dist">,根路径没有指向前端入口。补上 Context 并重启 Tomcat 解决。判断方法很简单:直接访问http://IP/index.html能打开,访问http://IP/打不开,就是这个问题。

第三条:Nginx 部署后接口全部 404,前端页面正常。现象是页面能打开、登录按钮点下去报 404。原因是proxy_pass末尾少了斜杠,/prod-api前缀被原样带给了后端。补上斜杠后nginx -t && nginx -s reload即可。这个字符的差别不会报配置错误,只能靠抓浏览器 Network 里的请求路径来定位。

第四条:jar 包启动几秒就退出,msg.log 里只有半截日志。现象是ps查不到进程,日志文件末尾没有Started字样。原因通常是 MySQL 连不上或者 Redis 没启动,Caused by被前面的框架日志淹没了。解决顺序是先systemctl status redis确认缓存活着,再用mysql -h -u -p手工连一次库,最后检查application-druid.yml里的账号密码和生产库名。少2>&1的时候这个日志根本不全,属于自己给自己挖坑。

第五条:前端上传文件报 413,或者大请求体被截断。现象是上传接口返回 413,小文件正常。原因是 Nginx 默认client_max_body_size只有 1M。在server块里加client_max_body_size 50m;后 reload。另外 Tomcat 方案下对应的是 Connector 的maxPostSize,两边都要看一眼,别只改一处。

6. 验收:从 IP 到菜单点开的一遍走查

部署完别急着交付,按顺序走一遍,能挡掉九成的「上线后才发现」:

步骤操作期望结果不通过先查
1浏览器访问http://IP出现登录页,静态资源 Network 全 200Nginx root / Tomcat Context
2直接访问http://IP/system/user后按 F5页面正常渲染不 404try_files / WEB-INF/web.xml
3用正确账号登录拿到 token,跳转首页/prod-api前缀、proxy_pass 斜杠
4点开任意二级菜单内容区有数据,不是空白publicPath是否为/
5退出再登录一次会话正常,无重复登录报错Redis 是否可写
6上传一个几兆的附件返回成功,无 413client_max_body_size

命令行侧再补两条确认:

# 后端进程和端口都在 ss -lntp | grep 8080 # 走一遍接口,确认代理链路通 curl -i http://127.0.0.1/prod-api/captchaImage

第二条命令直接打 Nginx 的 80 端口,返回里应该是一个正常的 JSON 验证码结构,而不是 404 页面或 502。返回 502 说明 Nginx 连不上后端,先看 jar 进程;返回 404 说明前缀被带过去了,回到proxy_pass那个斜杠。

从那以后我每次部署若依,不管是 Linux 还是 Windows 方案,都强制走一遍「访问 IP → 深层路由刷新 → 登录 → 点菜单 → 传文件」这五步,一步不过就不往下走。这套流程本身不复杂,复杂的是出问题时现象都长得很像——白屏可能是 publicPath,也可能是 Context,还可能是代理前缀。把验证顺序固定下来,至少能把排查范围一次缩小到一个环节。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表