
前后端分离这个词这几年几乎成了Web开发的默认姿势。打开招聘网站十个后端岗位有八个写着“熟悉Spring Boot Vue前后端分离开发”GitHub上热门的前端项目也几乎都长一个样dist目录打包静态资源后端只负责出接口。但“分离”到底分离了什么很多人其实没完全想清楚。有人觉得分离就是把HTML从后端模板里挪到前端工程里有人觉得分离就是前端用Vue、后端用Spring Boot还有人把前后端分离和跨域问题绑定在一起一提分离就想到CORS。我早年做项目是从JSP时代过来的那时候一个页面里HTML、Java代码、SQL语句混在一起改一个按钮颜色要重启整个服务数据库加一列字段前后端都要跟着动线上出问题还得把前后端代码一起回滚。后来切到前后端分离架构后整个节奏完全变了——前端调接口、后端改数据两条线并行推进互不扯皮。但这套模式真落地的时候又会踩到接口设计、鉴权、跨域、部署、联调协作一大堆坑。这篇文章我不打算只讲概念而是把这套前后端分离模式从设计思路、核心细节、项目实操到常见问题排查完整拆一遍。不管你是刚转行的新人还是想把现有项目拆成前后端分离的老手都能在这里找到一个可以照着做的完整方案。1. 前后端分离拆开的不只是代码更是协作方式1.1 从模板渲染到前后端分离为什么要拆先聊聊这套模式为什么会出现。在早期的Java Web开发里JSP是绝对的主流。一个.jsp文件里既能写HTML标签又能嵌入% %脚本块还能直接引用后端的JavaBean。这种写法的好处是“方便”前端页面想要数据直接在服务端取出来塞进页面就行。但坏处随着项目变大越来越明显第一耦合严重。一个页面的展示逻辑和业务逻辑搅在一起前端工程师要懂Java才能改页面后端工程师改个字段还要担心页面渲染崩掉。第二联调效率低。前端页面和后端代码打包在同一个Web应用里每次改动都要整个应用重新编译、重启。哪怕只是改个按钮颜色也得走一遍完整的构建部署流程。第三分工困难。前端和后端的技能栈完全不同硬塞在同一个工程里两边都没法独立开发、独立测试。前端想用一个更好的构建工具后端说不行会影响我的工程结构。前后端分离的本质就是把“数据”和“展示”这两件事彻底拆开。后端专注提供数据接口前端专注消费数据并渲染页面。双方通过一份约定好的接口契约交互谁都不需要关心对方内部是怎么实现的。这其实和“设计模式”里的“开闭原则”是一个思路——对扩展开放对修改关闭。接口稳定了前后端各自内部怎么改都不会影响对方。1.2 分离的真正边界工程、接口、部署很多人以为分离就是把代码文件分两个仓库前端一个、后端一个。这只是最表面的工程边界。真正的前后端分离要拆三层第一层是工程边界。前端工程用Vite、Webpack这类构建工具管理产出纯静态资源后端工程是独立的API服务不渲染任何页面。前后端各自有独立的代码仓库、独立的依赖管理、独立的测试流程。第二层是接口边界。这是前后端分离里最重要也最容易忽略的一层。后端暴露的每个API必须有明确的路径、方法、入参、出参定义。前端只依赖这两份东西不依赖后端的具体实现语言、框架、部署地址。接口就是双方签的合同合同摆好了各自开发才不互相等。第三层是部署边界。前端构建出的静态文件部署到Nginx这类Web服务器上后端服务独立部署在应用服务器或容器里。两者可以分别水平扩展——前端静态资源流量大了就多配几台Nginx后端接口压力大了就多扩几个实例。甚至可以把前端静态资源放到CDN上这是传统模板渲染模式根本做不到的。1.3 什么样的项目不适合做前后端分离写到这里我得泼一盆冷水。前后端分离不是银弹不是所有项目都适合无脑上。用这套模式是有成本的——接口设计要花时间、联调要花时间、跨域要处理、权限要重新梳理。如果项目很小比如一个公司内部不到十个页面、没有独立前端团队的报表系统用传统的服务端模板渲染反而更快一个人就能搞定。另外对SEO要求极高的内容型网站如果不上服务端渲染SSR或静态站点生成SSG纯前端渲染会带来很大劣势。搜索引擎爬虫抓到的可能是一个空的HTML骨架或者执行完JS跑出来的结果既要额外做预渲染又要解决直出问题成本很高。所以判断一个项目适不适合前后端分离核心看三个条件团队有没有明确的前端后端分工、项目规模大到值得维护一套接口契约、页面渲染和数据获取是否真的需要解耦。这三个条件都满足再谈分离才有意义。2. 分而不散接口、鉴权与跨域是三大核心细节2.1 接口契约先定好统一返回体与全局异常前后端分离项目最先要定也最容易被忽略的就是接口返回格式。很多项目前期没人管这件事张三写的接口返回一个JSON对象李四写的接口返回一个字符串王五心情好又套了一层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; } }同时配合全局异常处理器让后端代码里不用到处写try-catch。业务异常统一抛出最终都会转成上面这个结构返回给前端。这样一来前端在axios拦截器里只要判断一次code等于200就取data不等于200就提示message所有接口的交互逻辑完全一致。分页接口也要单独定一个通用结构。我常用的分页返回体包含total、list、pageNum、pageSize四个字段前端拿到这个结构就能直接渲染所有带分页的表格不用每个接口单独适配。2.2 JWT鉴权登录态如何跨端保持HttpSession那套方案依赖服务端保存会话状态天生就不适合前后端分离。因为前端可能是Web、可能是App、可能是小程序后端可能是单体、可能是微服务集群。保持登录态最主流的方案是JWTJSON Web Token。JWT的核心思路是用户登录成功后后端生成一个包含用户信息的签名Token返回给前端前端每次请求都在Header里带上这个Token后端验证签名即可。这个方案的好处是天然无状态后端不需要存Session集群环境下不需要做会话同步验收也方便。完整落地过程我一般分四步第一步登录接口发放Token。用户提交用户名密码校验通过后用HMAC或RSA算法签发Token有效时长一般设两小时到一天。第二步前端存储Token并在请求头携带。Token存localStorage还是cookie是有讲究的。存localStorage更简单但容易被XSS攻击读取存cookie要设置HttpOnly但会面临CSRF风险。项目里我常规做法是存localStorage然后在axios请求拦截器里给每个请求加Authorization头。这样后端能直接读Header不依赖cookie机制。// axios 请求拦截器 service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer ${token}; } return config; }, error Promise.reject(error));第三步后端配置拦截器统一校验。写一个JwtInterceptor排除登录、注册等白名单接口其余所有请求都先校验Token。校验失败直接返回401前端响应拦截器收到401就跳转登录页。第四步登录过期处理。Token是有时效的过期后前端所有请求都会401。我的做法是在响应拦截器里判断401后清除本地Token跳转到登录页并提示“登录已过期请重新登录”。更流畅的方案是搞一个刷新Token的机制——一个短期Access Token配一个长期Refresh TokenAccess Token失效时用Refresh Token重新换一个。这个机制在移动端很有用Web端如果嫌麻烦直接过期重新登录也完全能接受。2.3 跨域问题开发环境与生产环境要分开处理前后端分离项目必然面临跨域。前端页面跑在localhost:5173后端接口跑在localhost:8080端口不同浏览器的同源策略就会拦截请求。很多新人在这里被折磨到怀疑人生配置半天CORS还不生效。其实跨域这个问题的解法在开发和生产环境是完全不同的两套思路。开发环境的正解是前端代理。以Vite为例配置server.proxy把/api开头的请求转发到后端地址// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } });这样前端请求/api/user/listVite开发服务器会转发到http://localhost:8080/user/list浏览器里看到的请求始终是同源的完全不会触发跨域。这套机制的原理类似反向代理Vite开发服务器充当了中转站。生产环境的正解是Nginx统一入口。前端静态资源和后端接口都挂在同一个域名下通过路径前缀区分。/api开头的请求走反向代理到后端服务其余请求直接命中前端静态文件server { listen 80; server_name yourdomain.com; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://backend-server:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }这套配置下来前后端天然同源不需要后端额外开CORS。只有一种情况必须处理后端CORS——前后端域名分离部署比如前端挂在CDN、后端是独立API服务。这时候才用CrossOrigin注解或者CORS配置类而且要注意配置allowedOrigins时必须写完整的前端域名不能写*否则配了withCredentials也没用。3. 从零到一落地Spring Boot 3 Vue 3 前后端分离实操3.1 工程初始化与目录结构设计拿一个典型的Spring Boot 3 Vue 3项目举例我会把工程分成两个独立仓库前端用Vite构建后端用Maven管理。前端目录结构长这样frontend/ ├── public/ ├── src/ │ ├── api/ # 接口请求定义按模块拆分 │ ├── assets/ # 静态资源 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置 │ ├── store/ # Pinia 状态管理 │ ├── views/ # 页面组件 │ ├── utils/ # 工具函数axios实例一般放这里 │ ├── App.vue │ └── main.ts ├── package.json └── vite.config.ts后端目录结构长这样backend/ ├── src/main/java/com/example/ │ ├── controller/ # 接口层 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # 数据访问层 │ ├── entity/ # 实体类 │ ├── dto/ # 数据传输对象 │ ├── config/ # 配置类包含CORS、拦截器注册 │ ├── common/ # 统一返回体、异常处理、工具类 │ └── security/ # 认证授权相关 ├── src/main/resources/ │ ├── application.yml │ └── mapper/ # MyBatis XML文件 └── pom.xml这个结构不复杂但有几个关键的模块必须从第一天就放好common里的统一返回体、config里的拦截器注册、api目录下的接口请求封装。这几个模块是前后端协作的地基后面所有业务代码都建立在它们之上。3.2 开发联调后端接口文档与前端Mock双管齐下前后端分离之后最怕的是“后端还没写完前端没法开工”。解决这个问题的标准做法是后端用Swagger或Knife4j生成接口文档前端基于文档先做Mock数据。Spring Boot 3集成Knife4j的配置很简单引入依赖后在配置类里加上文档扫描注解启动项目后访问/doc.html就能看到接口文档页面。每个接口的路径、入参、出参模板一目了然前端照着文档就能写接口调用代码。前端自己再准备一套Mock也不是不行但既然后端文档已经定义了接口结构更高效的做法是前端在api目录下先把所有请求函数写好返回Promise在Mock阶段直接返回假数据。等后端接口跑通了只需要改一个baseURL切换开关。这里最忌讳的是前端自己造一套和后端结构不相符的假数据等联调时全都要返工。接口联调阶段我想强调一个工具不是聊技术是聊协作方式。前后端是同一团队的建议直接拉一个接口联调群或者用一个在线协作表格专门记录接口状态是“文档已出”“后端已实现”“前端已联调通过”还是“接口有变更”。很多项目的坑不是技术问题是“接口改了但前端不知道”、“前端改了但后端还在按旧参数排查”。一份实时更新的接口清单比什么工具都好用。3.3 后端接口数据交互的几个高频细节处理联调阶段有几个细节是新手必踩的我单独拎出来讲。第一个是Long类型精度丢失。数据库自增主键和雪花ID都是Long类型后端直接往前端返回JSON时超过JavaScript安全整数的数值会被截断。比如主键ID是1853413796856741888前端拿到手可能变成1853413796856741900再把这个ID传回后端查询直接查不到数据。解决办法有两种一是在实体类的ID字段上加JsonSerialize(using ToStringSerializer.class)直接序列化成字符串二是用Jackson Config全局处理。强烈建议全项目统一用第一种或者全局配置不要一个接口一个接口地处理容易漏。第二个是日期格式统一。后端返回日期默认是2025-06-11T10:30:00.00008:00这种格式前端展示起来很痛苦。我通常在application.yml里配置全局日期格式化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样后端所有日期接口都返回2025-06-11 10:30:00这种格式前端不用重复做格式化处理。第三个是下划线与驼峰映射。数据库字段习惯用user_nameJava实体类习惯用userName前端拿到JSON希望是userName。MyBatis-Plus默认开启驼峰映射如果用的原生MyBatis记得在配置里加上mapUnderscoreToCamelCasetrue否则字段映射不上查出来全是null前端就会一脸懵。3.4 遇到成熟脚手架项目时怎么合理地“抄作业”聊到实操不能不提那些前后端分离的开源脚手架。比如你可能会在网上看到大量基于Spring Boot Vue的快速开发平台像若依框架这类项目就是典型代表。它们已经把用户管理、权限控制、代码生成、系统监控这些通用功能都实现好了开发一个企业后台管理系统基于它二次开发效率极高。遇到这类脚手架我的建议是一定要借鉴但别无脑照搬。借鉴的是它的工程结构和通用模块设计——统一返回体怎么写的、权限拦截器怎么配的、菜单路由怎么动态生成的、代码生成器怎么用的。但要注意开源框架为了覆盖大量场景往往带了很多你用不到的功能模块如果直接拿来做生产项目一定要把用不到的功能删掉否则项目体积臃肿不说还要花额外精力维护。另外这类框架用的依赖版本一般比较保守整合进新项目前要先确认包版本有没有兼容性问题。4. 不止于编码部署、自动化与团队协作实践4.1 用Jenkins搭建前后端分离项目的自动化部署流水线项目开发完总要部署上线。前后端分离项目最大的优势就是在部署这一步体现得淋漓尽致——前后端可以独立构建、独立发布、独立回滚。我在生产环境最常用的部署方式是Jenkins从Git拉取代码并完成前端构建再将dist目录同步到Nginx服务器同时也拉取后端代码用Maven打包并重启后端服务。整套自动化流程我写成Jenkins Pipeline文件长这样pipeline { agent any stages { stage(拉取前端代码) { steps { git url: gitgithub.com:yourname/frontend.git, branch: main } } stage(构建前端) { steps { sh npm install --registryhttps://registry.npmjs.org sh npm run build } } stage(部署前端) { steps { sh scp -r dist/* rootyour-server:/usr/share/nginx/html/ } } stage(拉取后端代码) { steps { git url: gitgithub.com:yourname/backend.git, branch: main } } stage(打包后端) { steps { sh mvn clean package -DskipTests } } stage(重启后端服务) { steps { sh ssh rootyour-server systemctl restart backend.service } } } }这套流水线跑起来后开发只需要把代码推到主干分支剩下的构建、部署、重启全部自动完成。要注意前端的scp同步最好配合rsync做增量同步首次全量没问题后面每次构建只传变更文件部署速度会快很多。4.2 多人协作中最容易被忽视的接口变更管理前后端分离后团队内部的技术依赖变成了“接口依赖”。我最想给团队强调的一句话是接口不是写出来的是讨论出来的。后端的接口设计要秉承“面向前端需求”的原则不能我数据库有什么字段就返回什么字段而是前端页面要什么数据就返回什么数据减少一次请求携带无用字段。多人协作时接口变更管理是个大学问。代码层面要解决的是后端接口签名改了前端怎么及时感知前端换了接口后端怎么知道。解决思路是从流程上约束第一接口变更必须走文档更新流程。先改接口文档再改代码最后通知前端同事。顺序反了文档和代码就会出现不一致。第二接口版本管理要明确。如果是一次破坏性的重构——路径改了、参数改了、返回结构改了建议不要直接覆盖原接口而是新增一个V2版本让前端有时间平滑迁移。第三定期做接口联调评审。我一般建议在每次迭代结束前前后端坐在一起过一遍改动过的接口用真实数据跑一遍流程能提前发现一大批字段缺失、类型不匹配、状态码语义不统一的问题。5. 常见问题排查与避坑实录5.1 前后端分离项目高频问题速查表我把这几年实操中遇到的高频问题按“现象 — 原因 — 解决方案”整理成一个速查表方便你遇到问题直接对照现象原因解决方案前端接口请求404代理没配全或者Nginx的location匹配没生效检查Vite proxy配置Nginx配置里location顺序是否合理前端能登录但登录后接口401Token没带或者拦截器没放行该路径检查axios拦截器加Token的逻辑后端JwtInterceptor白名单是否遗漏刷新页面后404白屏前端用的是history路由Nginx没配try_files在Nginx静态资源location里加try_files $uri $uri/ /index.html接口返回了数据但页面显示不出来字段名对不上或者Long类型精度丢失检查后端是否启用驼峰映射Long类型ID按字符串序列化开发环境接口能通部署后跨域报错生产环境没走Nginx代理直接直连后端接口用Nginx统一反向代理保持同源页面中文乱码后端返回头没指定UTF-8或者前端页面编码不对检查后端server.servlet.encoding配置确保HTML文件是UTF-8保存上传大文件一直失败Nginx默认client_max_body_size太小在Nginx增加client_max_body_size 50m5.2 跨域配置不生效的排查思路跨域是最容易让人头大的问题。你配置了CrossOrigin但还是报CORS错误我每次排查都会按下面这个顺序走一遍第一确认请求是“简单请求”还是“预检请求”。如果前端请求带着自定义Header比如Authorization浏览器会先发一个OPTIONS请求探路。很多后端框架的拦截器直接把OPTIONS请求拦截掉了导致预检请求都到不了Controller层更谈不上返回CORS头。解决办法是在WebMvc配置类里让CORS配置和拦截器都放行OPTIONS请求。第二确认CORS配置没有和Spring Security冲突。如果你用了Spring SecurityCORS过滤器一定要配置在Security过滤器链之前否则Security先返回了403CORS头根本没机会写入响应。第三确认allowedOrigins没有写*。当你需要携带Cookie时Access-Control-Allow-Origin不能是星号必须是指定的完全匹配的域名而且要配合allowCredentials(true)使用。5.3 前端history路由刷新404的经典处理这个问题我见过不下十次。前端用Vue Router的history模式本地开发一切正常打包部署到Nginx后点击页面里跳转没问题但一旦按F5刷新直接就404。原因是history模式的路由路径是浏览器地址栏的真实路径比如/user/list。刷新时浏览器向服务器请求这个路径但服务器上根本没有这个路径对应的静态文件于是返回404。解决办法就是Nginx的try_files配置。它会在静态文件确实不存在时把请求重写到/index.html让前端路由自己去匹配。注意上面那个配置里location /块里必须写上try_files $uri $uri/ /index.html;缺一不可。还有一种变体如果前端部署在子路径下比如/admin那么Nginx配置和前端路由的base参数都要同步设置否则资源路径全乱。5.4 联调阶段最大的隐藏坑环境差异最后一个坑我必须强调。有时候开发和测试环境都好好的一上预发布环境就出一堆问题。多数情况不是代码问题是环境差异后端接口的IP或域名配置在前端是写死的还是走环境变量很多前端工程里把baseURL写死在src/config.js里换环境就要改代码重新构建。正确做法是区分构建环境用Vite的import.meta.env按环境加载配置或者更简单地把API地址放在Nginx的代理配置里前端始终请求相对路径/api这样换环境完全不用重发前端。数据库数据和开发环境不一致比如开发环境有100条数据测试环境只有3条就会出现分页组件的边界问题。这个要靠测试环境的数据尽量仿真解决。还有时间同步。后端服务器和用户本地时区差异会导致接口返回的时间相差8小时记住后端统一用GMT8前端展示时也要关注本地时区。前后端分离这套模式说到底不是技术有多高深而是它逼着你把“数据”和“展示”的边界想清楚。我见过太多项目名义上说了前后端分离实际上前端页面里到处硬编码接口地址、后端Controller里返回乱七八糟的结构最后联调一个月才上得了线。这些坑基本都在上面这些细节里。如果你正准备把一个老项目改造成前后端分离我的建议是从小处入手先把某个模块的接口切出来跑通开发联调部署全流程再逐步推广。这个过程里接口契约和自动化部署越早建立越好它们会反过来逼着团队规范起来。我自己踩了这么久的坑最深的体会是前后端分离的难点从来不在“分”而在“分完之后怎么还能高效地合在一起”。