
1. 为什么还要回头写一个前后端分离的Demo前后端分离这个词现在听起来已经不算新鲜了但真正动手从零搭一套 Vue Spring Boot 的完整项目很多人还是会卡在跨域、接口联调、打包部署这几个环节上。我这次回头重写这个 Demo不是为了追什么新技术而是想把这套最经典的组合彻底捋一遍把当年踩过的坑、绕过的弯路都整理出来给刚接触前后端分离的朋友一个能直接抄的参考。这个 Demo 的核心目标很明确前端用 Vue 负责页面渲染和交互后端用 Spring Boot 提供 RESTful 接口两边通过 HTTP 请求通信各自独立开发、独立部署。它解决的是传统 JSP 或 Thymeleaf 那种前后端代码混在一起、改一个按钮都要重新编译整个项目的问题。适合谁看如果你已经会一点 Java 和 JavaScript但还没完整搭过一个前后端分离项目或者你搭过但总是被跨域和接口格式搞得头大那这篇内容应该能帮到你。我这次用的技术栈是 Vue 3 Vite Axios 做前端Spring Boot 3 MyBatis-Plus MySQL 做后端整体结构不复杂但每个环节我都会把“为什么这么选”讲清楚。比如为什么用 Vite 不用 Webpack为什么接口返回要统一封装为什么跨域不能随便用CrossOrigin糊弄过去。这些细节才是真正决定项目能不能顺利跑起来的关键。2. 整体架构设计与技术选型思路2.1 前后端分离到底分的是什么很多人以为前后端分离就是前端写页面、后端写接口其实这只是表面。真正的分离体现在三个层面代码分离、开发分离、部署分离。代码分离意味着前端项目和后端项目是两个独立的代码仓库前端不依赖后端模板引擎后端也不关心页面长什么样。开发分离是指前端工程师和后端工程师可以并行开发只要提前约定好接口文档前端可以用 Mock 数据先跑起来。部署分离则是前端打包成静态文件放到 Nginx 或 CDN后端打成 Jar 包跑在服务器上两边互不干扰。我见过不少项目号称前后端分离结果前端代码里还嵌着后端返回的 HTML 片段或者后端接口直接返回页面路径这种就是典型的“假分离”。真正的分离要求后端只返回数据通常是 JSON 格式前端拿到数据后自己决定怎么渲染。这样做的好处是前端可以灵活替换技术栈今天用 Vue明天换 React后端完全不用动。后端也可以同时服务 Web 端、App 端、小程序端一套接口多处复用。2.2 为什么选 Vue 3 Vite 而不是其他组合Vue 3 的 Composition API 是我这次选它的主要原因。Options API 写多了之后逻辑分散在 data、methods、computed 里一个功能要改好几个地方。Composition API 可以把相关逻辑写在一起复用起来也方便。Vite 的冷启动速度比 Webpack 快很多改代码后热更新几乎是秒级开发体验提升明显。Axios 就不用说了拦截器机制用来统一处理 token 和错误提示非常顺手。后端选 Spring Boot 3 是因为它对 Java 17 支持更好而且 Spring Boot 的自动配置让整合 MyBatis-Plus、MySQL 驱动这些变得很简单。MyBatis-Plus 相比原生 MyBatis 省去了大量 XML 配置简单的增删改查直接用BaseMapper就能搞定复杂查询再写自定义 SQL。MySQL 选 8.0 版本主要是为了支持 JSON 字段类型和更好的性能。2.3 项目目录结构怎么划分才合理前端项目我习惯按功能模块划分目录而不是按文件类型。比如src/views下面放页面组件src/components放公共组件src/api放接口请求src/utils放工具函数。这样找文件的时候直接按业务模块找不用在components里翻半天。后端项目按经典的三层架构分controller层负责接收请求和返回响应service层处理业务逻辑mapper层操作数据库。实体类放在entity包数据传输对象放在dto包视图对象放在vo包。这里有个经验不要把entity直接返回给前端。数据库实体可能包含密码、删除标记这些敏感字段直接返回会有安全隐患。我一般用vo类专门做返回数据的封装只暴露前端需要的字段。比如用户表里有password字段但返回给前端的UserVO里只有id、username、nickname这样既安全又清晰。3. 后端核心细节与实操要点3.1 Spring Boot 项目初始化与依赖配置用 Spring Initializr 生成项目骨架的时候我勾选了 Spring Web、MyBatis Framework、MySQL Driver、Lombok 这几个依赖。Lombok 一定要加不然实体类的 getter、setter 写起来太啰嗦。pom.xml里我额外加了 MyBatis-Plus 的 starter版本选 3.5.5 以上对 Spring Boot 3 的支持更稳定。dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency配置文件application.yml里主要配三块数据源、MyBatis-Plus、服务器端口。数据源用 HikariCPSpring Boot 默认就是它性能比 Druid 好一些。MyBatis-Plus 要配mapper-locations指向 XML 文件位置还要开map-underscore-to-camel-case这样数据库的user_name字段能自动映射到 Java 的userName属性。spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl server: port: 8080注意serverTimezone一定要配不然 MySQL 8.0 连接时会报时区错误。log-impl配成 StdOutImpl 可以在控制台看到执行的 SQL调试的时候非常有用但上线前记得关掉不然日志会刷屏。3.2 统一接口返回格式的设计后端接口返回的数据格式必须统一不然前端处理起来会很乱。我定义了一个Result类包含code、message、data三个字段。code用 200 表示成功其他值表示各种错误。message放提示信息data放实际数据。这样前端只需要判断code是不是 200就能知道请求成功还是失败。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }这里有个细节data字段用了泛型这样返回不同类型的数据时不用强转。比如ResultUserVO和ResultListUserVO都能用同一个类。另外我建议加一个全局异常处理器用RestControllerAdvice捕获所有异常统一返回Result.error()这样前端永远不用处理 500 页面所有错误都是 JSON 格式。3.3 跨域问题的正确处理方式跨域是前后端分离绕不开的坎。浏览器同源策略规定协议、域名、端口三者只要有一个不同就算跨域。前端跑在localhost:5173后端跑在localhost:8080端口不同所以跨域。很多人图省事直接在 Controller 上加CrossOrigin但这样每个接口都要加而且生产环境可能还有 Nginx 代理配置会乱。我推荐用全局配置类来处理跨域在config包下建一个CorsConfig类实现WebMvcConfigurer接口重写addCorsMappings方法。这样所有接口自动支持跨域不用一个个加注解。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和allowedOrigins的区别在于前者支持通配符后者在allowCredentials为 true 时不能用*。所以如果你要传 Cookie 或 Token必须用allowedOriginPatterns。maxAge设置预检请求的缓存时间3600 秒意味着一天内同一个跨域请求不用重复发 OPTIONS 预检能减少请求次数。4. 前端核心细节与实操要点4.1 Vue 3 项目搭建与 Axios 封装用npm create vitelatest创建项目选 Vue 模板然后装 Axios 和 Vue Router。Axios 我习惯封装一个request.js在里面创建实例、配置基础路径、设置请求拦截器和响应拦截器。请求拦截器用来统一加 Token响应拦截器用来统一处理错误码。import axios from axios const request axios.create({ baseURL: http://localhost:8080, timeout: 10000 }) request.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error Promise.reject(error) ) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { alert(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { alert(网络异常请稍后重试) return Promise.reject(error) } ) export default request这样封装之后页面里调用接口只需要request.get(/user/list)返回的已经是data字段的内容不用每次写res.data.data。错误提示也统一在拦截器里处理了页面代码干净很多。4.2 接口请求的模块化管理不要把接口请求散落在各个组件里我习惯在src/api目录下按模块建文件。比如user.js放用户相关的接口article.js放文章相关的接口。每个文件里导出具体的请求函数组件里直接 import 调用。// src/api/user.js import request from /utils/request export function getUserList(params) { return request.get(/user/list, { params }) } export function addUser(data) { return request.post(/user/add, data) } export function updateUser(data) { return request.put(/user/update, data) } export function deleteUser(id) { return request.delete(/user/delete/${id}) }这样做的好处是接口路径集中管理改路径的时候只改一个地方。而且函数名有语义getUserList比直接写request.get(/user/list)更容易理解。如果后端接口有变动前端只需要改api目录下的文件组件代码不用动。4.3 页面路由与权限控制Vue Router 用createRouter创建实例路由模式选createWebHistory这样 URL 里没有#号看起来更干净。路由配置里可以加meta字段标记哪些页面需要登录然后在全局前置守卫里判断。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })注意前端路由守卫只是提升用户体验不能作为安全屏障。真正的权限校验必须放在后端每个接口都要验证 Token 是否有效、用户是否有权限访问该资源。前端守卫可以被绕过后端校验才是最后一道防线。5. 前后端联调与常见问题排查5.1 接口联调的标准流程联调的时候我习惯先用 Postman 或 Apifox 测后端接口确保单独跑没问题。然后再到前端页面里调如果前端报错先看浏览器控制台的 Network 面板确认请求 URL、请求方法、请求头、请求体是否正确。常见的问题有请求方法写错了比如后端要 POST前端发了 GET、参数格式不对后端要 JSON前端发了 form-data、请求头缺少 Content-Type。我一般会在后端 Controller 里加日志打印接收到的参数这样能快速判断是前端没传对还是后端没接住。前端也可以在 Axios 拦截器里打印请求配置两边对照着看问题很快就能定位。5.2 常见问题速查表问题现象可能原因排查方法解决方案浏览器报 CORS 错误后端未配置跨域看控制台是否有Access-Control-Allow-Origin相关提示添加全局跨域配置类请求返回 404接口路径不对Network 面板看请求 URL检查baseURL和接口路径拼接请求返回 401Token 无效或未传看请求头是否有 Authorization检查登录逻辑和拦截器请求返回 500后端代码异常看后端控制台堆栈信息根据异常信息修复代码数据返回但页面不渲染字段名不匹配对比接口返回字段和页面绑定字段统一字段命名或做映射跨域预检失败OPTIONS 请求被拦截Network 面板看是否有 OPTIONS 请求后端放行 OPTIONS 方法5.3 我踩过的几个坑第一个坑是RequestBody和RequestParam混用。前端发 JSON 数据时后端必须用RequestBody接收而且只能有一个。如果同时想接收路径参数和 JSON 体路径参数用PathVariableJSON 体用RequestBody两者不冲突。我当初把RequestBody和RequestParam写在一个方法里结果一直报 400 错误排查了半天才发现是注解用错了。第二个坑是日期格式。Java 的LocalDateTime默认序列化成数组或时间戳前端拿到后没法直接显示。解决办法是在字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)或者在配置文件里全局配置日期格式。我建议用全局配置省得每个字段都加注解。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第三个坑是打包部署后接口 404。开发环境前端跑在 5173 端口后端跑在 8080 端口跨域配置生效。但打包后前端静态文件放到 Nginx后端还是 8080这时候如果前端请求的baseURL还是localhost:8080用户浏览器访问的是 Nginx 的地址请求就会发到用户自己的电脑上自然 404。解决办法是在 Nginx 里配置反向代理把/api开头的请求转发到后端服务。location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这样前端baseURL改成/api请求先到 Nginx再由 Nginx 转发给后端就不存在跨域问题了。这也是生产环境推荐的做法比后端配跨域更安全、更灵活。6. 项目打包与部署的实操记录6.1 前端打包与静态资源优化前端打包用npm run buildVite 会把代码压缩、分割、生成带哈希的文件名。打包产物在dist目录里面是index.html和assets文件夹。我一般会把dist里的文件传到 Nginx 的html目录然后配置 Nginx 的root指向这个目录。Vite 默认的打包配置已经够用但有几个地方可以优化。比如build.rollupOptions.output.manualChunks可以把第三方库单独打包避免业务代码更新后用户还要重新下载 Vue、Axios 这些不变的库。还有build.chunkSizeWarningLimit可以调大避免打包时提示文件过大。// vite.config.js export default defineConfig({ build: { chunkSizeWarningLimit: 1500, rollupOptions: { output: { manualChunks: { vendor: [vue, vue-router, axios] } } } } })6.2 后端打包与运行后端用 Maven 打包mvn clean package生成可执行的 Jar 文件。运行的时候用java -jar xxx.jar可以加--spring.profiles.activeprod指定生产环境配置。生产环境的配置文件里数据库密码、日志级别这些要和开发环境区分开。我习惯把配置文件分成application-dev.yml和application-prod.yml主配置文件里用spring.profiles.active指定当前环境。这样本地开发用 dev服务器部署用 prod互不干扰。生产环境的日志级别调成 WARN避免 INFO 日志太多占满磁盘。6.3 部署后的验证清单部署完成后我一般按这个清单逐项验证前端页面能否正常打开、静态资源是否加载成功看 Network 面板有没有 404、登录接口是否正常返回 Token、带 Token 的请求是否通过鉴权、数据库增删改查是否生效、Nginx 反向代理是否配置正确。每一项都确认没问题才算部署完成。提示部署后如果页面白屏先看浏览器控制台有没有报错。常见原因是静态资源路径不对比如 Vite 默认的base是/如果部署在子目录下需要改成./或具体子路径。还有可能是 Nginx 的try_files没配刷新页面后 404需要加try_files $uri $uri/ /index.html。7. 这个 Demo 还能怎么扩展这套基础框架跑通之后可以往上加的东西很多。比如加 JWT 做无状态鉴权把 Token 存到 Redis 里做黑名单管理加 Spring Security 做细粒度权限控制区分管理员和普通用户加 Swagger 或 Knife4j 自动生成接口文档前后端对接更高效加 Redis 缓存热点数据减轻数据库压力加消息队列处理异步任务比如发送邮件、生成报表。前端方面可以加 Pinia 做状态管理把用户信息、权限列表这些全局状态统一管理加 Element Plus 或 Ant Design Vue 做 UI 组件库页面开发速度能快不少加 ESLint 和 Prettier 做代码规范团队协作时风格统一。我个人在实际操作中的体会是不要一上来就把所有东西都堆进去。先把最基础的增删改查跑通把跨域、接口封装、异常处理这些基础设施搭好后面加功能就是水到渠成的事。我见过太多项目一开始就上微服务、上消息队列结果基础功能还没跑通光环境问题就耗了一两周。先把单体能跑通再考虑扩展这个顺序不能乱。最后再分享一个小技巧接口文档一定要在开发前就定好用 Apifox 或 Swagger 写清楚每个接口的路径、方法、参数、返回格式。前后端并行开发的时候前端用 Mock 数据先跑页面后端按文档实现接口联调的时候基本一次过。我试过几次没写文档直接口头沟通结果字段名对不上、参数类型不对改来改去浪费的时间比写文档多得多。