1. 十年前我为什么要带头把项目拆成前后端
先把时间拉回到2013年左右。那时候我刚工作没几年,团队接的都是企业级管理系统,技术栈高度统一:后端用Java,页面用JSP,数据库用MySQL,服务器清一色Tomcat。听起来没什么问题,但真正动手的时候,所有人都憋着一肚子火。
当时最典型的场景是这样的:前端工程师改一个按钮,要把整个Java项目在本地跑起来,启动一次Tomcat少说也要几十秒,等Spring容器加载完、等MyBatis的Mapper扫完,半分钟就过去了。更难受的是JSP页面里经常混着Java代码,<%= request.getAttribute("xxx") %>这种写法到处都是,后端写了一段逻辑,前端想看分支判断得连Java语法一起懂。
代码改完要联调,那就更费劲了。一个页面上有三个区块,A区块是张三做的,B区块是李四做的,两个人的代码都在同一个JSP文件里,用SVN往上一提交,冲突能吵半天。那时候我们常开玩笑说:前后端没有分离,分离的是人和人之间的耐心。
真正的导火索是一次上线事故。电商促销页面要改版,前端希望两周搞定交互,后端说接口要按他的数据结构排期,结果两边僵到最后一周,前端用iframe嵌了一个纯HTML页面,勉强把效果做出来了,但数据全部写死。那一次之后,我们团队做了一个很朴素的决定:把控制器的返回类型从ModelAndView改成@ResponseBody,把JSP改成纯HTML,前端自己调接口,后端只负责出数据。
这个决定就是前后端分离在中小团队里的雏形。回头看,它解决的核心问题不是技术,而是协作边界。后端能专心做业务和数据处理,前端能自己做交互、样式和部分业务逻辑,两边的交付节奏不必再被绑在同一个发布周期里。
2. 从JSP到RESTful API:前后端分离的技术路线是如何走出来的
2.1 第一阶段:Ajax小规模使用,页面仍是多页应用
其实前后端分离并不是突然出现的。2005年Gmail把Ajax大规模带到浏览器端之后,很多人就发现页面局部刷新比整页提交要快得多。但那时候Ajax只是作为JSP页面的一个辅助手段:用户点一个按钮,页面不跳转,前端用XMLHttpRequest向服务端要一段数据,然后DOM操作把结果塞到某个div里。
这个阶段严格说不叫"分离",叫"异步化改造"。后端依然是MVC那一套,Controller返回的还是JSP页面,只不过页内有一部分交互走接口。基于这种模式,前端压力不大,但前端代码的责任边界已经开始露头:谁负责维护那些繁杂的DOM操作?页面一多,回调一层套一层,代码很快就没法看了。
2.2 第二阶段:SPA爆发,前端真正接管页面渲染
大概在2013到2015年,AngularJS、React、Vue陆续进入视野,单页应用(SPA)的概念开始普及。SPA最大的变化是:页面渲染完全交给前端,后端不再返回HTML,而是返回结构化数据(一开始是XML,后来几乎统一成JSON)。
这个变化是决定性的。因为一旦后端不再产HTML,前端就需要自己去解决路由、状态管理、模板编译、组件通信等一系列问题。而一旦前端开始像做工程那样管理代码,Node.js作为构建工具链的基底就被迅速普及。现在大家用烂了的npm、webpack、babel,都是那两年沉淀下来的。
我自己经历过的典型改造路径是这样的:后端把Controller拆成两层,一层专出JSON(比如/api/user返回用户对象),一层不再用JSP,把原有页面静态化扔给Nginx托管;前端项目单独初始化一个工程,用Vue CLI之类脚手架搭好之后,开发阶段用proxy插件把/api请求代理到后端端口,生产阶段再交给Nginx做反向代理。
2.3 RESTful API成了大家嘴上不说但必须认的规矩
前后端分离走到一定规模之后,大家发现一个问题:接口规范如果靠口头约定,迟早要出事。比如"获取用户信息"这个接口,A团队写成/getUser,B团队写成/user/query,C团队直接/user?id=1,联调和维护成本急剧上升。
RESTful风格之所以能普及,不是因为"REST"这个词有多高级,而是它给出了一套足够简单的约定:资源用名词,操作用动词映射到HTTP方法。GET /user/1获取用户,POST /user创建用户,DELETE /user/1删除用户。这套约定大大降低了前后端沟通时的理解成本,也让接口文档变得好写。
当然RESTful不是万能的。企业管理系统里大量存在"导出报表""批量审批""复杂条件分页查询"这类动作,如果死抠REST语义会很别扭。成熟的团队通常的做法是:核心资源走RESTful风格,报表、提交、批量操作等场景用RPC风格接口或加/action后缀,比如POST /report/export、POST /order/batchAudit,目的只有一个——让调用的人一眼看懂,不产生歧义。
3. SpringBoot + Vue这套组合为什么成了主流,若依框架解决了什么
3.1 SpringBoot消灭了配置地狱,Vue解决了组件复用
如果只选一套技术栈来代表过去五年前后端分离的主流实践,SpringBoot + Vue绝对是最有代表性的。先说SpringBoot这一侧,它最大的贡献是把SSM时代混乱的XML配置压缩成了零配置或极少配置。以前搭一个SpringMVC项目,要写web.xml、spring-mvc.xml、mybatis-config.xml,一堆bean定义看得人头皮发麻;SpringBoot通过自动配置和起步依赖,一套spring-boot-starter-web就搞定了大部分东西,前后端对接时互相传递的接口模型也更好维护。
Vue这边呢,它的学习曲线比React平缓不少,模板语法非常接近"写HTML"的心智模型,模板、组件、路由、状态管理一套下来极其顺手。而且Vue对中后台项目简直天生适配:表格、表单、弹窗、抽屉、分页这一套东西,做一个页面几乎等于做一次组件拼装。
我在SpringBoot + Vue项目里最常用的目录约定是:前端src/api模块统一封装axios,每个后端Controller对应该目录里的一个JS文件;后端这边controller、service、mapper三层严格分层,接口返回统一封装成Result<T>结构。
3.2 一个完整接口的典型链路:从前端点击到后端落库
很多人刚接触前后端分离时,容易被"一堆框架"吓到,但其实整个调用链路很朴素:
- Vue页面里用户点"保存"按钮,触发一个methods里的方法。
- 该方法调用
src/api/user.js里封装的addUser(data)。 addUser内部调用axios发起POST /api/user请求,并设置请求头Content-Type: application/json。- 请求先到Nginx(或开发时经过Vite代理),被转发到SpringBoot后端的8080端口。
- SpringBoot的
UserController.addUser(@RequestBody UserDTO dto)接收JSON,做参数校验,调到UserService,再经UserMapper执行MYSQL的INSERT语句。 - 结果一层层返回到前端,Vue组件根据返回的
code字段判断成功失败,成功了就关弹窗、刷新列表,失败了就在页面上展示错误消息。
这个链路里最值得注意的约定有两个。第一,接口返回结构要全局统一。我见过很多团队一开始不统一返回格式,有的接口直接返回数组,有的返回{success: true},有的返回{code: 0},前端拿到不同接口要做不同的判断,痛苦不堪。统一成{code: 200, data: {...}, message: "success"}之后,前端只需封一个响应拦截器,判断一次code,异常提示全局处理,省下大量重复劳动。
第二,DTO和实体类一定要分清。很多新手图省事,直接把MyBatis的实体类用在Controller入参上,结果有一天数据库加了一个敏感字段(比如用户表的password_hash),前端只要多传一个字段,不小心就会把脏数据写进库里。正确的做法是Controller接收DTO,Service层负责把DTO转换成实体,再交给Mapper操作,这样接口层永远不会暴露数据库结构。
3.3 若依框架:中后台项目的"开箱即用"方案
如果你接触过国内大量企业级前后端分离项目,一定绕不开若依(RuoYi)。这个开源框架本质上是把SpringBoot + Vue + MyBatis这一套中后台的公共能力全部预置好了:登录认证、权限管理(RBAC)、代码生成器、定时任务、操作日志、数据字典、多数据源配置,几乎一个不落。
很多新手会问:若依到底解决了什么问题?我的理解是,它解决了"新项目从零搭建时那最难熬的一周"。没有若依之前,做一个后台管理系统的第一周基本都在搭框架:画登录页、搞用户表角色表、写token校验逻辑、写权限拦截器、做菜单管理。有若依之后,你clone一下代码,改个数据库连接,跑起来就已经有一个能登录、能管理菜单、能分配权限的基础后台了。
但用若依也有需要注意的事。第一,它的代码风格偏传统,很多地方用了startWith、endWith拼条件的方式写SQL,如果你习惯写MyBatis-Plus的LambdaQueryWrapper,刚上手可能会觉得不够"现代化"。第二,若依的权限模型是经典RBAC,也就是用户—角色—菜单三级模型,如果你的业务场景需要数据权限行级控制,光靠若依的默认配置是不够的,得自己扩展数据权限的SQL拼接逻辑。
我在几个外包和政府项目中都基于若依改过,最大的体会是:拿它当脚手架没问题,但不要被它的结构框死。新模块尽量按你自己的业务领域去划分包结构,只复用它的基础能力(登录、权限、代码生成),业务代码尽量独立,这样后续即使框架升级,你的业务代码迁移成本也有限。
4. 前后端分离项目的部署真相:Tomcat和Nginx到底各管哪一段
4.1 一个常被问的问题:SpringBoot前后端分离项目能不能丢Tomcat?
搜索热词里"tomcat部署前后端分离项目"排在很前面,可见这是很多人实际会遇到的场景。先给出明确结论:能,但不能直接用Tomcat把前端页面和SpringBoot的API一起部署。
为什么?因为前后端分离之后,前端产物是纯静态资源(HTML、CSS、JS、图片),它不需要Servlet容器来处理,只需要一个静态文件服务器;而后端的SpringBoot应用是一个Jar或War包,虽然内嵌了Tomcat,但它的职责是提供API。如果把前端静态文件也塞进Tomcat的webapps目录里,不是说完全不能跑,而是会带来几个问题:静态资源加载路径和接口路径混在一起,部署升级时改前端也要动Tomcat、动Java进程,完全违背了"独立部署、独立发布"的初衷。
更实际的问题是性能。Tomcat对静态文件的处理能力远不如Nginx,Nginx处理静态资源用事件驱动模型,高并发下占用的系统资源更少,配合gzip压缩、缓存过期头、浏览器协商缓存,同样一台服务器,前端页面的加载速度差距非常明显。
4.2 推荐的部署架构:Nginx托管前端,反向代理到Tomcat
这家单(个人最常见也最推荐的)结构如下:
用户浏览器 ↓ Nginx(80端口) ├── / → 托管前端dist静态文件 └── /api → proxy_pass 到后端Tomcat(8080端口)Nginx配置的核心长这样:
server { listen 80; server_name your-domain.com; # 前端静态资源 root /opt/ruoyi/dist; index index.html; # 兼容Vue Router的history模式 location / { try_files $uri $uri/ /index.html; } # 后端API location /api/ { proxy_pass http://127.0.0.1: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; } }这里有一个必须重点说的细节:proxy_pass http://127.0.0.1:8080/;末尾这个斜杠。如果你写成http://127.0.0.1:8080(没有结尾斜杠),那么/api/user/list会被转发到后端变成/api/user/list;如果你写成带斜杠的http://127.0.0.1:8080/,那么/api/user/list会被替换成/user/list,也就是**/api前缀被剥掉**。
后端Controller如果统一要求路径是/user/list(没有/api前缀),就采用带斜杠的写法;如果后端Controller本身就带/api/user/list前缀,就别加末尾斜杠。这个坑我见过不止一次,很多人前端请求栈报404,检查了半天最后发现是Nginx的proxy_pass斜杠问题。
4.3 Vue Router的history模式:刷新就404的根因
部署前后端分离项目时,另一大高频踩坑点是:打开首页一切正常,一刷新某个子路由,Nginx直接返回404。
原因是Vue Router在history模式下,路由是前端通过History API模拟的,浏览器地址栏的/user/list在后端服务器上根本不存在对应的物理文件。如果Nginx没有配置try_files,请求到达服务器后发现找不到文件,自然会404。
解决办法就是我上面配置里写的:
location / { try_files $uri $uri/ /index.html; }这行的意思是:如果请求的路径能找到静态文件,就直接返回文件;如果找不到,就回退到index.html,把路由交给前端的Vue Router自己去解析。
如果你的项目部署在子路径下,比如http://server.com/myapp/,那问题会复杂一些:Vue Router的base要设成/myapp/,Nginx的location也要对应调整成location /myapp/ { ... },而且try_files要写成try_files $uri $uri/ /myapp/index.html;。我第一次遇到子路径部署时,在这上面折腾了整整大半天,后来学乖了:默认都用根路径部署,实在不行再考虑子路径。
4.4 前后端跨域问题:Dev阶段和Prod阶段的处理不一样
跨域(CORS)也是前后端分离初学者最容易绕晕的点。先把这个机制说透:浏览器会拦截"跨域请求"(协议不同、域名不同、端口不同都算跨域),注意是浏览器拦截,服务器之间的请求不受这个限制。
开发阶段的解决办法非常简单:在Vite或Webpack的devServer里配一个proxy代理,把/api自动转发到后端的http://localhost:8080,因为浏览器发出的请求是同源的(都指向前端脚手架自己的端口),浏览器根本感知不到跨域,自然不会被拦截。
生产阶段的处理思路就不同了。既然我们已经在Nginx做了反向代理,前端和后端统一通过同一个域名访问(前端是/,后端是/api),本质上是同源的,跨域问题根本不存在。所以我在正式环境部署时,几乎不会去后端代码里加@CrossOrigin注解,也不建议团队加。
只有一种情况需要在后端处理跨域:前端和后端的域名或端口无法统一,比如前端部署在公司CMS平台上,后端独立部署在某台内网服务器。这时候才需要在SpringBoot里配置全局CORS,但请注意,allowOrigins尽量不要写成*,而是明确指定允许的来源列表,否则接口会被任意第三方页面调用,存在安全风险。
4.5 会话与Token:前后端分离后登录态该放哪
JSP时代,登录态靠Session + Cookie,服务端能直接感知用户状态。前后端分离之后,页面和接口分开了,Session的维护变得别扭,尤其是在跨域、多实例部署的场景下。于是Token成为主流的认证方案。
简单说,用户登录成功后,后端返回一个Token(常见的是有效期内签名的JWT),前端把它存在本地(常用的有localStorage、sessionStorage或者内存变量),之后每个请求通过请求头Authorization: Bearer <token>携带。后端用一个拦截器或AOP切面统一校验Token,解析出用户身份,再放行请求。
这里有几个实践心得:
不要把Token放在localStorage里存死。localStorage的读取会被任何同源脚本访问,如果页面存在XSS漏洞,Token很容易被偷走。折中的方案是存内存变量,刷新页面后丢失,再去调一个刷新接口重新获取。
JWT的核心是签名,不是加密。JWT里塞的用户信息是Base64编码的,任何拿到JWT的人都能解码看到,只是无法篡改。所以敏感字段(手机号、身份证号)不要直接放进JWT。
Token过期后要有一个平滑的刷新机制。后端接口返回
401时,前端响应拦截器要捕获到,然后用refresh_token去换新的access_token,换成功了重放原请求,换失败了再跳登录页。这个链路如果没做好,用户的体验就是"用着用着突然被踢回登录页"。
5. 前后端分离的代价:我们不能假装这不是一把双刃剑
聊到这里,如果只说前后端分离的好话,那是不负责任的。这些年我在项目里也见过不少"伪分离"和"过度分离",这些坑说穿了其实都是人祸,不是这个架构模式本身的问题。
5.1 伪分离:接口数量翻三倍,前端还是在写死数据
有些团队名义上做了前后端分离,实际上只是把原来的JSP变成了"后端生成前端JS变量"。Controller返回一个JSON壳,里面嵌套一段由后端拼出来的HTML字符串,前端拿过来直接用v-html或dangerouslySetInnerHTML渲染。这种模式表面上是接口返回数据,实际上数据里全是渲染好的HTML标签,前端想改成组件化渲染根本无从下手。
这种伪分离比不分离更糟,因为它既有前端工程的复杂性,又没有真正解放前端。要判断你们是不是伪分离,有一个很简单的标准:把接口返回的JSON里所有HTML标签删掉,看前端还能不能正常渲染页面。如果不能,那你就还是在用分离的壳做耦合的事。
5.2 过度设计:只是一个信息展示页,硬拆成三个系统
反过来,另一种极端是:一个很小的项目也强行搞前后端分离、微服务、消息中间件、容器编排。我见过一个只有三个功能模块的内部工具,前端用了全套Vue全家桶,后端拆了四个微服务,每个服务配一个独立的MySQL库,部署还要走K8s。项目的成本大部分花在了"为了让系统显得很专业"上,而不是花在业务交付上。
说真的,前后端分离的最小可行规模,至少得满足一个条件:你有一个明确的"接口消费者"和"接口提供者"。如果只有一个人做全栈,页面和逻辑都在同一个需求下快速迭代,采用一个成熟的SSR框架(比如SpringBoot + Thymeleaf,或者Next.js)反而更省力。等团队规模上来了,页面角色复杂了,再切到前后端分离也不迟。
5.3 接口成为新的性能瓶颈与排障难点
分离之后,前端看到的大部分"慢",已经从"后端渲染慢"变成"前端调了一堆接口"。尤其是首屏需要展示二三十个字段,如果前端没有做并发请求、懒加载、字段裁剪优化,很容易出现页面上每个区块都在转圈的情况。
一个页面调一堆接口还有一个隐患:接口失败率呈指数级放大。任何一个环节慢了或挂了,整块页面就渲染不全。所以我现在做项目一定会规划一个"API聚合层"或者一个"BFF层",让页面优先请求一两个聚合接口,后端内部把多个数据源的拼装做完,而不是让浏览器直接打印一个接口轰炸清单。
排障难也是实实在在的代价。分离之前,一条请求全链路都在应用日志里,好查;分离之后,一个请求要依次经过Nginx、网关、后端服务、数据库,日志散落在多处,前端发现问题时,后端往往已经轮转了半天。解决这个问题没有银弹,只能靠全链路追踪(如SkyWalking、Zipkin)和规范化的traceId透传,把一次请求串起来看。
6. 前后端分离之后的演进方向:哪些趋势会留下,哪些只是概念
6.1 SSR与同构渲染:向"首屏体验"妥协
前后端分离的SPA模式有一个天然短板:首屏加载慢、SEO不友好。搜索引擎爬虫虽然已经在进步,但对大量异步渲染内容的SPA页面,收录效果依然不如服务端渲染稳定。
于是这几年SSR(如Nuxt.js、Next.js)重新流行起来。它的思路是:首屏由服务端把JavaScript跑一遍,渲染出完整HTML返回给浏览器,后续页面交互再前端接管。听起来像是回到了"服务端渲染",其实是另一回事——渲染能力变成了同一套代码的两种执行环境,也就是"同构渲染"。
落地实践里,很多团队并不会全站SSR,而是选几个关键页面做SSR,比如首页、落地页、详情页,其余管理后台依然用SPA模式。我自己在电商项目里的做法是:C端页面用Nuxt SSR,B端管理后台保持Vue SPA,两边尽量复用公共组件库,部署分开但不互相阻塞。
6.2 微前端:多团队大型中后台的整合方案
"微前端"在过去几年被提到了很高的位置。它的核心诉求是:一个大型后台管理系统,由多个团队各自负责一部分,大家技术栈不一、发布节奏不一,但最终要集成在一个统一的壳里。微前端通过路由分发,把多个子应用整合成一个对外统一的应用,各自独立开发、独立部署。
这条路线我用下来觉得它解决的是组织问题多于技术问题。如果你的团队大了、系统复杂到一定程度,微前端能让相互协作的代价变低;但如果只是几个人维护一个小后台,强行上微前端只会引入大量的Remote Entry、沙箱隔离、公共依赖共享等问题,得不偿失。
6.3 BFF层:给前端定制"刚刚好"的接口
BFF(Backend For Frontend)的概念其实很朴素:在数据提供者和前端消费者之间,加一层专门为前端服务的聚合层。前端需要什么,BFF就组装什么,后端各领域服务不直接暴露给前端。
为什么要BFF?因为真正的后端服务接口往往是为"业务能力"设计的,字段很全,而前端页面可能需要的是"视图模型",字段不多但跨了多个服务。我在一个商城项目里,商品详情页要展示商品基本信息、库存状态、促销活动、店铺信息,如果让前端分别请求4个服务接口,首屏会慢得没眼看。后来我在BFF层用并行调用把4个服务的数据组装成一个详情DTO,前端一次请求全部拿到,首屏速度翻了近一倍。
但BFF不是免费的。它多了一层代码要维护,多了一层故障点。如果团队规模不大,我建议先把BFF逻辑放在原生后端Service层里,等确实出现"一个页面需要多服务数据拼装"再单独抽层。
6.4 前后端分离与低代码平台的博弈
最后说一个很多人没意识到的趋势:低代码平台正在"消灭"一部分常规的前后端分离开发。在低代码平台上,表单和列表通过可视化配置直接生成,底层封装好了接口调用,你不需要再关心数据是怎么来的。这确实让"没有技术背景的业务人员也能搭页面"变成现实。
但低代码从来不是替代前后端分离,而是把分离出来的接口复用到了更高一层。那些被低代码平台调用的数据服务,恰恰还需要后端工程师以规范的方式提供。低代码解决的是"页面渲染无关紧要的部分",而复杂业务逻辑、数据模型、权限边界,永远需要工程化的代码去兜底。
7. 说句实在话:面对还没开始分离的团队
如果一个项目还没有做前后端分离,或者正在考虑要不要做,我的建议很直接,分三步走。
第一步,把登录和权限梳理清楚。前后端分离后最痛苦的就是认证体系改造,先想好Token怎么发、怎么验、怎么刷新、跨域怎么处理,这比选框架重要一百倍。
第二步,挑一个新模块做试点。不要试图把几百个老JSP页面一次性翻新,那不叫重构,叫冒险。先拿一个交互复杂、变更频繁的模块做样板,梳理出接口规范、前端工程结构、部署管道,效果好了再复制。
第三步,把接口文档和状态码规范定死。前后端分离的工程能不能跑得顺,很大程度取决于两边对接口的"常识"是否一致。状态码怎么定义、分页参数怎么传、字段命名是驼峰还是下划线、时间格式是字符串还是时间戳,这些细节必须在项目初期就落成文档。
从我这些年的经验看,前后端分离最大的价值不是技术上的"解耦",而是让每个角色都能在自己的专业领域做到极致。后端不必再被前端页面逼着学习JavaScript和CSS,前端也不必为了改一个按钮去重启Tomcat。它把工程协作的边界定清楚之后,团队的交付速度和幸福感都会上一个台阶。
当然,它也逼着我们每个人走出舒适区:后端要懂接口设计、懂Token、懂跨域;前端要懂工程化、懂部署、懂性能。时代没有淘汰哪种语言或框架,它淘汰的永远是"只愿意守着自己那一亩三分地"的人。