ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

前后端分离架构实战:Spring Boot + Vue + Jenkins自动化部署全流程

前后端分离架构实战:Spring Boot + Vue + Jenkins自动化部署全流程 前后端分离这个词估计很多开发都已经听到耳朵起茧了。特别是这两年随便搜一下前后端分离项目实战跳出来全是 Spring Boot Vue 的组合好像不这么写都不好意思说自己是做 Web 的。但真正从零走完一遍设计、开发、联调、部署的人其实并没有想象中那么多。我最近刚好把一个老项目从模板渲染逐步改造成前后端分离模式又折腾了 Windows 服务器上的 Jenkins 自动部署整个过程下来踩了不少坑也沉淀了不少能直接用的经验。这篇就把我对前后端分离模式的理解以及一套可复制的落地流程整理出来给正准备从单体架构切换到分离架构的团队或同学作个参考。1. 前后端分离模式到底是什么1.1 从我踩过的单体应用坑说起我早期做项目的时候用的是 JSP Servlet 或者 Spring Boot Thymeleaf 这种服务端渲染方式。所有页面和后端代码放在同一个工程里前端的东西被拆成一个个模板文件嵌在 WEB-INF 下面。当时最大的痛点不是页面渲染慢而是改一个样式或者调一个点击事件前端同学需要把整个后端工程拉下来本地起一个完整的服务器环境改完还得重新编译、重启。到了前后端联调阶段后端一改接口前端就得跟着重构页面一个 bug 能来回确认半天。后来逐步切到前后端分离模式才真正体会到什么叫“各司其职”。后端只负责写 API返回 JSON前端专心做页面通过 HTTP 请求拿数据。两边通过一份接口约定协作开发期独立启动互不阻塞。移动端以后要复用接口也非常方便不需要为 App 单独再写一套后台。这个模式在架构设计里本质上是“表现层与逻辑层分离”的思路和代码层面的 23 种设计模式不是一回事但思想是一脉相承的——通过解耦来降低复杂度和维护成本。1.2 分离模式适合什么场景不适合什么场景前后端分离适合什么场景首先要明确它不是万能药。如果是一个对 SEO 要求极高的新闻门户、内容站点服务端渲染依然有不可替代的优势因为搜索引擎爬虫对 JavaScript 渲染页面没那么友好。如果团队连一个像样的前端工程师都没有强行分离反而会增加沟通成本。但如果你做的是中后台系统、管理平台、SaaS 应用或者将来要同时输出 Web、小程序、App 多种客户端前后端分离几乎就是必然选择。我判断一个项目是否值得分离主要看三点第一前端交互复杂度是否已经高到需要独立的工程化构建流程第二后端是否要支撑多个不同的客户端第三团队是否能明确划分职责至少前端和后端各有人能独立交付。只要满足其中两点就可以下手做分离。真正决定成败的不是技术而是接口约定和团队协作机制这一点在后面我会反复提到。2. 整体设计与思路拆解2.1 技术选型Spring Boot Vue为什么不是别的前后端分离的技术选型网上最常见的答案是 Spring Boot Vue。这个组合不是唯一解但胜在生态成熟、资料多、上手快。Spring Boot 内嵌 Tomcat天然适合提供 RESTful APIVue 的组件化开发非常直观配合 Element Plus 这种组件库搭后台页面效率很高。Node 生态那套构建工具链也能很好地把资源打包成静态文件交给任意 Web 服务器托管。还有人会直接用若依框架RuoYi这类前后端分离脚手架你可以把它看成是一个已经帮你做好了登录、权限、菜单管理、代码生成器的“全家桶”非常适合快速落地企业后台。如果你只是想搞一个学习和验证用的项目不用上若依如果目标是交付一个真正的管理系统用若依能省至少两周的重复劳动。我这次选择手写一套精简版主要目的就是不想被脚手架盖住底层逻辑。下面是技术栈表方便直接抄层次选型说明后端框架Spring Boot 2.7快速构建 REST API内嵌 Tomcat持久层MyBatis-Plus单表 CRUD 不用手写 SQL数据库MySQL 8主流、稳定适合业务系统前端框架Vue 3 Vite组合式 API开发灵活UI 组件库Element Plus中后台组件全省去造轮子认证方案JWT无状态 Token适合分离部署接口文档Springdoc OpenAPI自动生成 Swagger 文档部署方案Nginx Jenkins静态资源与 API 分离自动化发布2.2 接口规范统一返回体和状态码如果接口没有统一格式前端每接一个接口都要去看它的返回结构开发效率会急剧下降。所以我们约定了一套统一返回体code 表示业务状态码message 是给前端展示的提示信息data 是真正的业务数据。成功就是 200未登录就是 401没权限就是 403参数错误 400服务器内部错误 500。HTTP 状态码和业务 code 要分工明确不能混用。比如接口接收到了请求但业务校验失败HTTP 状态可能还是 200业务 code 返回 50001这样网关或 Nginx 不会误判为网络故障。前端在 Axios 拦截器里先看 HTTP 状态码再解析 body 里的业务 code两步分流处理。这个约定在前后端分离模式里是地基越早定下来越好。另外分页数据、时间格式也要提前统一比如分页统一定义为 page/size/total/records日期全部用时间戳或统一字符串避免每个接口各写各的。2.3 跨域与代理开发期和生产期不要同一种玩法前后端分离后前端运行在 8080 端口后端运行在 8081 端口浏览器就会触发跨域。解决跨域一般有两种思路一是后端开启 CORS允许指定来源访问二是通过代理服务器转发让浏览器认为请求是同源的。开发期我倾向于用前端构建工具的 proxy 做代理比如 Vite 里配置 server.proxy把 /api 开头的请求转发到后端地址这样浏览器请求的是 localhost:5173没有跨域问题也不需要后端额外放开 CORS。生产环境则更建议用 Nginx 做反向代理把前端静态资源和后端 API 统一挂在一个域下。这既能解决跨域又能隐藏后端真实端口还方便以后做负载均衡。一个小坑是如果后端接口要保持路径前缀比如 /apiNginx 的 location /api/ 转发时要处理好路径重写否则后端可能收到带前缀的路径导致 404。这个我后面部署部分会给一个完整配置。2.4 鉴权设计JWT 怎么融进分离架构以前用 Session 保持登录状态因为前后端同域Cookie 天然可用。改造成前后端分离后前端是一个独立部署的静态站后端是另一个服务Session 共享成了问题。这时候最常用的方案是 JWT流程是用户登录后后端验证账号密码签发一个 JWT 返回给前端前端把它存在 localStorage 或 Pinia 里每次请求在 Authorization 头里带上后端通过过滤器拦截检查签名和有效期从 token 里解析用户信息。JWT 的优点是无状态后端不用像 Session 那样存在内存或 Redis 里方便水平扩展。缺点也很明显token 一旦签发在到期之前很难主动作废。所以设计时要注意两点一是设置合理的过期时间比如 2 小时二是提供刷新机制前端检测到 401 后带着 refreshToken 去换新的 accessToken。还有安全细节前端不要把 token 放到 URL 参数里避免出现在日志中localStorage 有 XSS 风险如果项目非常注重安全可以放到 HttpOnly Cookie 里配合 CSRF 防护。这些都是我在实际项目里被人问过很多次的点。3. 核心细节解析与实操要点3.1 后端 API 开发的三个关键习惯第一个习惯是坚持 RESTful 风格。资源用名词复数比如 /users动作用 HTTP 方法GET 查询、POST 创建、PUT 更新、DELETE 删除。不要出现 /getUserInfo、/delUser 这种动词式接口。第二个习惯是参数校验和统一异常。Controller 里不要写一大坨 if 判断用 Valid 注解在 DTO 上做校验业务异常通过全局异常处理器返回统一结构。第三个习惯是接口文档同步更新。如果能用 Springdoc / Swagger 生成 OpenAPI 文档就尽量用前端可以拿着在线文档联调不用天天来问字段含义。一个简单的登录接口大概长这样Controller 接收 LoginRequest 对象密码校验通过后生成 token返回 LoginResponse。代码里我会把用户校验的逻辑放到 Service 层Controller 只做参数封装和结果返回。这样后期如果要换成 OAuth2 或接入其他认证方式Service 层不用大改。实际动手的时候接口命名和包结构尽量保持稳定否则联调阶段改来改去会消耗掉前后端分离省下来的时间。RestController RequestMapping(/auth) public class AuthController { PostMapping(/login) public ResultLoginResponse login(RequestBody Valid LoginRequest request) { LoginResponse response authService.login(request.getUsername(), request.getPassword()); return Result.success(response); } GetMapping(/info) public ResultUserInfo getUserInfo(RequestHeader(Authorization) String token) { UserInfo userInfo authService.getUserInfo(token.replace(Bearer , )); return Result.success(userInfo); } }3.2 前端工程化结构与请求封装前端如果只有一个 views 目录组件到处乱放过几个月没人能维护。我常用的 Vue 项目结构是api 目录按业务模块放接口定义assets 放静态资源components 放公共组件views 放页面组件router 统一配置路由store 放全局状态。这样分层非常清晰新增一个功能模块的时候路由、接口、页面、状态都有固定的落点。Axios 请求封装是前端必须做的一步。网上很多项目直接在每个页面里调用 axios.get看起来简单但一旦要改请求头或者统一处理错误提示就得全局搜索改几十个地方。正确做法是创建 request.js实例化一个 axios 实例设置 baseURL 和超时时间请求拦截器里从 store 或 localStorage 取 token 放到 header响应拦截器里先判断 HTTP 状态再解构 bodycode 不是 200 就用 Element Plus 的 Message 弹出错误。这样页面代码里只需要写业务逻辑不用在每个页面重复处理异常。// request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) 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 }, error { if (error.response?.status 401) { localStorage.removeItem(token) window.location.href /login } else { ElMessage.error(error.response?.data?.message || 网络异常) } return Promise.reject(error) } ) export default request3.3 前后端联调Mock、Swagger 和若依框架的启示前后端并行开发是分离模式的主要收益但如果后端接口还没写好前端也不能干等。这时候就需要 Mock 和接口文档工具。先约定好接口返回结构前端用 Mock.js 或者直接在后端用 Mock 数据跑起来保证页面能开发后端接口好了以后把前端代理切到真实后端地址就行。Swagger 在这里特别有用后端启动后访问 /swagger-ui.html 就能看到每个接口的请求参数和返回值前端可以照着文档造数据。如果你用的是若依框架它的代码生成器能根据数据库表直接生成前后端 CRUD 代码接口文档也一并生成对这种“样式统一”的中后台项目来说效率非常可观。但也要注意脚手架生成的代码只是起点业务复杂到一定程度后还是要把核心逻辑从生成代码里抽离出来否则后面每个功能都依赖框架。3.4 设计模式在业务层如何落地策略模式的组合玩法前后端分离解决的是架构层面问题但代码内部照样会遇到扩展和维护的难题。比如订单模块里有多种折扣策略如果用 if/else 判断订单类型每次新增一种都要改核心方法测试也麻烦。我在这次项目中用了策略模式加工厂模式来处理把每种折扣算法写成独立的 Service 类实现同一个接口再通过 Spring 的依赖注入把它们放到一个 Map 里key 是折扣类型value 是策略实现。调用时直接从 Map 取后续新增策略只需要加一个类其他代码都不用动。如果你对 23 种设计模式不熟也不需要死记口诀。记住一个原则看到多个条件分支且分支行为未来会频繁变化就考虑策略看到对象创建过程复杂且要隔离变化就考虑工厂看到要动态扩展对象行为就考虑装饰器。设计模式不是炫技是为了让代码在需求变化时改得少、改得稳。项目里最值得一用的几个模式除了策略还有模板方法、观察者、责任链这些在后端业务开发中出现频率很高。4. 从零搭建一个可运行的前后端分离项目4.1 环境准备这个部分要给具体步骤。首先要装 JDK 11 或 17、Maven 3.6、Node.js 16 以上、MySQL 8。前后端分离项目建议用数据库脚本初始化表不要靠 Hibernate 自动建表生产环境容易出幺蛾子。准备好 IDEA 和 VSCode一个写后端一个写前端。这块没什么高深内容但版本匹配容易踩坑比如 JDK 版本和 Spring Boot 版本不兼容Node 版本太高导致旧的依赖安装失败。我一般会把版本号写进 README团队里统一环境否则每个人报的错都不一样。数据库表可以先建一张简单的用户表字段包含 id、username、password、role、create_time。密码字段长度至少 60因为 BCrypt 加密后字符串比较长。初始化脚本里插入一个测试用户密码用 BCrypt 在线工具生成方便后面测试登录。4.2 后端登录接口和用户信息接口后端用 Spring Initializr 创建工程依赖选择 Spring Web、MyBatis-Plus、MySQL Driver、Spring Security 或 JWT 库。如果不需要 Spring Security 那么重可以直接用一个拦截器实现 token 校验。这里我展示一个不带密码加密简化的登录 Service通过 userId 去数据库查用户用 BCrypt 校验密码成功后生成 JWT 返回。注意密码永远不要明文存数据库至少要 BCrypt 加密项目里可以写一个密码工具统一处理。Service public class AuthService { Autowired private UserMapper userMapper; public LoginResponse login(String username, String password) { User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getUsername, username)); if (user null || !BCrypt.checkpw(password, user.getPassword())) { throw new BusinessException(40001, 用户名或密码错误); } String token JwtUtil.createToken(user.getId(), user.getUsername()); return new LoginResponse(token, user.getUsername()); } public UserInfo getUserInfo(String token) { Long userId JwtUtil.parseUserId(token); User user userMapper.selectById(userId); return new UserInfo(user.getId(), user.getUsername(), user.getRole()); } }之后还要写用户信息接口 UserController用 RequestHeader(Authorization) 拿 token解析用户 ID返回用户名和角色。这个接口是后续前端判断权限的入口。全局异常处理器也很重要我写了一个 RestControllerAdvice捕获 BusinessException 和参数校验异常返回统一格式的 Result 对象前端就不用担心后端偶发抛出的默认错误页。4.3 前端登录页和请求拦截前端用 Vite 创建 Vue3 项目命令是 npm create vitelatest选择 Vue TypeScript。然后安装 vue-router、pinia、axios 和 element-plus。我需要提供登录页的代码调用 request.post(/auth/login, form)成功后把 token 存到 localStorage然后 router.push。在请求封装中拦截器设置 Authorization响应拦截器如果遇到业务 code 401就清空本地 token 并跳转登录页。登录页主要是一个 form button点击提交后把表单数据交给 loginApi。这里有个细节登录页的表单校验要用 Element Plus 的 rules但接口错误提示尽量交给统一的响应拦截器页面里不要写大量 try/catch。如果使用 TypeScript先定义 LoginRequest 和 LoginResponse 类型避免接口返回的 any 满天飞。script setup langts import { reactive } from vue import { useRouter } from vue-router import request from /api/request const router useRouter() const form reactive({ username: , password: }) const handleLogin async () { const res await request.post(/auth/login, form) localStorage.setItem(token, res.data.token) router.push(/dashboard) } /script4.4 本地联调代理配置与踩坑本地开发时前端启动在 5173后端在 8080。首先在 vite.config.ts 里配置 server.proxy把 /api 代理到 http://localhost:8080。注意后端接口如果本身没有 /api 前缀代理配置里需要用 rewrite 去掉。我给一个配置示例。// vite.config.ts export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })设置好代理后登录、获取用户信息基本能跑通。常见的坑是后端接收参数时遇到跨域之外的 404可能是代理路径写错了另一个是前端请求带了 token 但后端过滤器没有从 header 中解析导致一直 401。后面常见问题部分再展开。5. Windows 上用 Jenkins 部署前后端分离项目5.1 为什么我选了 Jenkins 而不是手工发布之前很多项目都是手动 Maven 打包然后传到服务器前端 npm run build 之后再拷贝 dist 目录重复劳动不说还容易漏。这次接手的是 Windows 服务器项目托管在 GitLab 上我直接用了 Jenkins 做自动化部署。Jenkins 能监听 Git 提交自动触发构建前端执行 npm install npm run build后端执行 mvn clean package最后把产物放到指定目录重启服务。Windows 上安装 Jenkins 比较省事下载安装包后以 Windows 服务方式运行默认端口 8080。有两点需要提前规划一是 Jenkins 工作空间的路径不能有中文或特殊字符二是构建机需要提前装好 Maven、Node、Git并把环境变量配好否则构建任务里调用命令会失败。我建议在 Jenkins 全局工具里配置 JDK、Maven、Node 的路径而不是依赖系统 PATH这样多节点构建时更可控。5.2 前端构建与后端打包流水线怎么配Jenkins 流水线我用的是 Pipeline 加 Jenkinsfile实际部署时把脚本放在项目仓库里拉下来就能构建。前端部分进入 frontend 目录执行 npm install可以加 --registry 指定镜像源npm run build构建产物在 dist 目录。后端部分进入 backend 目录执行 mvn clean package -DskipTests生成 jar 包。最后的发布步骤就是停止旧进程、替换文件、启动新进程。pipeline { agent any stages { stage(checkout) { steps { git url: http://gitlab.example.com/project.git, branch: main } } stage(frontend build) { steps { dir(frontend) { bat npm install --registryhttps://registry.npmmirror.com bat npm run build } } } stage(backend package) { steps { dir(backend) { bat mvn clean package -DskipTests } } } stage(deploy) { steps { bat net stop DemoService || true copy /y backend\\target\\demo.jar D:\\deploy\\demo.jar net start DemoService } } } }如果后端不想注册成 Windows 服务也可以用 javaw 命令启动但进程管理不太方便日志也不好收集。我这边是把 jar 包注册成一个 NSSM 管理的服务Jenkins 发布时只需要 net stop / net start整个过程很顺畅。5.3 Nginx 反向代理与静态资源分离前端 dist 和后端 jar 都准备好之后接下来是部署方式选择。我最终选择了用 Nginx 托管前端静态资源并把 /api 请求反向代理到本机的 Spring Boot 服务。原因很简单如果直接把 dist 丢进 Spring Boot 的 static 目录虽然也能跑但严格意义又回到了“混合部署”而且以后前端升级很别扭。用 Nginx 分离之后前端只关心静态文件后端只关心 API职责清晰。server { listen 80; server_name localhost; root D:/project/frontend/dist; index index.html; 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; } }这里 try_files 是解决前端 history 路由刷新 404 的关键proxy_pass 里的尾斜杠会把 /api/ 前缀去掉再转发给后端和本地代理的 rewrite 对应。部署时还要注意 Windows 防火墙要允许 80 端口和 8080 端口不然外部访问不了。6. 常见问题与排查技巧实录6.1 跨域问题反复出现该怎么办现象是浏览器控制台报 CORS或者请求直接失败。排查步骤先看请求发到哪个地址如果还是 5173 就说明代理没生效再看代理路径 rewrite 是否正确最后才考虑后端 CORS 配置。我推荐开发期一律用代理生产用 Nginx这样跨域问题出现频次会低很多。还有一个常见误操作后端配置 CORS 时用 allowCredentials(true) 同时又指定了多个 allowOrigin这种组合浏览器会拒绝。如果必须要带 Cookieorigin 不能写通配符。6.2 刷新页面 404 的经典解法现象是登录后进入页面按 F5 刷新变成 404。原因是 Vue Router 默认是 history 模式路径是经过前端 router 控制的但 Nginx 收到 /user/1 后会去找真实文件找不到就 404。解决就是用 try_files 把不存在路径回退到 index.html。还有一个选择是改用 hash 模式但 URL 不美观不建议。如果用了 CDNCDN 的回源规则也要配成同样的 fallback 逻辑否则分布式边缘节点上文件不存在时同样 404。6.3 Token 失效后的 401 风暴现象是 token 过期那一刻页面发起多个并发请求全部 401前端弹出很多报错。解决响应拦截器里只处理一次“未登录”比如加一个标志位第一个 401 就去清理 token 并跳转登录页其他请求直接 reject。如果要做得更好用刷新 token 机制把并发请求缓存起来等新 token 返回后再重放。这一块比较复杂但做了之后体验会好很多。我项目里的做法是封装一个 isRefreshing 变量和一个队列刷新期间挂起的请求先进入队列等新 token 拿到后统一带上重发。6.4 接口返回格式不一致的隐患现象是页面反复校验 data 里的字段但还是偶尔报 undefined。原因是不同开发写的接口有些返回 { data: {...} }有些直接返回数组有些错误时只返回 message。这个在前后端分离项目里非常常见。根本解法是团队强制统一返回体并且代码评审时就看这个临时解决办法是前端在响应拦截器里做一层适配但治标不治本。我见过最严重的情况是后端同学把异常信息直接堆到 message 里前端弹窗把数据库报错原样展示给用户体验很差。所以全局异常处理器一定要兜住所有异常并转换成用户能看懂的话。6.5 排查问题速查表下表是我整理的最常见问题和解决办法适合贴到团队知识库里。症状可能原因解决方案登录失败一直 401密码错误或 token 解析失败先检查密码校验逻辑再看 JWT 密钥是否一致部署后页面白屏前端静态资源路径 base 配置错误Vite 设置 base: ./ 或使用绝对路径刷新页面 404Nginx 未配置 try_files增加 try_files $uri $uri/ /index.html接口偶尔超时后端线程池太小或慢 SQL调整 Tomcat 线程数优化 SQL 和索引Jenkins 构建失败环境变量未配置路径含中文在全局工具配置 JDK/Node/Maven 路径Windows 服务启动失败端口被占用或缺少运行参数检查日志确认 application-prod.yml 端口跨域报错生产环境接口和前端域名不一致统一域名通过 Nginx 反向代理 /api最后再分享一点个人体会。我实际操作下来好的模式都是约定出来的。前后端分离不像想象中只是把代码分成两个目录它要解决的是跨端协作、接口规范、鉴权方式、部署拓扑这些系统性问题。所以先别急着上框架把基础规范和链路走通后面每一步都会顺很多。就拿这次部署来说如果当初没有先定好返回码和代理规则后面排查起来肯定要花好几倍时间。项目后续如果再扩展我会在微前端方向和渐变式 SSR 上继续尝试但那是另一个话题了。希望这篇能帮你少踩几个坑。
返回列表