多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MVC架构从后端到前端:Spring Boot、Vue与React的落地实践全解析

MVC架构从后端到前端:Spring Boot、Vue与React的落地实践全解析 做后端十几年又往前端折腾了几年被问得最多的一个问题是MVC 架构到底是什么。每次我都回一句它不是三本书叠在一起而是你写代码时脑子里那张“谁干什么”的地图。MVC 架构既是后端经典分层的起点也是现代前端 MVVM、组件化、状态管理这些方案共同的祖宗。这篇文章想从后端经典讲到前端现代化结合 Spring Boot、Django、若依、Vue、React 这些实际框架把 MVC 的来龙去脉、落地方式以及前后端分离项目里的跨域、传参、重复提交、大文件上传、部署这类硬核问题一次讲清楚。适合三类人被八股文绕晕的初学者刚从前端转后端或者从后端转前端的人以及正在准备前端/后端面试的朋友。1. 从后端经典说起MVC 到底在解决什么问题1.1 MVC 的三个角色和一条单向依赖链MVC 把一次请求处理拆成三个角色。Model 负责数据和业务规则View 负责把结果展示出来Controller 接收用户输入决定调用哪个模型和哪个视图。很多人背得出这三个字母但没真正理解它最关键的设计依赖是单向的。Controller 可以同时知道 Model 和 View 的存在但 View 不应该反向依赖 ControllerModel 更不应该知道页面长什么样。这句话值得反复琢磨。只要依赖方向控制住了你就能很自然地对每层做替换和测试不喜欢某个 View 实现换一个想改业务规则不动页面代码要加新入口只需要加一个 Controller 方法去组合已有 Model。这和写代码时需要面向接口是同一个道理先定义边界再填实现。我用餐厅来打比方Controller 是服务员Model 是后厨的菜谱和食材View 是端上桌的摆盘。顾客找服务员点菜服务员去后厨下单后厨按菜谱做菜摆盘后由服务员端出来。后厨不需要知道今天的盘子是圆的还是方的服务员也不用关心红烧肉到底放了几颗冰糖。各干各的餐厅才能同时接待一百桌客人。当然这个类比不能推得太远真实系统里 Model 和 View 之间还有联动比如 View 需要根据 Model 变化刷新。但在后端 Web 开发里绝大多数情况是一个请求进来Controller 拿着参数去 Model 拿数据再把数据交给 View 渲染出去链路非常短。1.2 不要把 MVC 和三层架构画等号我刚工作时也常把 MVC 和“三层架构”混在一起说。后来才搞明白经典三层架构是按系统分层表现层、业务逻辑层、数据访问层。而 MVC 更多是用来组织“表现层内部”的代码控制器接收请求、模型承载业务、视图负责输出。后端实践里Controller 属于表现层Service 属于业务层Mapper/Repository 属于数据层MVC 只覆盖了其中表现层那一小段。概念核心关注点典型对应经典三层架构系统纵向分层表现层 / 业务层 / 数据层MVC表现层内部职责分离Controller / Model / View后端实践组合请求进来后的完整链路Controller → Service → Mapper很多后端框架把 MVC 做成默认结构但如果你只把代码分成 Controller、Service、Mapper 三包却让 Controller 直接拼 SQL、让 Service 往回塞 HTML那它就不算合格的 MVC只是长了三个名字的一坨代码。分层是形式职责清晰才是目的。1.3 为什么几十年前的经典到现在还能打MVC 是上世纪 70 年代末 Smalltalk 时代提出的思想到今天后端主流框架仍然在遵守。原因不是我辈程序员缺乏创新能力而是 Web 请求模型太适合这套逻辑了一个 HTTP 请求进来解析参数、调用业务、返回响应天然就是 Controller→Model→View 的流水线。框架再替你把标记注解、路由映射、依赖注入这些重复劳动做了剩下的核心还是这套分工。但 MVC 不是银弹。做得不好会暴露两个典型问题一是 Controller 越来越“胖”什么逻辑都往里塞二是 Model 变成纯数据类只装字段没有行为业务规则全散落在 Service 里。后者在 Java 项目里尤其常见有人专门叫它“贫血模型”。这两种坏味道不是 MVC 的问题而是没有遵守 MVC 对职责边界的约定。带着这两个问题我们去看后端框架里 MVC 的真正落地。2. 后端落地实录主流框架怎么把 MVC 变成约定2.1 Spring Boot当 Controller、Service、Mapper 成为团队默认节奏用 Spring Boot 写后端几乎每个团队都会长成同一个样子controller 包放接口入口service 包放业务逻辑mapper 包放数据库访问entity 或 domain 放数据模型dto/vo 放接口出入参。注意这里 MVC 中的 Model 不是一个实体类那么简单。真正业务里的 Model 是“领域模型”但在工程上大家常常用 service dto entity 的组合来承担它名称不必较真边界必须清楚。一个查询订单接口的流转大致是Controller 拿到 userId 和订单号先做参数校验然后调用 OrderServiceService 里开启事务调用 OrderMapper 去查数据库查出来的 Order 实体再转成 OrderVO 返回给前端。写成代码就是下面这样RestController public class OrderController { private final OrderService orderService; GetMapping(/orders/{orderId}) public OrderVO getOrder(PathVariable Long orderId) { return orderService.getOrderDetail(orderId); } }Controller 里只有一行调用是不是看起来像“没写东西”对这就是对的。接口地址、参数接收、状态码这些是 Controller 的职责业务判断、事务控制、数据组装是 Service 的职责。如果你在 Controller 里看到了 try/catch 包业务、看到了事务注解、看到了 Mapper 调用基本就可以断定代码开始腐化了。这里还要提一下“后端 docx 模板生成”这类需求。很多系统要按模板导出 Word、PDF正确做法是把模板加载和渲染放到 Service 层由 Service 把数据填进模板返回文件流Controller 只负责把流写到响应里。这样换模板、换数据源都不影响接口入口。类似地集成 OnlyOffice 这类在线文档时也建议由后端统一获取文件配置、拼接下载地址前端只负责渲染这个“后端找数据、前端管展示”的分工就是 MVC 在后端服务化之后的延续。2.2 Django 的 MVT 与 Python 后端的 MVC 实践Python 生态里最典型的是 Django官方喜欢叫它 MVTModel、View、Template。区别是这里 View 同时承担 Controller 的职责——它接收 HTTP 请求操作 Model最后渲染 Template。换句话说Django 把 Controller 和 View 合并成一个“视图函数”再用 URL conf 做路由映射。用 DRFDjango REST Framework写接口时APIView 和 Serializer 又把职责重新划清了一点APIView 负责请求解析、权限判断、调用业务Serializer 负责数据校验、序列化和反序列化承担了 Model 和 DTO 的一部分工作。Python 后端常会遇到“后台有数据主动推给前端”的需求。Django 本身是同步处理 HTTP 请求的长连接推送不适合用普通 View 硬撑。实际项目里一般用 Django Channels 做 WebSocketConsumer 充当控制器Model 照常负责数据通道层把消息推给前端。这也是 MVC 思想的延伸实时通信只是换了一个“入口协议”职责拆分没有变。另外一个很常见的点Python 后端也要注意 Controller 层厚度。有人喜欢在 View 函数里写一坨业务一旦数据量上来测试和管理都会变得很痛苦。把业务放到 Service 层或独立模块里View 只做“接线”这是所有后端框架通用的准则。2.3 PHP 框架与若依这类脚手架里的 MVC 范式PHP 老牌框架同样贯彻 MVC。Laravel 里路由文件把 URL 分给 ControllerController 里调用 Model 拿数据用 Blade 模板渲染 ViewThinkPHP 也类似。PHP 的 MVC 因为部署简单、上手快经常出现在小团队和外包项目里但缺点也很明显模板引擎够用但不够现代前后端分离后更多人只用它写 JSON APIBlade 这类模板慢慢让位给 Vue/React。国内项目里绕不开的还有若依RuoYi。若依有单体版和前后端分离版分离版后端是 Spring Boot前端是 Vue它的代码生成器能根据数据库表直接生成 Controller、Service、Mapper、Entity 和一整套前端页面。这个脚手架本身就是 MVC 的“教学切片”生成出来的目录结构规范权限注解、分页查询、数据字典都已经预置好。若依这类框架用起来要注意三点第一不要改框架核心代码否则升级和排查变得非常困难第二新增业务尽量走代码生成然后在这个基础上改比手写一整套省事太多第三权限相关注解 PreAuthorize 要理解清楚它是 Controller 层做权限控制的入口绕过它等于裸奔。可以说若依把 MVC 从“理论”变成了“生成代码”这对初学者是很好的实践起点。3. 前端现代化从 MVC 到 MVVM 再到组件化状态管理3.1 早期前端的 MVC 尝试前端早期最有名的 MVC 尝试是 Backbone.js。它把数据放在 Model 和 Collection 里View 监听 Model 的 change 事件只要数据一变View 就重新渲染用户操作触发 View 里的函数再调用 Model 的方法更新数据。这套设计在当时很有启发性但用起来非常累事件绑定多了以后很难跟踪一个页面的 Model 和 View 可能有几十个对象关系网络比蜘蛛网还密。jQuery 时代就更“写意”了没有强制分层大家的代码习惯是点击按钮从 $(#input) 取值$.ajax 发请求成功回调里再 $(#list).append(...)。这种写法在小页面里很爽一旦页面复杂度上来“这行代码到底改的是哪个状态”都查不清楚。MVC 在前端早期失败的根本原因是DOM 本身就是 View 也是 Model数据存在页面上状态天然难以追踪。3.2 MVVM 到底改良了什么MVVM 的出现解决了“怎么让 View 跟随数据自动变化”的问题。ViewModel 夹在 View 和 Model 之间通过双向绑定或单向数据流把数据变化自动同步到视图上。Vue 的响应式系统是典型代表你修改一个 data 字段页面自动更新React 则是单向数据流加 setState 触发重新渲染理念上更像“单向 MVVM”。很多人以为 MVVM 是对 MVC 的推翻其实它只是把 MVC 里的 Controller 拆开把“更新视图”这个动作自动化了。如果你写过 Excel可以把单元格公式理解成绑定改 A1B1 自动变成 A1 的两倍。前端里 data 就是 A1页面就是 B1框架是那套公式引擎。经典 MVC 里的手工操作被框架吸收了但“数据、视图、逻辑”三者分离的思想一点没变。3.3 组件化之后M、V、C 都去哪儿了现在前端很少说“我这个项目是 MVC”因为组件化改变了组织单位。组件本身同时承担了 View 和一部分 Controller 职责模板是 View组件里的交互函数是 Controller 的局部调度逻辑。全局数据则由状态管理库承担 Model 的角色Vue 用 PiniaReact 用 Zustand、Redux它们把共享状态抽离出组件让多个页面共享同一份数据。View组件模板和样式Model全局 store、接口返回的领域数据Controller路由分发、事件处理、状态管理里的 action组件化以后最受益的是大型应用比如数字孪生大屏这类项目各种图表、地图、实时数据混在一起。如果不做状态分层组件一多数据流马上乱。把“每个组件自己管自己 state”和“全局共享数据走 store”这两条线划清楚MVC 的思想就还在。企业级前端框架比如 hzero 也是这个路数页面组件只管交互公共模型和接口封装统一放框架层业务代码才不会被细节淹没。3.4 前后端分离后MVC 的职责被重新划了界前后端分离之前后端框架既管接口又管页面模板MVC 是后端内部的事。分离之后后端变成纯 API 提供方只负责资源和业务规则前端接管视图渲染、交互状态、路由等于把“View 部分 Controller”整体搬到了浏览器端。这时候接口就是前后端之间唯一的“契约”。前端工程里值得单独建一个 api 封装层统一配置 baseURL、自动携带 token、统一解析错误码和业务码、统一处理 loading 和超时。这个 api 层相当于前端的“Controller 入口”业务组件不直接碰 axios 细节。这样做的收益很直接后端接口调整时只需修改 api 层组件里永远调 this.api.getOrder(id) 而不是拼一堆 URL。4. 前后端分离项目里的真实工程问题传参、跨域、部署与防重4.1 跨域问题先搞清楚为什么会跨域前后端分离后前端跑在 localhost:5173后端跑在 8080这就产生了“协议、域名、端口任一不同”的跨域。浏览器的同源策略是为了安全但确实挡住了正常请求。开发环境最省事的做法是让 Vite 或 Webpack 配一个 proxy把 /api 转发给后端这本质上不是浏览器发的请求而是 dev server 代理的所以不触发 CORS。生产环境我更推荐 Nginx 反向代理把前端静态文件和后端接口放在同一个域名下面比如 /api 统一转发到后端服务。这样浏览器只访问一个源既解决跨域又避免暴露内网地址。配置很简单server { listen 80; location / { root /home/app/dist; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } }如果一定要用 CORS 解决后端要正确返回 Access-Control-Allow-Origin并且处理 OPTIONS 预检请求。Spring Boot 里可以加一个 CorsFilterDjango 用 django-cors-headers。CORS 的坑在于带 Cookie 时Allow-Origin 不能是 *而且前端要配置 withCredentials看起来小事一桩排查起来一晚上就没了。4.2 前端传参和后端接收的“接口对账”前后端联调大部分时间花在参数对不上。前端传的是 query、path 还是 body后端接收注解必须一一对上RequestParam 收 query 参数PathVariable 收路径参数RequestBody 收 JSON body。传错位置最常见的表现就是某个参数永远接不到或者报 400/415。除了位置还有几个高频坑日期格式不一致前端传 2026-03-01 10:00:00后端 LocalDateTime 没配格式字段命名不一致前端 camelCase后端 snake_case数组参数需要逗号分隔或者重复 key。我的建议是项目里统一约定接口契约文档或者用 OpenAPI/Swagger 生成前端拿着文档写 api 层后端拿着文档写 DTO能避免大量无意义的“我以为你传的是...”。4.3 按钮重复提交前后端各管一段“保存订单”按钮点了十下数据库里出现十笔订单这是前后端都逃不掉的问题。前端要做的很简单点击后按钮置灰或进入 loading同时用状态标志位防止并发点击。但这只是用户体验层面的优化接口仍然是敞开的绕过前端直接调接口依然可以重复提交。真正的防线必须放在后端。后端常见的做法有四种数据库唯一约束比如订单号、业务流水号幂等性设计同一个请求 key 只处理一次Redis 分布式锁按用户业务类型加锁锁释放后再允许下一次以及每次都向后端申请一个一次性 Token提交时带上并校验。推荐组合是“唯一约束 Token 幂等”前者兜底后者防重。这个问题的本质是让“重复提交”在服务端变成无害操作而不是靠前端自觉。4.4 前端大文件上传Worker 什么时候上场上传一个几十 MB 甚至几个 GB 的文件如果直接一次性 POST后端容易超时、内存爆炸前端进度条也会卡死。常见的方案是分片上传把文件切成若干片一片一片传后端再合并。分片的关键有两个一是切片的 hash 要稳定方便断点续传二是计算 hash 不要阻塞 UI。大文件算 hash 是 CPU 密集型操作搁在主线程会白屏卡顿。实际项目里可以用 Web Worker 干活在主线程里把 File 对象交给 WorkerWorker 里用 crypto API 或 SparkMD5 计算整个文件的 hash同时按固定大小切片计算过程不影响界面滚动和点击worker 算完再 postMessage 把结果传回主线程再由主线程逐片上传。前端负责体验后端提供 /upload/chunk 和 /upload/merge 两个接口就够了。4.5 部署前后端怎么在同一台服务器上“同居”前端打包出来是纯静态文件后端是独立服务两者可以分开部署也可以合体。“数据不占用本地空间”这类需求本质就是把数据库和文件存储放到服务器上本地只保留源码不跑数据库不写本地磁盘。最经典的前后端分离部署形态服务器装 Nginx 放前端 dist后端用 Tomcat 或直接 java -jar 起一个 Spring Boot 服务Nginx 把 /api 反向代理过去。如果想省一个进程也可以把前端 dist 拷贝到 Spring Boot 的 src/main/resources/static 下重新打包成单一 jar访问时由后端直接返回静态资源。这种方式适合小项目、内网工具但对大型应用不友好前端每次发版都要动后端缓存策略也不好做。5. 常见问题与排查技巧实录5.1 “这个 Controller 太胖了”一次重构实录见过很多项目里 Controller 写了三四百行又是校验、又是拼 SQL、又是发邮件。我一般会先按动作拆分参数解析留在 Controller数据校验抽成独立的校验器业务处理下沉 Service把发邮件、记录日志、调外部接口这些副作用抽成单独的服务。一次真实的订单创建重构最后 Controller 从 400 行降到 40 行Service 变成三个类每个类不超过 150 行测试反而更好写了。重构的方法论无非一句话看到 Controller 在干不属于“接线员”的活就搬出去。5.2 2026 面试高频考点速查表MVC 几乎是前端和后端面试的共同题库。这里整理几个最高频的问题和回答要点问题期望回答要点MVC 和 MVVM 的区别MVC 的 View 更新依赖手动/事件MVVM 通过 ViewModel 和绑定自动同步重点讲数据驱动三层架构和 MVC 是什么关系三层是系统分层MVC 是表现层内部组织方式不要混为一谈Spring MVC 的请求流程DispatcherServlet → HandlerMapping → Controller → Service → ViewResolver/ResponseBody前后端分离后 Controller 还存在吗后端 Controller 变成 API 入口前端 View/Controller 职责由组件和状态管理承接什么是贫血模型Model 只放字段没有行为业务逻辑全在 Service属于领域建模不到位的信号前端组件间通信的 MVC 影子props/events 是局部 Controller全局 store 是 Model路由是分发器这些内容不是背题就能拿下的最好自己动手写一个前后端分离的小项目把 MVC→MVVM 的切换过程亲身走一遍比背诵十遍八股文都管用。5.3 几个踩过才记得住的坑第一个坑是事务不生效。同一个类里 this 调用自己的方法时Spring 的 Transactional 会失效因为代理没有经过。必须注入自身代理或者把需要事务的方法放到另一个 Service 类里。第二个坑是 JSON 序列化循环引用。JPA 或 MyBatis 返回的实体经常带关联对象直接作为响应返回时会出现无限递归。解决办法是转 DTO或者用 JsonIgnoreProperties 控制字段不建议在实体上到处加 JsonIgnore那会污染业务数据。第三个坑是时区和日期格式。后端存的是 UTC前端显示的是本地时间两端没约定好就是一天到晚差 8 小时。建议接口全部传带时区的时间字符串或时间戳展示层的时区转换交给前端。第四个坑是权限只做前端。前端按钮级权限只是菜单显隐后端一定也要在 Controller 层用权限注解控制接口访问。前端隐藏按钮是体验优化后端校验才是安全底线。做了这么多年项目我个人最大的体会是MVC 从来不是让人背的三个字母而是团队沟通的共同语言。你不需要在每个项目里都画一个标准四层图但一定要确保每个代码文件都清楚自己属于哪个角色。如果你正打算做一个新项目我的建议是先不去引入复杂框架就用最朴素的 Controller 接请求、Service 写逻辑、前端组件管交互把一条最简单的链路跑通当你开始觉得某个文件“又厚又乱”的时候再回头看看 MVC 的职责边界很多问题的答案其实就藏在这三个字母里。最后再分享一个小技巧每次技术评审时把争议描述成“这是 Controller 的活还是 Model 的活”团队往往几分钟就能达成一致比争论“架构过不过时”有用得多。
返回列表