首页 / 资讯中心 / 文章详情

Spring Boot + Vue 全栈实战:从工程搭建到部署监控指南

Spring Boot + Vue 全栈实战:从工程搭建到部署监控指南 ★ FEATURED ARTICLE
我是一个做了一年多全栈开发的人去年一整年基本都在跟 Spring Boot Vue 这套组合打交道。老实说刚开始用这套技术栈时走了一堆弯路很多问题不是功能难度大而是前后端之间那层隐形墙——项目结构没理清、接口规范没统一、跨域配置来回折腾。这篇实战指南算是我把这一年多踩过的坑、验证过的方案、沉淀下来的习惯统一梳理一遍从工程搭建到联调部署尽量把那些网上搜不到完整答案的细节补齐。无论你是刚学完 Spring Boot/Vue 基础的学生还是准备把个人项目从单体 JSP 升级成前后端分离的开发者这篇文章的路线应该都能省下你不少排查时间。1. 为什么是 Spring Boot Vue技术选型的现实考量你让我推荐全栈入门技术栈我大概率会首推 Spring Boot Vue不是因为它最新潮而是因为它最稳。1.1 后端生态与市场真实需求的匹配Java 后端在国内的存量市场实在太大了从传统企业信息系统到各类电商、管理系统Spring Boot 几乎是无处不在。它的优势不光是生态成熟更在于上手之后的横向扩展能力强——你今天能用 Spring Boot 写接口明天接 Spring Cloud、Spring Security、消息队列都不需要换语言换框架。对全栈开发者来说后端语言选 Java 的另一个好处是遇到难以排查的问题能搜到的资料量级是其他语言没法比的。Spring Boot 2.3.x、2.6.x 和 3.x 这几个版本我实际用下来的感受是如果公司存量项目多2.6.x 是最稳的选择稳定、资料全、各种 starter 兼容性好如果是 2024 年以后新起的项目直接上 3.x它要求 JDK 17长期来看是趋势。我在项目里用 Spring Boot 3 Vue 3 的组合跑了半年除了个别老依赖需要替换没遇到什么大坑。1.2 前端选 Vue 而不是其他框架的真实体验Vue 最大的特点是渐进式——你可以只在一个页面里引入它当 jQuery 用也可以配合 Vite、Vue Router、Pinia 搭完整工程。这个特性对全栈开发者特别友好因为你前端可能不是天天写隔一段时间再来Vue 那种模板语法 响应式数据的心智模型捡起来特别快。对比 React 需要理解 JSX、Hooks 的渲染逻辑Vue 的 template 更接近传统的 HTML 思维对比 Nuxt 这种重框架Vue 本身做中后台系统完全够用不必额外承担 SSR 的学习成本。我见过好几个后端转全栈的同事Vue 是唯一一个他们看了两周就能写业务页面的前端框架。1.3 什么场景最适合这套组合用了这么久我的判断是Spring Boot Vue 最适合数据驱动型业务系统比如后台管理系统、商品管理、订单处理、内容发布平台。这类系统的特点是后端要处理复杂的业务逻辑和数据库事务前端主要做表单、表格、状态展示对 SEO 没有要求对首屏加载速度的容忍度也比 C 端产品高。如果你要做的是强交互的 C 端产品比如抖音那种信息流或者对性能极致敏感那这套组合不一定最优。但在绝大多数中小型公司里Spring Boot Vue 就是那个不会错的答案招聘市场上需求量大外包接单也好找人协助。2. 从零搭建前后端工程目录结构与初始化细节很多新手全栈的第一个坎不是写代码而是不知道项目文件该往哪放。我在代码审查里最常见的毛病就是所有 Controller 堆在一个包里所有接口都在一个类里前端页面全部写在 App.vue。这套习惯在个人项目里还能忍一旦进入团队协作就是灾难。下面是我目前用下来最顺手的工程组织方式。2.1 后端项目结构与分包思路后端我用 Maven 构建Java 包名根据功能横向划分而不是按技术类型划分。对比一下两种分包方式按技术分包controller / service / mapper / entity每层一个包按功能分包user / order / product每个功能域内部再分层小项目按技术分包没问题项目一旦超过二十个接口按功能分包查代码效率高得多。我现在习惯的包结构是这样com.example.project ├── config # 配置类跨域、拦截器、Swagger ├── controller # 接口层 ├── service # 业务层 │ └── impl ├── mapper # 数据访问层MyBatis-Plus 用 mapper 接口 ├── entity # 数据库实体 ├── dto # 请求参数对象 ├── vo # 返回视图对象 └── common # 统一返回体、异常处理、常量其中 dto 和 vo 很多新手容易忽略我实际写的规范是接口接收参数用 dto返回参数用 voentity 只在 service 和 mapper 之间传递。这样做的核心价值是数据库字段变动时接口的出入参结构不会被牵连前后端联调不会因为一个字段改名就崩。2.2 前端工程的目录规划与组件化意识前端这块新项目我建议直接用 Vite 创建 Vue 3 工程命令是npm create vitelatest project-name -- --template vue。Vue CLI 目前处于维护模式新项目没必要再用它但如果你需要在老项目里维护 Vue 2那 webpack 和 Vue CLI 还是绕不开。Vite 生成的项目里src目录我习惯这样组织src ├── api # 接口请求定义按模块拆文件 ├── assets # 静态资源 ├── components # 通用组件按需放 ├── router # 路由配置 ├── store # Pinia 状态管理 ├── views # 页面级组件 ├── utils # 工具函数、axios 实例 └── App.vue关于组件化我特别想多说一句不是所有东西都要封装成组件。如果你发现一个组件只在某个页面用一次放 views 目录下当局部组件即可不必为了组件化而强行抽到 components。我在项目里看到过不少反面案例封装了十几层的通用表格组件结果每次需求变更都要改组件源码比直接写页面还累。组件的价值和复用次数成正比一个组件至少有三个地方在用才值得抽象。2.3 单仓库还是双仓库一个现实的取舍如果你是自己做个人项目或者团队只有两三个人我建议用一个 Git 仓库里面建backend和frontend两个平级目录。这样提交一个 PR 就能同时看到前后端改动回滚也方便。如果团队规模已经超过五个人前后端分别建仓库更合适因为这时候接口文档就是契约前后端各自独立发版需要能单独回滚。3. 开发环境配置最容易耽误进度的几个坑我在帮初学者看环境问题时发现大部分报错不是代码问题而是环境没配对。下面这几个坑我几乎每个月都会遇到一次。3.1 JDK、Maven、Node 的版本搭配先说结论再解释原因Spring Boot 2.x → JDK 8 或 JDK 11Maven 3.6 都能跑Spring Boot 3.x → 必须 JDK 17Vue 3 Vite → Node 16 以上18/20 都挺稳我遇到的比较头疼的问题是 Maven 依赖下载慢和下载失败。解决办法很简单在~/.m2/settings.xml里配置阿里云镜像mirrors mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror /mirrors前端同理用 npm 的镜像源npm config set registry https://registry.npmmirror.com3.2 IDEA 社区版跑 Spring Boot 的完整方案很多人用 IntelliJ IDEA Community Edition 问得最多的问题就是社区版怎么用 Spring Boot其实社区版完全能跑 Spring Boot 项目区别只是没有 Spring Initializr 这个项目初始化向导。我的做法是两步先在浏览器打开start.spring.io勾选需要的依赖Spring Web、MyBatis、Lombok、MySQL Driver 等生成一个 zip 包然后在 IDEA 里直接 Open 解压后的文件夹Maven 会自动导入依赖。跑 Spring Boot 主类的 main 方法社区版的调试功能完全够用。补充一个增强体验的小技巧社区版的应用商店搜 Spring Boot Helper 插件装完后可以像旗舰版一样一键启动和重启 Spring Boot 应用免费很香。3.3 开发环境的前后端启动顺序我推荐的启动顺序是先启动后端再启动前端。原因是前端开发服务器启动时会检查代理配置如果后端没起登录接口直接报错你分不清是前端问题还是后端问题后端先启动可以用 Postman 或浏览器直接访问 Swagger 接口文档确认服务正常再搞前端。后端启动成功后我一般会在浏览器先访问一下接口文档路径Spring Boot 3 用http://localhost:8080/swagger-ui/index.htmlSpring Boot 2 用http://localhost:8080/swagger-ui.html看到接口列表再去启动前端减少了 90% 的怎么连不上问题。4. 前后端联调的核心接口规范、跨域与 axios 封装前后端分离项目的联调阶段才是真正的主战场。如果你想开发过程中不互相扯皮接口规范必须在开工前定好。这一块我把自己沉淀下来的规定完整写出来。4.1 统一返回体所有接口的输出格式我见过不少团队的口头禅是接口返回格式随便定前端自己处理这句话成了无数混乱的根源。毫不过分地说如果一家公司所有接口都能保证返回格式一致前后端联调的效率至少提升一半。我习惯的统一返回体长这样{ code: 200, message: success, data: {} }code 为 200 表示成功非 200 表示业务失败或异常。前端 axios 拦截器只需要判断 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(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }4.2 接口路径规划资源命名与第三方接口边界RESTful 风格说了很多年我实际落地时的规则很朴素用名词加 HTTP 方法表达动作。比如获取用户列表是GET /api/users创建用户是POST /api/users更新用户是PUT /api/users/{id}。不要出现GET /getUserByType这种动词式路径一个资源一个 Controller路径统一加/api前缀方便后面做代理和权限拦截。热词里有一个很典型的问题Spring Boot 对外提供的接口给第三方应该放在哪里是单独的服务还是放在对应的服务里 我的经验是如果只是两三个接口放一个独立的thirdparty包独立 Controller独立鉴权逻辑如果接口数量多了或者责任边界必须清晰拆成单独服务。原因是给第三方的接口和内部接口在安全要求上完全不同——第三方接口需要签名验签、限流、独立的 QPS 控制跟内部接口混在一起很容易出安全事故。我见过一个项目把第三方回调接口写在用户 Controller 里结果某次接口权限调整直接把回调搞挂了排查了很久。4.3 跨域问题的三种解法从开发到生产跨域是前后端分离绕不开的话题后端和前端各有一种最省事的写法但真正生产环境用的通常是第三种。开发阶段最简单的办法是在后端加CrossOrigin注解或者在配置类实现全局 CORSConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*); } }前端的最省事办法是在 Vite 配置里配 proxy让前端请求/api时自动转发到http://localhost:8080// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })如果你用了 proxy开发环境是不需要后端配置 CORS 的因为浏览器看到的请求是同源的都来自前端开发服务器。但生产环境用 nginx 反向代理的话nginx 就把跨域解决了。CORS 只在前端和后端分别用不同域名/端口直接访问时才真正需要。我自己现在的习惯是前后端各自都做一层代理配置开发期用前端 proxy生产期用 nginx后端基本不再写 CORS。4.4 axios 封装为什么不能每个页面直接请求axios 封装的意义不是写着好看而是解决三个实际问题统一注入 token每次请求都自动把 JWT 加到 Authorization 头不用每个接口手动写统一拦截错误code 不为 200、HTTP 401、网络异常都能自动提示统一处理加载状态可以在拦截器里集中控制进度条或 loading 遮罩我项目里最简单但完整的 axios 封装大致是这样// src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, 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) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(error.response?.data?.message || 网络异常) } return Promise.reject(error) } ) export default request用的时候每个 api 模块就是干净的接口定义// src/api/user.js import request from /utils/request export function getUserList(params) { return request.get(/users, { params }) } export function createUser(data) { return request.post(/users, data) }5. 登录鉴权与动态路由从后端发令牌到前端守卫中后台系统几乎都逃不掉登录和权限控制。这套逻辑前端和后端各占一半我拆开来说。5.1 后端签发与校验 JWT 的完整链路我用的方案是 Spring Boot JWT 拦截器不引入 Spring Security如果是简单的角色权限Security 的配置成本反而偏高等业务复杂到需要方法级权限控制时再引入也不迟。后端主要做三件事。第一件事是登录取用户账号密码查到用户后用 BCrypt 校验密码密码永远不能明文存储注册时BCryptPasswordEncoder.encode()登录时matches()对比。第二件事是签发 JWT我用 jjwt 库Configuration public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire-days}) private int expireDays; public String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expireDays * 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }第三件事是拦截器校验写一个AuthInterceptor实现HandlerInterceptor在preHandle里从 Authorization 头取出 token 并解析解析失败直接返回 401。然后注册拦截器时排除登录接口和 Swagger 路径。5.2 前端登录态管理与路由守卫前端拿到 token 后我习惯存localStorage因为页面刷新后Pinia状态会丢失而 token 需要持久化。请求拦截器会自动带上 token后端校验通过就能访问数据。路由守卫的核心逻辑是没有 token 时任何页面跳转都重定向到 /loginrouter.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else if (!token) { next(/login) } else { next() } })5.3 动态路由按权限加载菜单和页面热搜里提到的vue 动态路由是一个进阶需求。最常见的场景是不同角色的用户登录后看到的菜单不同路由表不能在前端静态写死否则用户直接在地址栏输入 URL 就能越权访问未授权页面。我的实现思路分四步登录成功后调用GET /api/auth/menus获取当前用户的权限编码和菜单列表把后端返回的菜单数据递归生成符合 Vue Router 规范的RouteRecordRaw数组用router.addRoute()逐条动态添加全局守卫里判断如果 store 中没有动态路由则先拉取菜单并添加路由再放行这样处理之后前端渲染的菜单、后端接口的权限、路由的访问控制三方统一权限变更只需要改数据库不用重新发前端包。5.4 按钮级权限和 Vue 插槽的实际使用场景菜单级权限解决后热词里vue 插槽对应的是按钮级权限的常见实现——我给你举一个实际例子。在表格操作列不同角色能看到的操作按钮不同我封了一个AuthButton组件内部用自定义指令判断权限编码来决定是否渲染template el-button v-ifhasPermission(authCode) v-bind$attrs slot / /el-button /template这里的slot /就是插槽的应用调用方往里传入按钮文字AuthButton auth-codeuser:delete删除/AuthButton如果你想让组件更灵活还可以用具名插槽、作用域插槽等。核心思想是插槽是组件提供的一个可插入内容的占位符让父组件决定子组件中间的显示内容。理解到这一层Vue 插槽就算真正入门了。6. 常见业务需求落地文件上传、视频播放与 PDF 展示中后台系统做到一定程度文件相关功能基本躲不开。我把高频的三个需求一起说完。6.1 图片视频文件上传后端接收与前端组件后端接收文件非常简单Spring Boot 的MultipartFile一顿操作PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { // 1. 校验文件大小和类型 // 2. 生成存储路径用 UUID 重命名避免中文名、乱码、覆盖 // 3. 保存到本地磁盘比如 D:/upload/2025/01/xxx.jpg // 4. 返回可访问的 URL }注意三点文件不能直接存数据库除非极小文件保存路径不能是项目内部目录重新部署会丢失返回 URL 要通过 nginx 或静态资源映射暴露出去。前端多文件上传用 Element Plus 的el-upload设置action属性指向http://localhost:8080/api/upload或者自定义http-request用 axios 带上 token 上传。注意el-upload默认收到的返回值格式和后端统一下发格式不一致需要在on-success回调里做一次数据格式转换。6.2 播放 m3u8 视频前端免插件方案vue 播放 m3u8 免安装是热词里的高频问题。m3u8 是 HTTP Live Streaming 的播放列表格式浏览器原生不能直接播放需要引入hls.js。这个库会把 m3u8 解析成浏览器能播放的片段实现如下template video refvideoRef controls autoplay stylewidth: 100%/video /template script setup import Hls from hls.js import { onMounted, ref } from vue const videoRef ref(null) onMounted(() { const video videoRef.value const videoUrl https://example.com/video/index.m3u8 if (Hls.isSupported()) { const hls new Hls() hls.loadSource(videoUrl) hls.attachMedia(video) } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持 HLS video.src videoUrl } }) /script至于 m3u8 文件怎么来一般是后端用 ffmpeg 把视频转码切片出来ffmpeg -i input.mp4 -c:v h264 -c:a aac -f hls -hls_time 10 -hls_list_size 0 output/index.m3u86.3 Vue 里显示 PDF别再照 img 直接怼vue image 能显示 pdf 吗这个热词答案是img标签不能直接显示 PDF浏览器对 PDF 的处理是直接打开新页面或下载而不是渲染进图片框。要在页面内嵌 PDF 预览我有两种方案简单方案用iframe :srcpdfUrl /或embed :srcpdfUrl typeapplication/pdf /浏览器自带 PDF 阅读器就能预览。缺点是不同浏览器样式不一致移动端支持差。进阶方案用pdfjs-distMozilla 出品的 PDF.js 库可以按页渲染到 canvas能自定义翻页、缩放、水印等功能。缺点是代码量多不少而且大文件渲染慢需要做分页加载优化。我个人的建议是非核心场景用 iframe三分钟搞定如果对交互有要求再考虑 pdfjs-dist别一上来就上重方案。7. 打包部署与项目交付Vue 进 Spring Boot 的两种姿势全栈项目做完最后一定绕不开部署。前后端分离项目的部署方式直接决定了你维护成本的高低。7.1 姿势一Vue 打包进 Spring Boot 的 static 目录热词里vue 打包放进 springboot中是这个问题的经典描述。这种姿势适合个人项目、小型 demo、内网工具系统。操作流程在前端项目执行npm run build生成dist目录把dist里的所有文件复制到后端的src/main/resources/static/用 Maven 打包mvn clean package生成一个包含前后端的 jar启动 jar浏览器访问http://localhost:8080就能看到整个全栈应用这个方案的核心坑有两个。第一个是Vue Router 的 history 模式问题你直接访问http://localhost:8080/user/list会 404因为后端没有这个路由。Spring Boot 需要加一个转发规则让所有非 API 路径都回到index.htmlConfiguration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{spring:\\w}) .setViewName(forward:/index.html); registry.addViewController(/{spring:\\w}/**) .setViewName(forward:/index.html); } }或者更省事构建时直接用 hash 模式路由变成/#/user/list刷新就不会 404代价是 URL 难看一点。第二个坑是打包体积和更新效率如果前端经常改每次都要重新打整个 jar传到服务器几百 MB 很浪费时间。所以这个姿势我更倾向于纯演示或一次性交付场景。7.2 姿势二前后端分离部署nginx 做反向代理稍微正式一点的团队项目都应该用这种方案。流程是前端dist部署到 nginx 的静态目录比如/usr/share/nginx/html后端 jar 用java -jar app.jar启动在 8080nginx 配置把/api/开头的请求转发到后端server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; # 前端 history 路由回退 location / { try_files $uri $uri/ /index.html; } # 接口代理 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这种方式的维护优势是前端更新只需要把新的 dist 传上去替换静态文件后nginx -s reload就完事全程不需要停后端。后端接口更新重启 jar 时前端用户无感知。我负责任地说只要你的项目要持续迭代用姿势二。7.3 项目源码交付时的注意事项热词里vue 项目源码怎么发给别人这个看似简单的问题实际交付时经常出乱子。我的标准操作是检查.gitignore是否配置正确node_modules、dist、target绝对不能发提供.env.example文件把VITE_API_BASE_URL这类环境变量写清楚让对方自己复制成.env.development后端数据库脚本单独放在sql/目录并写明 MySQL 版本README 里写清楚 JDK/Node/Maven 版本要求加启动步骤先启动后端再启动前端的说明不能省8. 上线后的监控与排查Spring Boot Admin 与日志体系项目一旦上了生产开发期那套看控制台输出的办法就不够用了。这个问题热词里也提到了spring boot 实现监控都有哪些需求和功能我用自己的落地实践来回答。8.1 Spring Boot Admin十分钟搭一个轻量监控面板Spring Boot Admin 不需要写什么业务代码它是通过 actuator 暴露的端点来收集应用信息然后在前端面板上可视化展示。搭建分两步。第一步新建一个独立的 Spring Boot 工程作为监控服务端引入依赖后主类加EnableAdminServerdependency groupIdde.codecentric/groupId artifactIdspring-boot-admin-starter-server/artifactId version3.2.0/version /dependency第二步被监控的应用引入 client 依赖并在配置里指定服务端地址spring: application: name: business-service boot: admin: client: url: http://localhost:9000启动两个服务后访问监控服务端的 9000 端口就能看到业务服务的状态。我对这套面板最常用的功能有三个一是健康检查看服务是否活着、数据库连接是否正常二是内存和 GC 曲线排查内存溢出和频繁 Full GC三是在线查看日志不用 SSH 上服务器翻文件。8.2 日志规范线上排查的第一只手监控面板只能发现问题定位问题还得靠日志。我的日志规范是从一次线上事故里学来的教训——那次系统半夜报了接口超时因为之前的日志没有记录请求参数和耗时完全无从下手。现在我的后端在每个 Controller 方法上都加了一个简单的切面统一记录请求路径、参数、响应时间Aspect Component Slf4j public class ApiLogAspect { Around(execution(* com.example.project.controller.*.*(..))) public Object logAround(ProceedingJoinPoint pjp) throws Throwable { String method pjp.getSignature().toShortString(); long start System.currentTimeMillis(); Object result pjp.proceed(); long cost System.currentTimeMillis() - start; log.info(接口调用 {}耗时 {}ms, method, cost); return result; } }logback 配置里我习惯按天切分日志文件保留 15 天rollingPolicy设置fileNamePattern为logs/app-%d{yyyy-MM-dd}.logmaxHistory设为 15。这是我踩过很多坑之后沉淀下来的整套 Spring Boot Vue 实战路线技术选型想清楚为什么、工程结构从第一天就立好规范、联调靠统一接口体系和 axios 封装、权限用 JWT 加动态路由闭环、部署用 nginx 分离方式、监控靠 Spring Boot Admin 保底。每次带新人或者自己接手新项目我都会按这条路径先理顺环境再动手写业务。最后提一个很多人忽视的建议前后端接口文档一定要跟着代码同步更新我现在是把 Swagger/Knife4j 集成到 CI 流程里自动发布的接口变动自动生成新文档人工维护文档这件事本身就不靠谱。
阅读完成 · 觉得有帮助?
咨询建站