Spring Boot + Vue前后端分离项目从零搭建实战指南
从零开始搭一个Spring Boot Vue的前后端分离项目这个话题我在面试和带新人的时候聊过无数次。很多人说“我会Spring Boot和Vue”但真到动手时卡点往往不在某个语法上而是整条链路——从环境搭建、项目初始化到接口联调、部署上线——总有几个环节让人一头雾水。这篇内容我想按一个真实项目的推进顺序来写把那些网上教程通常跳过的“为什么”和“坑在哪”补上适合刚学完框架基础、准备做第一个完整项目的人也适合想系统梳理一下前后端协作细节的同学。1. 环境准备与技术选型先让所有工具处于同一频道1.1 版本选择JDK、Node、Spring Boot、Vue 的搭配逻辑我见过太多项目出问题根源都是版本不匹配。Spring Boot 3.x 要求 JDK 17 起步推荐直接上 JDK 21因为 21 是 LTS 版本而且虚拟线程在这些版本里已经非常成熟后续做高并发会轻松很多。Node 方面Vue 3 配合 Vite 构建工具Node 18 以上是底线建议装 20 LTS。新项目别再用 Vue 2 了虽然存量项目很多但新上手直接学 Vue 3 组合式 API 是性价比最高的选择。JDK 安装后一定要确认JAVA_HOME环境变量指向正确命令行执行java -version能显示版本号才算完。Node 安装建议用 nvmWindows 用 nvm-windows做版本管理因为不同项目的 Node 要求可能不一样用 nvm 可以随时切换避免“在我电脑上是好的”这种尴尬局面。1.2 开发工具搭配IDEA 和 VSCode 各司其职后端开发我推荐 IntelliJ IDEA社区版够用。前端我用 VSCode轻量装几个 Vue 插件Volar、ESLint、Prettier体验就很顺手。有些朋友非要用一个工具搞定所有事IDEA 装 Vue 插件也可以但说实话前端调试、扩展生态这些方面 VSCode 还是更舒服。两个工具配合还要注意一个细节前端项目目录和后端项目目录建议分开但放在同一个父目录下比如my-project/backend和my-project/frontend。这样后续做 Git 仓库管理、Docker 部署都清晰。前后端项目放一个仓库没问题但目录必须独立否则后期的构建配置会纠缠在一起特别麻烦。1.3 前后端分离的核心认知一次请求走过的路前后端分离不等于“有两个项目”而是指它们通过 HTTP 接口通信、各自独立部署、独立演进。前端只管页面渲染和用户交互后端只管业务逻辑和数据存取。一次完整的请求链路是用户在浏览器点击按钮Vue 组件里的方法发出请求通过 axios 这类工具请求带上 token 等信息到达后端 ControllerController 不做具体业务调用 Service 层处理逻辑Service 层调用 Mapper 层操作数据库数据原路返回前端拿到数据后更新页面状态。搞清楚这条链路后面所有的配置——跨域、代理、拦截器——都是在为这条链路上的某些环节打通障碍。2. 后端骨架用 Spring Boot 搭出第一个能跑的接口2.1 项目初始化与依赖管理别把时间浪费在手工建目录上创建 Spring Boot 项目最标准的方式是去 start.spring.io 生成基础工程或者直接在 IDEA 里新建 Spring Initializr 项目。我习惯用网页生成因为可以直观勾选依赖。Maven 还是 Gradle新手用 Maven资料多、排查问题方便Gradle 构建更快、脚本更灵活但前期配置理解成本稍高。这两个说到底只是构建工具不决定项目质量别在这里纠结太久。基础依赖选择上Web 必选Validation 建议选上参数校验不是可选项MySQL Driver 选上Lombok 建议选上减少样板代码。MyBatis-Plus 不在这里选因为 start.spring.io 不提供第三方依赖后面需要手动加。这里有个容易踩坑的点Spring Boot 3.x 里 MyBatis-Plus 要使用mybatis-plus-spring-boot3-starter这个新坐标老教程里的mybatis-plus-boot-starter在 3.x 下会有兼容性问题。2.2 目录分层与代码结构为什么一定要分层一个标准的后端项目目录结构大概是这样的Controller 层只接收请求、返回响应Service 层写业务逻辑Mapper 层访问数据库Entity 对应数据库表DTO 是接口传输对象VO 是给前端展示的对象。核心原则是Controller 里不写 SQLService 里不出现 HttpServletRequest。很多初学者觉得分层麻烦一个 Controller 里全搞定。等到项目变大你会发现一个接口被三个页面调用、一个业务逻辑要同时更新两张表时不分层代码根本没法维护。分层的本质是控制依赖方向上层依赖下层下层不感知上层这样改数据库表结构时不会波及到接口层。2.3 从“Hello World”到规范的统一响应新手最容易忽略的部分很多人创建完项目直接写RestController public class UserController { GetMapping(/hello) public String hello() { return Hello World; } }这个能跑但放到真实项目里会有问题。前端拿到响应后怎么知道请求成功还是失败错误信息怎么统一处理所以项目一开始就要定义统一响应体public class ResultT { private Integer code; // 200成功500失败 private String message; // 提示信息 private T data; // 实际数据 // 省略构造器、getter/setter }对应地每个接口都返回ResultT配合全局异常处理器。这样前端只需要判断code就能统一处理成功、参数错误、服务器异常等场景不用每个请求单独写 try-catch。2.4 日志与多环境配置从第一天就按规范来Spring Boot 默认使用 Logback 做日志框架但默认配置只会输出到控制台。项目上线后日志必须落盘、按天滚动、按级别区分。我在resources下放logback-spring.xml配置控制台输出和文件输出文件按日期和大小滚动保留 30 天。配置管理方面一个application.yml显然不够。我习惯拆成application.yml公共配置、application-dev.yml开发环境、application-prod.yml生产环境。启动时用spring.profiles.active指定环境。这样切环境只需要改一个参数不用满文件找数据库地址。这里有一个细节生产环境千万别把数据库密码、密钥硬编码在配置里要使用环境变量占位比如${DB_PASSWORD}。3. 数据访问实战从单表 CRUD 到复杂查询3.1 ORM 选型MyBatis-Plus 还是 Spring Data JPA这是一个老生常谈但确实重要的问题。我的建议很简单国内项目、团队熟悉 SQL、表结构复杂多变选 MyBatis-Plus纯 Java 技术栈、表结构稳定、追求开发效率可以试试 JPA。MyBatis-Plus 本质上是在 MyBatis 基础上做了增强内置了单表 CRUD 方法复杂查询还是写 XML SQL灵活度高。有一个点必须提MyBatis-Plus 的“乐观锁插件”和“逻辑删除”是高频使用的功能但很多人不知道这俩需要自己配置插件和注解。逻辑删除就是在实体字段上加TableLogic删除操作自动变成更新deleted字段乐观锁是更新时自动带上版本号校验防止并发覆盖。这两个不配置功能上也能用但生产环境真的出问题时会很被动。3.2 单表 CRUD 与字段自动填充减少重复劳动MyBatis-Plus 内置的BaseMapperT提供了selectById、insert、updateById等方法。很多业务表都有create_time、update_time这类公共字段如果每个新增和修改方法都手动 set代码冗长且容易遗漏。用TableField(fill FieldFill.INSERT)配合 MetaObjectHandler 实现自动填充新增时自动写创建时间更新时自动更新时间省心。分页查询也是高频操作。MyBatis-Plus 的分页插件要显式配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }不配置这个Page对象只能查出全部数据再内存分页数据量一大就出问题。3.3 XML 与 Mapper 放在同一目录一个解决过很多次的配置项目里接口方法和 SQL 分离会让代码更清晰但“Mapper 接口和 XML 文件如何组织”是个高频问题。我习惯让接口和 XML 同名放在同一个包下比如mapper/UserMapper.java和mapper/UserMapper.xml这样 IDE 里可以直接跳转。要让它生效需要两步配置mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml然后还要在pom.xml里加上build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources /build第二步很关键因为 Maven 默认不会把src/main/java下的非 Java 文件打包进去。漏掉这一步本地能跑、打 jar 包后找不到 XML这个坑我踩过不止一次。3.4 事务和数据库设计的基本功别等服务真的出问题Service 层方法如果涉及多表操作必须加Transactional。不加的话第一张表更新成功、第二张表失败数据就不一致了。需要注意的是事务默认只在遇到 RuntimeException 时回滚如果方法里 catch 了异常并吞掉事务不会回滚。很多线上数据事故就是这么来的。数据库设计上我建议所有表都加上这几个字段id主键、create_time、update_time、deleted逻辑删除标记、version乐观锁版本号。这些是业务表的“标准配置”一开始就加上后面改造表结构时你会感谢自己。4. 前端 Vue 工程化从零搭建一套规范项目4.1 用 Vite 创建 Vue 3 项目与依赖安装Vue 官方现在推荐 Vite 作为构建工具创建项目命令一行就够npm create vitelatest frontend -- --template vueVite 冷启动速度快开发体验确实比 Webpack 时代好太多。创建后进入目录npm install安装依赖。这里又一个坑如果公司网络环境不好npm 安装会非常慢或者直接失败。解决办法是设置镜像源npm config set registry https://registry.npmmirror.com。Vite 创建的项目默认是一个“最小骨架”路由和状态管理都要另外装。vue-router是必须的状态管理我推荐 PiniaVue 官方推荐同时也是vuex的继任者。4.2 目录结构与工程化改造别在 src 里堆所有东西新建项目的 src 目录很简洁但真实项目需要重新规划。我习惯按业务模块拆分src/ api/ # 接口请求定义 assets/ # 静态资源 components/ # 公共组件 router/ # 路由配置 stores/ # Pinia 状态 views/ # 页面组件 utils/ # 工具函数 styles/ # 全局样式这里有一个关键理念api 目录下的每个文件按业务模块封装接口请求函数比如user.js里只放用户相关的请求。页面里不直接写axios.get(/api/user/xxx)而是调用getUserInfo(id)。这样当接口地址变化时只需要改 api 层不需要一个页面一个页面去找。4.3 路由配置与参数传递别再用 query 传一切了Vue Router 4 的配置方式很直观const routes [ { path: /, component: Home }, { path: /user/:id, component: UserDetail }, { path: /login, component: Login } ]路由参数有两种玩法路径参数/user/:id适合详情页组件里用route.params.id取query 参数/search?keywordxxx适合搜索、筛选这类非结构化条件。很多教程在传对象时会用query序列化这会遇到一个奇葩问题刷新页面后参数丢失或乱码。解决方式是用state传参但要注意state是存在内存里的刷新页面后同样会丢所以页面间传参要区分“临时传参”和“需要持久化”的场景。路由守卫也是大项目必配的。比如未登录用户访问需要权限的页面直接跳转登录页。把判断逻辑写在全局前置守卫里比每个页面单独判断靠谱得多。4.4 状态管理与组件通信从 props 到 v-model 到 PiniaVue 组件通信有一套完整方案。父子组件传值用props和emit事件这个是最基础也最能覆盖 80% 场景的方式。跨层传值用provide/inject比如从根组件注入用户信息到任意层级的子组件。全局共享状态用户信息、登录状态、购物车数据用 Pinia。v-model 的本质需要说清楚。它就是一个语法糖等于:modelValue加上update:modelValue。自定义组件里做双向绑定其实就是接收modelValueprop然后通过emit(update:modelValue, newValue)更新值。比如封装一个搜索框组件父组件能直接v-modelkeyword内部逻辑就两件事监听输入、触发事件。4.5 样式方案与 UI 组件库别什么都自己造轮子样式方面scoped是最基本的隔离方式但要注意 scoped 样式里如果想修改子组件内部的样式得用:deep()。.parent :deep(.child-class) { color: red; }CSS 预处理语言建议选 Less 或 SCSS 其中之一。UI 组件库方面桌面端项目 Element Plus 还是主流组件全、文档好、中文社区活跃。移动端推荐 Vant。这里有个技巧组件库按需引入不要在 main.js 里全量导入并 use否则打包体积会大很多。按需引入配合 Vite 插件unplugin-vue-components能自动按需加载。还有一点容易被忽略UI 组件库的样式默认变量和业务主题不一致。Element Plus 支持 CSS 变量覆盖在全局样式里改几个颜色变量就能适配品牌色不用到处覆盖样式。5. 前后端联调打通数据链路的关键一步5.1 Axios 封装统一拦截器与错误处理前端项目中 axios 建议封装成一个统一模块。我通常会做三件事基础配置、请求拦截器、响应拦截器。import axios from axios const request axios.create({ baseURL: /api, timeout: 15000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { // 统一错误提示 return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response?.status 401) { // 跳转登录页 } return Promise.reject(error) } )请求拦截器里自动带 token响应拦截器里统一解包Result结构页面层拿到的就是业务数据不用每个页面都做一次 code 判断。401 场景全局处理避免每个请求都重复写“token 过期跳登录页”的逻辑。5.2 开发代理与跨域问题的本质跨域是前后端分离开发中的头号拦路虎。很多新手一遇到跨域就在后端加CrossOrigin注解或者配置 CORS 过滤器但这只是开发阶段的“止痛药”生产环境多半用不上。开发环境的正确姿势是代理转发。Vite 的vite.config.js里配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }这样浏览器里请求/api/user/listVite 开发服务器会把请求转发到http://localhost:8080/user/list浏览器认为请求是同源的跨域问题直接消失。这比后端开 CORS 好在哪好在生产环境你把构建好的前端放 nginx 下nginx 反代也能用同一套路径规则代码不用改。5.3 接口联调的规范建议从根上减少返工接口联调阶段最容易出乱子的不是技术而是约定。我总结了一个短清单前后端先商定统一响应格式夹带私货迟早出问题字段命名统一用驼峰还是下划线提前说好MyBatis-Plus 默认驼峰映射DTO 字段别乱七八糟接口文档用快速工具生成Swagger 或 knife4j 都行前端照着文档调试接口地址统一带版本前缀比如/api/v1后续接口升级不至于全部推倒联调中还有一件事要提前做Mock 数据。后端接口没写完是常态前端用 Mock 先模拟响应开发进度不受阻塞。Vite 支持 mock 插件或者直接在后端没好的接口前面加个模拟返回。6. 部署上线与常见问题排查从能跑到能上线6.1 后端打包普通 jar 与 GraalVM 原生镜像Spring Boot 项目打包成可执行 jar 在 pom/xml 里配置 spring-boot-maven-plugin 即可mvn clean package -DskipTests生成的 jar 在target/目录下java -jar app.jar就能运行。用spring-boot-maven-plugin打包的 jar 是“可执行 fat jar”内嵌了 Tomcat不需要额外安装容器。有人问过能不能把 Spring Boot 打包插件换成 GraalVM 原生镜像。可以GraalVM 能把应用编译成原生可执行文件启动速度从秒级降到毫秒级内存占用大幅减小。但代价是构建时间长、反射和动态代理需要额外配置这是个大工程很多框架对原生镜像支持并不完善。我的建议是项目规模不大、追求低延迟启动场景可以尝试否则老实跑 JVM 更稳。Spring Boot 3.x 里官方给出了 native 构建方案但别指望零改造直接适应。6.2 前端构建与 Nginx 反代一套经过验证的部署方案前端的部署相对简单npm run build后dist/目录就是你要部署的静态资源。放到 Nginx 下我常用的配置模板server { listen 80; server_name your-domain.com; root /var/www/html; index index.html; # 前端 history 路由刷新 404 的关键配置 location / { try_files $uri $uri/ /index.html; } # 后端接口反代 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 静态资源缓存 location /assets/ { expires 30d; add_header Cache-Control public, no-transform; } }try_files $uri $uri/ /index.html这行是 Vue Router history 模式必须配置的否则刷新非首页路径时会 404。location /api/的代理路径要注意proxy_pass后面的斜杠http://127.0.0.1:8080/会把/api前缀去掉再转发如果你不想去前缀就要写http://127.0.0.1:8080不带斜杠。这个细节错一个字符接口路径就全乱了。6.3 常见报错速查表那些让我熬夜排过的 bug现象排查思路前端请求 404先看代理路径对不对再看后端实际的 RequestMapping 路径前端请求 500看后端控制台异常堆栈常见的 NullPointerException、SQL 语法错跨域报错开发环境走代理生产环境走 nginx 反代别乱加全局 CORS 配置前端刷新页面 404没有配置 try_files或者静态部署的 web server 不支持 history fallbackIDEA 启动项目不显示端口号大概率是启动失败看控制台日志root cause 才是关键打 jar 后找不到 XMLMaven 没有把 XML 打包进 classpath检查 resource 配置依赖版本冲突用 IDEA 分析依赖关系图或者 mvn dependency:tree 查具体版本MyBatis-Plus 逻辑删除不生效检查实体字段有没有加 TableLogic配置有没有被覆盖第四行的问题频率最高不管是新手还是老手只要换一个部署环境就容易遇到。记住history 路由不是免费的它需要服务器端配合做 fallback。7. 从 CRUD 到真实项目进阶方向的速览7.1 文件存储与 MinIO脱离本地目录的第一步项目里上传图片、导入 Excel 这类需求太常见了。把文件存在服务器本地目录虽然简单但问题很多应用重启可能丢文件、多实例部署时文件不共享、磁盘空间不好扩展。靠谱的方案是引入对象存储服务自建就选 MinIO要省事直接用云厂商的 OSS 服务。MinIO 和 Spring Boot 集成的思路很清晰后端提供预签名上传地址前端调用 MinIO SDK 直传文件文件路径存在数据库里。这里要强调一个规范姿势文件中转不要经过后端应用服务器而是直接传到对象存储否则后端带宽会被文件流量占满。流程是前端请求后端拿 uploadUrl前端直传文件到 MinIO再请求后端把这个文件的 URL 关联到业务记录上。7.2 视频流播放与地图组件真实业务里逃不掉的需求视频点播场景用户搜索“vue 播放 m3u8”的频率不低。m3u8 是 HLS 协议的索引文件浏览器原生 video 标签不支持直接播放需要引入 hls.js 或者 video.js 配合 contrib-hls 插件。ts 分发页如果直接渲染 all in one 的播放列表性能一般建议只在页面有播放器时按需加载 hls.js。地图场景腾讯地图或高德在 Vue 里的集成相对直接官方提供了 JS SDK加载后初始化地图实例、添加标记点、绘制覆盖物。一个容易忽略的点地图 SDK 的加载是异步的要在脚本加载完成后的回调里初始化所以首页打开直接渲染地图偶尔报 init is not a function就是这个原因。7.3 工作流与虚拟线程可靠但值得知道的扩展方向项目中如果要做审批流这类功能自研费时费力不稳定集成现成的工作流引擎是更聪明的选择。热词里提到的 deer-flow 是一个国产轻量级工作流引擎核心思路是把流程定义、流程实例、任务节点通过接口暴露出来以 jar 包的方式集成进 Spring Boot 项目。它比 Activiti 那些重引擎轻很多适合中小型项目的审批场景。虚拟线程是 Java 21 里最有感知度的特性之一。Spring Boot 3.2 以后 Tomcat 支持虚拟线程开启方式很简单配置一个参数即可。对于大量 I/O 密集型任务比如调用外部接口、数据库操作虚拟线程能显著降低线程内存开销。但既然是“虚拟”它不解决计算密集型问题需要合理评估场景。从零开始做一个 Spring Boot 和 Vue 的项目最关键的节点其实不在代码量而在于把整条链路的每个环节都打通。我自己带过不少新人能独立把环境、后端、前端、联调、部署跑通的人后面学什么都快。如果你照着这篇内容动手走了一遍也许前几个接口还是有点磕磕绊绊但相信我做第二个项目的时候你会有一种“原来这些坑都长这样”的熟悉感。最后再说一点Vue 和 React 的对比、Vue 源码响应式原理这类话题是面试高频区有空的时候值得花时间看但那是“进阶”的事先把项目做出来再慢慢往深挖。