1. 为什么这个老掉牙的Demo值得反复回看前后端分离这个概念喊了快十年Vue Spring Boot 的组合更是被无数教程写烂了。但有意思的是我最近帮几个朋友排查他们新起的项目问题时发现出问题的往往不是那些花哨的新技术而是最基础的分离架构没搭明白——跨域配置写错位置、前端打包产物路径对不上、接口返回格式前后端各说各话。这些问题在当年那个最小可运行Demo里其实都有标准答案只是很多人跳过了那一步直接上了脚手架生成的全家桶结果底层逻辑一知半解。所以这篇不是教你从零写一个增删改查而是把 Vue Spring Boot 前后端分离 Demo 拆到骨头里讲清楚每一个配置项为什么存在、不这么配会出什么事、以及在实际项目里怎么根据场景做取舍。适合两类人看一是刚接触前后端分离、被各种配置文件绕晕的新手二是用了很久脚手架但没深究过原理、遇到诡异问题只能靠搜索碰运气的开发者。我会用从业者之间聊天的口吻把踩过的坑、验证过的方案、以及那些文档里不会写的经验都摊开讲。核心关键词就几个前后端分离、Vue、Spring Boot、跨域、接口联调、项目结构。整篇内容围绕一个最小但完整的 Demo 展开前端负责页面渲染和交互后端提供 RESTful 接口两边通过 HTTP 通信各自独立开发、独立部署。这个模型虽然简单但它是所有复杂分离架构的根根扎稳了后面加网关、加鉴权、加微服务才不会晃。2. 动手之前先把分离这两个字想透2.1 前后端分离到底分离的是什么很多人以为前后端分离就是前端写 Vue、后端写 Spring Boot这没错但只看到了表面。真正的分离体现在三个层面代码分离、开发分离、部署分离。代码分离意味着前端仓库里没有 Java 代码后端仓库里没有 HTML 模板开发分离意味着前端工程师不需要等后端接口写完就能用 Mock 数据推进部署分离意味着前端打包出来的静态文件可以扔到 Nginx 或者对象存储后端只负责提供 JSON 接口。我见过不少团队嘴上说分离结果后端还在用 Thymeleaf 渲染页面前端只是嵌在模板里的几个 Vue 组件。这种半分离状态最要命既享受不到分离带来的并行开发效率又要承担两套技术栈的维护成本。判断标准很简单后端返回的是 HTML 还是 JSON如果返回 HTML那本质上还是传统 MVCVue 只是个视图层工具而已。2.2 这个 Demo 的技术选型为什么是它俩Vue 和 Spring Boot 能成为经典组合不是偶然。Vue 的渐进式设计让新手可以从一个index.html引入 CDN 开始慢慢过渡到单文件组件和路由管理Spring Boot 的自动配置和起步依赖把传统 Spring 那堆 XML 配置压缩成了几个注解。两者都对初学者友好同时又能支撑生产级应用。具体到版本选择我建议 Demo 阶段用 Vue 3 Spring Boot 3.x但要注意几个关键点Vue 3 的 Composition API 和 Options API 可以混用新手不必一上来就强求script setupSpring Boot 3.x 要求 Java 17 起步如果本地环境还是 Java 8要么升级要么退回 Spring Boot 2.7.x。这个选择直接影响后面依赖能不能拉下来我见过太多人卡在版本不匹配上浪费半天。2.3 最小可运行 Demo 应该包含哪些东西一个合格的 Demo 不需要花哨功能但必须覆盖完整链路。我的标准是前端有一个列表页和一个表单页能调用后端接口获取数据、提交数据后端有两个 RESTful 接口一个 GET 查列表一个 POST 新增数据库用内存级的 H2 或者干脆用 List 模拟重点是跑通流程而不是持久化。这个最小集看起来简单但它逼着你面对所有核心问题跨域怎么配、请求怎么发、参数怎么传、返回值怎么接、异常怎么处理。把这些跑通一遍比看十篇理论文章都管用。下面这张表是我建议的 Demo 功能清单照着做能覆盖 90% 的日常开发场景功能模块前端职责后端职责涉及核心知识点数据列表页面渲染、分页控件查询接口、分页参数GET 请求、JSON 数组解析新增表单表单校验、提交按钮接收参数、返回结果POST 请求、请求体格式删除操作确认弹窗、刷新列表根据 ID 删除路径参数、RESTful 风格错误提示拦截器统一处理统一异常返回HTTP 状态码、错误码设计3. 后端骨架从 Spring Initializr 到第一个 JSON 接口3.1 依赖勾选里藏着的坑用 Spring Initializr 建项目时依赖勾选看着简单但选错了后面要花时间补。必选的是Spring Web它自带 Tomcat 和 Jackson是提供 REST 接口的基础。如果要用数据库加Spring Data JPA和对应驱动Demo 阶段我推荐加H2 Database内存模式启动即用不用装任何东西。这里有个细节Lombok要不要加我建议加但要知道它的代价。Lombok 通过注解处理器在编译期生成 getter/setter代码是干净了但团队里如果有人 IDE 没装插件打开项目就是一片红。Demo 自己玩无所谓团队协作要统一 IDE 配置。另一个容易忽略的是Spring Boot DevTools它能在代码改动后自动重启开发体验提升明显但要注意它和某些热部署方案的冲突。3.2 包结构怎么划分才不乱新手最容易犯的错是把所有类堆在启动类同级目录下十几个文件平铺找起来眼花。我习惯按职责分层controller放接口入口service放业务逻辑entity放数据模型dto放传输对象config放配置类。Demo 阶段可以简化但分层意识要有。// 典型的 Controller 写法 RestController RequestMapping(/api/users) public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService userService; } GetMapping public ResultListUser list() { return Result.success(userService.findAll()); } PostMapping public ResultUser create(RequestBody User user) { return Result.success(userService.save(user)); } }注意这里用了构造器注入而不是Autowired字段注入。原因很简单构造器注入能保证依赖不为 null方便单元测试也避免了循环依赖的隐患。这是 Spring 官方推荐的方式但很多老教程还在用字段注入新手照着抄就带进了坏习惯。3.3 统一返回格式别让前端猜你的数据结构后端接口返回什么格式直接决定前端写起来顺不顺。我见过有的接口成功返回数组失败返回字符串前端得写一堆if判断。正确做法是定义统一的返回包装类public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(int code, String msg) { ResultT r new Result(); r.code code; r.message msg; return r; } }这样前端拿到响应后先看code判断成败再取data或展示message逻辑统一。关键点在于 HTTP 状态码和业务 code 要分开HTTP 200 表示请求到达了服务器业务 code 才表示这次操作成没成功。有些团队图省事直接用 HTTP 状态码表达业务错误结果前端拦截器里要处理一堆 4xx、5xx反而更乱。3.4 跨域配置为什么加了注解还是报错跨域是前后端分离绕不过去的坎。浏览器同源策略规定协议、域名、端口三者任一不同就算跨域。开发阶段前端跑在 5173Vite 默认或 8080后端跑在 8080 或 8081端口不同必然跨域。Spring Boot 里加跨域有两种方式。一种是CrossOrigin注解加在 Controller 上简单但粒度粗每个接口都要加或者加在类上。另一种是全局配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里有个大坑allowedOrigins(*)和allowCredentials(true)不能同时用。如果前端请求带了 cookie 或认证头浏览器会拒绝通配符来源。解决办法是用allowedOriginPatterns代替allowedOrigins这是 Spring 5.3 之后提供的更灵活的方式。我当初在这个问题上卡了很久报错信息又不直观最后翻源码才搞明白。提示跨域配置只在开发阶段需要生产环境前端打包后由 Nginx 统一代理前后端同源根本不存在跨域问题。所以别把跨域配置当成核心功能它只是开发便利手段。4. 前端搭建Vite 起步到接口调通4.1 用 Vite 还是 Vue CLI现在新建 Vue 项目我毫不犹豫推荐 Vite。启动速度差了一个数量级Vue CLI 基于 Webpack冷启动动辄十几秒Vite 用 esbuild 预构建依赖基本秒开。热更新也是 Vite 快得多改一行代码几乎无感刷新。创建命令很简单npm create vitelatest my-frontend -- --template vue cd my-frontend npm install npm run dev但要注意 Vite 的默认端口是 5173不是 8080。很多人照着老教程配代理端口写错了请求发不出去还找不到原因。另外 Vite 的配置文件是vite.config.js和 Vue CLI 的vue.config.js不是一回事代理写法也不同。4.2 开发代理让前端请求看起来同源开发阶段解决跨域除了后端配 CORS更推荐前端配代理。原理是前端请求发给自己的开发服务器开发服务器再转发给后端浏览器看来请求始终是同源的。// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端代码里写fetch(/api/users)就行不用写完整域名。changeOrigin: true的作用是修改请求头里的 Host 字段让后端以为是同源请求。代理方案比 CORS 更干净因为生产环境用 Nginx 也是同样的转发逻辑开发和生产行为一致不会出现开发能跑、上线跨域的尴尬。4.3 封装请求别在每个组件里裸写 fetchDemo 阶段用fetch或axios都行但一定要封装一层。裸写请求的问题在于每个组件都要处理错误、拼 baseURL、加请求头重复代码多改起来到处找。// utils/request.js import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { console.error(业务错误:, res.message) return Promise.reject(new Error(res.message)) } return res.data }, error { console.error(网络错误:, error.message) return Promise.reject(error) } ) export default request拦截器里做了两件事剥掉外层包装让组件直接拿到data统一处理业务错误避免每个调用处都判断code。这样组件里写const users await request.get(/users)就完事了干净利落。4.4 组件里怎么用一个列表页的完整写法以用户列表为例用 Vue 3 的 Composition API 写template div table tr v-foruser in users :keyuser.id td{{ user.name }}/td td{{ user.email }}/td /tr /table /div /template script setup import { ref, onMounted } from vue import request from /utils/request const users ref([]) onMounted(async () { try { users.value await request.get(/users) } catch (e) { // 错误已在拦截器处理这里可做兜底 } }) /script这里ref定义响应式数据onMounted在组件挂载后发请求。注意await拿到的已经是拦截器剥过的data直接赋值即可。如果接口返回的是分页对象users.value res.list这样取。5. 联调阶段最容易翻车的几个地方5.1 请求发出去了但后端收不到参数这是联调最高频的问题。前端POST发 JSON后端用RequestParam接结果参数全是 null。原因是RequestParam从查询字符串或表单里取值而 JSON 在请求体里得用RequestBody接。反过来前端用application/x-www-form-urlencoded发后端用RequestBody接也会失败。判断标准看请求头 Content-Typeapplication/json对应RequestBody表单格式对应RequestParam或直接对象接收。我建议统一用 JSON前后端都省心。5.2 日期格式对不上导致反序列化失败前端传2024-01-15后端实体里是LocalDate默认反序列化可能报错。解决办法是在字段上加JsonFormat(pattern yyyy-MM-dd)或者全局配置 Jackson 的日期格式。全局配置更省事spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8注意时区一定要配否则可能出现差 8 小时的问题。这个坑在跨时区部署时更明显Demo 阶段先养成配时区的习惯。5.3 前端拿到的数据多了一层壳后端返回{code:200, message:success, data:[...]}前端如果直接users.value response.data拿到的是整个包装对象而不是数组。这就是为什么要在拦截器里剥一层。如果没封装拦截器就得写response.data.data两层data看着就晕。统一在拦截器处理组件里只关心业务数据这是纪律。5.4 打包部署后接口 404开发时用代理请求/api/users能通。打包后扔到 Nginx如果 Nginx 没配反向代理/api请求会打到 Nginx 自己的静态文件目录自然 404。Nginx 配置要加location /api/ { proxy_pass http://backend-server:8080/; }注意proxy_pass末尾的斜杠带斜杠表示把/api/替换掉不带则保留。这个细节搞错会导致路径多一层或少一层排查起来很费时间。6. 从 Demo 到真实项目还差哪些东西6.1 鉴权JWT 还是 SessionDemo 阶段接口裸奔没问题真实项目必须加鉴权。前后端分离场景下JWT 比 Session 更合适因为后端不用存会话状态天然支持水平扩展。流程是登录接口验证账号密码签发 token前端存 tokenlocalStorage 或内存每次请求在 header 里带上后端拦截器校验 token 有效性。要注意的是token 过期处理。前端拦截器里判断 401 状态码跳转登录页。别把 token 存 localStorage 就完事XSS 攻击能直接偷走。更安全的做法是存内存 refresh token 机制但复杂度上升Demo 阶段了解概念即可。6.2 环境配置开发、测试、生产怎么切真实项目至少三套环境接口地址、数据库连接都不同。Vue 侧用.env.development和.env.production文件通过import.meta.env.VITE_API_BASE读取。Spring Boot 侧用application-dev.yml和application-prod.yml启动时指定--spring.profiles.activeprod。关键原则敏感信息不进代码仓库。数据库密码、密钥这些用环境变量注入或者用配置中心。Demo 里写死在配置文件无所谓真实项目这么干就是安全事故。6.3 接口文档别再用 Word 传了前后端联调最怕接口定义对不上。推荐用 OpenAPISwagger自动生成文档Spring Boot 加springdoc-openapi依赖注解写清楚参数和返回值前端直接看页面调试。这样接口改了文档自动更新不会出现文档写的和实际返回不一样的情况。Operation(summary 查询用户列表) GetMapping public ResultListUser list() { // ... }6.4 我踩过的三个印象最深的坑第一个是跨域配置加了但 OPTIONS 请求被拦截。浏览器发复杂请求前会先发 OPTIONS 预检如果后端安全框架没放行 OPTIONS预检失败真正的请求根本发不出去。解决办法是在安全配置里放行 OPTIONS 方法。第二个是前端代理配了但没生效。排查半天发现是vite.config.js改完没重启开发服务器Vite 的配置文件改动需要重启才生效热更新不覆盖配置。第三个是后端返回中文乱码。原因是响应头没指定字符集加上produces application/json;charsetUTF-8或者全局配置spring.http.encoding解决。现在 Spring Boot 默认就是 UTF-8但老版本或者特殊配置下仍会遇到。7. 这套 Demo 骨架还能怎么扩展把最小 Demo 跑通之后它就是一个可以不断加东西的底座。想练手可以按这个顺序加先加分页查询后端用 Pageable前端加页码组件再加文件上传体会 multipart 请求和静态资源映射然后加登录鉴权把 JWT 流程走一遍最后加Redis 缓存把热点数据缓存起来观察响应时间变化。每加一个功能都回头看看前后端职责有没有混淆、接口格式有没有保持一致、错误处理有没有遗漏。这套骨架的价值不在于它现在能做什么而在于它提供了一个清晰的边界让你知道什么该放前端、什么该放后端、两边怎么对话。我自己的经验是每次接手新项目先花半小时把它的前后端交互链路画出来比直接读代码快得多。这个 Demo 就是帮你建立这种链路直觉的最短路径。