在现代前后端分离的开发模式中,将 Vue 单页应用(SPA)打包并嵌入 Spring Boot 后端进行统一部署是一种非常常见的架构。然而,这种部署方式经常会遇到 404 Not Found 或 500 Internal Server Error 的坑。
本文将结合实战案例,从路由模式判断 、打包配置 到后端过滤器,全方位剖析这个问题。
🧐 第一步:确认前提------你的 Vue 是 History 模式吗?
在解决问题之前,必须先确认你的前端路由模式。这是导致问题的根源。
1. 代码层面的"铁证"
打开你的 Vue 项目中的路由配置文件(通常是 src/router/index.js 或 src/router/index.ts)。
-
如果是 History 模式(本文讨论的场景) :
代码中会使用createWebHistory()。javaimport { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), // 👈 关键代码:这就是 History 模式 routes }) -
如果是 Hash 模式 :
代码中会使用createWebHashHistory()。// history: createWebHashHistory(), // 这种模式 URL 会带 # 号
2. 浏览器地址栏的直观表现
- History 模式 :URL 干净清爽,没有特殊符号。
- 例如:
http://localhost:8080/user
- 例如:
- Hash 模式 :URL 中带有
#号。- 例如:
http://localhost:8080/#/user
- 例如:
结论 :如果你的代码是
createWebHistory()且 URL 没有#,那么你遇到的 404 错误就是因为服务器无法识别前端路由路径。
⚙️ 第二步:打包配置------base 必须设置为 /
在将 Vue 项目打包放入 Spring Boot 之前,必须确保 vite.config.ts(或 vue.config.js)中的基础路径配置正确。
为什么要设置 base: '/'?
Spring Boot 默认将 src/main/resources/static 目录作为 Web 应用的根路径(/)。为了让打包后的 index.html 能正确引用 assets 目录下的 JS 和 CSS 文件,我们需要告诉 Vite:所有资源都基于根路径查找。
正确配置示例
TypeScript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
base: '/', // 👈 关键配置:设置为根路径
build: {
outDir: '../src/main/resources/static', // 输出到 Spring Boot 的 static 目录
emptyOutDir: true,
}
})
如果 base 设置错误(例如设置为 ./ 或其他路径),即使后端配置正确,页面也会因为加载不到 JS/CSS 而显示空白。
📖 第三步:错误案例剖析(404 与 500)
案例 1:500 错误 - Whitelabel Error Page
很多开发者在遇到 404 时,会尝试使用 WebMvcConfigurer 来解决,结果导致了 500 错误。
错误代码(避坑指南)
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// ❌ 错误做法:会导致循环转发
registry.addViewController("/*")
.setViewName("forward:/index.html");
}
}
原因分析
- 用户访问
/,被转发到/index.html。 - Spring Boot 的静态资源处理器试图处理
/index.html。 - 由于配置冲突,请求在
DispatcherServlet和静态资源处理器之间死循环。 - 最终导致
java.lang.StackOverflowError→ 500 错误。
案例 2:404 错误 - 跳转子页面或刷新时
场景重现
- 首页
localhost:8080/正常显示。 - 点击跳转
/window正常显示(前端路由拦截)。 - 刷新
/window页面 或 直接输入 URL → 404 Not Found。
原因分析
当浏览器请求 /window 时,请求发送到了服务器。Spring Boot 在 static 目录下找不到名为 window 的文件或文件夹,也没有对应的 @Controller 映射,因此返回 404。
✅ 第四步:正确解决方案------SpaForwardFilter
解决 SPA 路由问题的最佳实践是使用 Filter(过滤器)。Filter 的执行时机早于 Spring MVC,可以避免与静态资源处理器的冲突。
完整代码
java
package com.example.liverank.config;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class SpaForwardFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getRequestURI();
// 1. 放行 API 请求
if (path.startsWith("/api/")) {
chain.doFilter(request, response);
return;
}
// 2. 放行静态资源(包含 . 的路径,如 .js, .css, .png)
if (path.contains(".")) {
chain.doFilter(request, response);
return;
}
// 3. 放行根路径
if (path.equals("/")) {
chain.doFilter(request, response);
return;
}
// 4. 核心逻辑:将所有其他请求转发到 index.html
// 让 Vue Router 去处理这些路径
request.getRequestDispatcher("/index.html").forward(request, response);
}
}
代码逐行解析
@Component:将过滤器注册为 Spring Bean,使其生效。path.startsWith("/api/"):如果是后端接口请求,直接放行,交给 Controller 处理。path.contains("."):如果是静态资源请求(如/assets/index.js),直接放行,交给默认的资源处理器。forward("/index.html"):对于/window、/admin等未知路径,服务器内部转发到index.html。浏览器 URL 保持不变,Vue 加载后根据 URL 渲染对应组件。
🔍 第五步:原理深度解析
请求处理链的优先级
使用 Filter 能够解决问题的核心在于它的执行时机:
浏览器请求
↓
┌─────────────────────────────────────┐
│ Filter (SpaForwardFilter) │ <--- 最早拦截
│ ├─ 是 API? → 放行 │
│ ├─ 是资源? → 放行 │
│ └─ 是路由? → 转发到 index.html │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ DispatcherServlet (Spring MVC) │
│ (此时请求已经是 /index.html) │
└─────────────────────────────────────┘
Filter vs WebMvcConfigurer
| 特性 | Filter (推荐) | WebMvcConfigurer (不推荐) |
|---|---|---|
| 执行时机 | Servlet 容器层面(最早) | DispatcherServlet 层面 |
| 静态资源冲突 | 不会冲突 | 会冲突 (导致 500) |
| 适用范围 | 所有请求 | 仅 Spring MVC 管理的请求 |
🗣️ 第六步:面试回答模板
如果面试官问:"Vue 部署到 Spring Boot 遇到 404/500 怎么解决?" 你可以这样回答:
"这个问题通常出现在 Vue 使用 History 模式路由时。
404 的原因 是:刷新页面时,浏览器向服务器请求了
/sub-page这样的路径,但 Spring Boot 找不到对应的 Controller 或静态资源,所以返回 404。500 的原因 是:如果尝试用
WebMvcConfigurer进行转发,容易和 Spring Boot 的静态资源处理器产生循环转发,导致栈溢出。我的解决方案 是使用一个 Filter(过滤器) 。
在 Filter 中,我通过判断请求路径:
- 如果是
/api/开头的接口请求,或者包含.的静态资源请求,直接放行。- 其他所有路径,统一使用
request.getRequestDispatcher("/index.html").forward()转发到首页。这样既避免了冲突,又能让 Vue Router 正确接管路由。另外,在打包时,我也会确保 Vite 的
base配置为'/'。"
📌 第七步:总结与扩展
核心知识点总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 404 Not Found | 服务器不认识前端路由路径 | 使用 Filter 转发所有非 API/非资源请求到 index.html |
| 500 Error | WebMvcConfigurer 导致循环转发 |
改用 Filter,它在更早的阶段拦截请求 |
| 页面空白 | 资源路径引用错误 | 检查 vite.config.ts 中的 base: '/' |
扩展知识:Forward vs Redirect
- Forward (转发) :服务器内部行为,URL 地址栏不变 ,请求次数为 1 次。(SPA 必须用这个)
- Redirect (重定向) :浏览器行为,URL 地址栏改变,请求次数为 2 次。
生产环境最佳实践
在实际生产环境中,通常不使用 Spring Boot 托管静态文件,而是使用 Nginx 反向代理:
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html; # Nginx 的 SPA 配置
}
但掌握 Spring Boot 内部的 Filter 解决方案,对于理解 Java Web 请求生命周期非常有帮助!