前后端分离: 开发模式、协作流程与工程结构拆分
纲要
前后端分离开发,解决当前工程的一个结构性问题:
- 现状问题 :
reggie_take_out一个Maven工程里既有Java代码,又有backend/front两个目录的前端页面 - 四个痛点:分工不明确、开发效率低、不便于管理、招聘困难
- 分离后的变化 :工程拆成前端工程与后端工程;部署时后端打
jar上Tomcat,前端打包上Nginx - 协作流程:定制接口 → 并行开发 → 前后端自测 → 联调 → 提测
- 什么是"接口" :一个
HTTP请求的完整约定(地址、方法、参数、响应) - 前端技术栈 :
VSCode/HBuilder、NodeJS、Vue、ElementUI、Mock、Webpack
#mermaid-svg-qom9rTMfawkKXbvB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qom9rTMfawkKXbvB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qom9rTMfawkKXbvB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qom9rTMfawkKXbvB .error-icon{fill:#552222;}#mermaid-svg-qom9rTMfawkKXbvB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qom9rTMfawkKXbvB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qom9rTMfawkKXbvB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qom9rTMfawkKXbvB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qom9rTMfawkKXbvB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qom9rTMfawkKXbvB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qom9rTMfawkKXbvB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qom9rTMfawkKXbvB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qom9rTMfawkKXbvB .marker.cross{stroke:#333333;}#mermaid-svg-qom9rTMfawkKXbvB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qom9rTMfawkKXbvB p{margin:0;}#mermaid-svg-qom9rTMfawkKXbvB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qom9rTMfawkKXbvB .cluster-label text{fill:#333;}#mermaid-svg-qom9rTMfawkKXbvB .cluster-label span{color:#333;}#mermaid-svg-qom9rTMfawkKXbvB .cluster-label span p{background-color:transparent;}#mermaid-svg-qom9rTMfawkKXbvB .label text,#mermaid-svg-qom9rTMfawkKXbvB span{fill:#333;color:#333;}#mermaid-svg-qom9rTMfawkKXbvB .node rect,#mermaid-svg-qom9rTMfawkKXbvB .node circle,#mermaid-svg-qom9rTMfawkKXbvB .node ellipse,#mermaid-svg-qom9rTMfawkKXbvB .node polygon,#mermaid-svg-qom9rTMfawkKXbvB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qom9rTMfawkKXbvB .rough-node .label text,#mermaid-svg-qom9rTMfawkKXbvB .node .label text,#mermaid-svg-qom9rTMfawkKXbvB .image-shape .label,#mermaid-svg-qom9rTMfawkKXbvB .icon-shape .label{text-anchor:middle;}#mermaid-svg-qom9rTMfawkKXbvB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qom9rTMfawkKXbvB .rough-node .label,#mermaid-svg-qom9rTMfawkKXbvB .node .label,#mermaid-svg-qom9rTMfawkKXbvB .image-shape .label,#mermaid-svg-qom9rTMfawkKXbvB .icon-shape .label{text-align:center;}#mermaid-svg-qom9rTMfawkKXbvB .node.clickable{cursor:pointer;}#mermaid-svg-qom9rTMfawkKXbvB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qom9rTMfawkKXbvB .arrowheadPath{fill:#333333;}#mermaid-svg-qom9rTMfawkKXbvB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qom9rTMfawkKXbvB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qom9rTMfawkKXbvB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qom9rTMfawkKXbvB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qom9rTMfawkKXbvB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qom9rTMfawkKXbvB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qom9rTMfawkKXbvB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qom9rTMfawkKXbvB .cluster text{fill:#333;}#mermaid-svg-qom9rTMfawkKXbvB .cluster span{color:#333;}#mermaid-svg-qom9rTMfawkKXbvB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qom9rTMfawkKXbvB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qom9rTMfawkKXbvB rect.text{fill:none;stroke-width:0;}#mermaid-svg-qom9rTMfawkKXbvB .icon-shape,#mermaid-svg-qom9rTMfawkKXbvB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qom9rTMfawkKXbvB .icon-shape p,#mermaid-svg-qom9rTMfawkKXbvB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qom9rTMfawkKXbvB .icon-shape .label rect,#mermaid-svg-qom9rTMfawkKXbvB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qom9rTMfawkKXbvB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qom9rTMfawkKXbvB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qom9rTMfawkKXbvB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
定制接口
★ 最关键环节
并行开发
后端自测
Postman / Swagger
前端自测
Mock 模拟数据
前后端联调
校验数据格式
格式是否符合约定?
提交测试
一、当前工程的问题
现状
打开 reggie_take_out 工程,目录构成是:
txt
reggie_take_out/
├── src/main/java/ # 后端 Java 代码
├── src/main/resources/
│ ├── application.yml
│ ├── mapper/
│ ├── static/ # 静态资源
│ │ ├── backend/ # 后台管理页面(HTML/JS/CSS)
│ │ └── front/ # 移动端页面
│ └── templates/
└── pom.xml
src/main/resources/static 下的 backend 与 front 两个目录,装着全部前端代码------HTML、JS、CSS、图片。
前后端代码混合在同一个 Maven 工程里。
四个痛点
分工不明确 。同一个开发人员既要写 Java 后端,又要写 HTML/JS 前端。职责边界模糊,出了问题容易互相甩锅。
开发效率低。一会儿写前端,一会儿写后端,上下文频繁切换。人的大脑切换任务是有成本的,来回跳转会显著降低效率。
不便于管理 。很多时候只改了前端的一个按钮样式,后端一行没动,却必须打开整个 Maven 工程,重新打包部署整个应用。一个小改动引发一次全量发布,这在小版本中尤其浪费。
招聘困难 。要求一个人既精通 Java 生态(Spring Boot、MyBatis、Redis、MySQL)又精通前端(Vue、Webpack、CSS),这样的人极少,即便有也成本高昂。
混合开发的其他问题
补充课程未展开但实践中同样突出的几点:
- 构建耦合 :前端改一个字也要跑一遍
Maven打包,构建时间从秒级变成分钟级 - 无法独立扩缩容 :前端是静态资源,本该由
Nginx高效处理,现在却要占用Tomcat的连接与JVM内存 - 技术栈升级困难:想换前端框架就得动后端工程,风险外溢
二、前后端分离开发
定义
前端代码交给专门的前端开发人员负责,后端代码交给专门的后端开发人员负责。分工明确,各司其职。
三个收益
提高效率------让合适的人做擅长的事。前端人员专注交互与体验,后端人员专注业务与数据。
可以并行开发 ------这是最大的收益。接口定好后,前端与后端同时开工,而不是前端等后端接口写完才能动。项目周期显著缩短。
便于管理与维护------两个工程独立版本控制、独立构建、独立部署。
工程结构的变化
分离后从"一个 Maven 工程"拆成两个工程:
txt
拆分前: 拆分后:
reggie_take_out/ reggie-backend/ reggie-front/
├── src/main/java/ ├── src/main/java/ ├── public/
├── src/main/resources/ ├── src/main/resources/ ├── src/
│ ├── static/ │ ├── application.yml │ ├── api/
│ │ ├── backend/ ← 前端 │ ├── mapper/ │ ├── views/
│ │ └── front/ ← 前端 │ └── static/(已清空) │ ├── router/
│ └── templates/ └── pom.xml │ └── utils/
└── pom.xml ├── package.json
└── vue.config.js
后端工程 :只有 Java 代码与配置文件,不再包含任何 HTML/CSS/JS。
前端工程 :只有前端代码,没有任何 Java。
部署方式的变化
这是容易被忽略但非常重要的一点:
#mermaid-svg-YJPSoakIBxqqpray{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YJPSoakIBxqqpray .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YJPSoakIBxqqpray .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YJPSoakIBxqqpray .error-icon{fill:#552222;}#mermaid-svg-YJPSoakIBxqqpray .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YJPSoakIBxqqpray .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YJPSoakIBxqqpray .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YJPSoakIBxqqpray .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YJPSoakIBxqqpray .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YJPSoakIBxqqpray .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YJPSoakIBxqqpray .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YJPSoakIBxqqpray .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YJPSoakIBxqqpray .marker.cross{stroke:#333333;}#mermaid-svg-YJPSoakIBxqqpray svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YJPSoakIBxqqpray p{margin:0;}#mermaid-svg-YJPSoakIBxqqpray .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YJPSoakIBxqqpray .cluster-label text{fill:#333;}#mermaid-svg-YJPSoakIBxqqpray .cluster-label span{color:#333;}#mermaid-svg-YJPSoakIBxqqpray .cluster-label span p{background-color:transparent;}#mermaid-svg-YJPSoakIBxqqpray .label text,#mermaid-svg-YJPSoakIBxqqpray span{fill:#333;color:#333;}#mermaid-svg-YJPSoakIBxqqpray .node rect,#mermaid-svg-YJPSoakIBxqqpray .node circle,#mermaid-svg-YJPSoakIBxqqpray .node ellipse,#mermaid-svg-YJPSoakIBxqqpray .node polygon,#mermaid-svg-YJPSoakIBxqqpray .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YJPSoakIBxqqpray .rough-node .label text,#mermaid-svg-YJPSoakIBxqqpray .node .label text,#mermaid-svg-YJPSoakIBxqqpray .image-shape .label,#mermaid-svg-YJPSoakIBxqqpray .icon-shape .label{text-anchor:middle;}#mermaid-svg-YJPSoakIBxqqpray .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YJPSoakIBxqqpray .rough-node .label,#mermaid-svg-YJPSoakIBxqqpray .node .label,#mermaid-svg-YJPSoakIBxqqpray .image-shape .label,#mermaid-svg-YJPSoakIBxqqpray .icon-shape .label{text-align:center;}#mermaid-svg-YJPSoakIBxqqpray .node.clickable{cursor:pointer;}#mermaid-svg-YJPSoakIBxqqpray .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YJPSoakIBxqqpray .arrowheadPath{fill:#333333;}#mermaid-svg-YJPSoakIBxqqpray .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YJPSoakIBxqqpray .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YJPSoakIBxqqpray .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YJPSoakIBxqqpray .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YJPSoakIBxqqpray .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YJPSoakIBxqqpray .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YJPSoakIBxqqpray .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YJPSoakIBxqqpray .cluster text{fill:#333;}#mermaid-svg-YJPSoakIBxqqpray .cluster span{color:#333;}#mermaid-svg-YJPSoakIBxqqpray div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YJPSoakIBxqqpray .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YJPSoakIBxqqpray rect.text{fill:none;stroke-width:0;}#mermaid-svg-YJPSoakIBxqqpray .icon-shape,#mermaid-svg-YJPSoakIBxqqpray .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YJPSoakIBxqqpray .icon-shape p,#mermaid-svg-YJPSoakIBxqqpray .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YJPSoakIBxqqpray .icon-shape .label rect,#mermaid-svg-YJPSoakIBxqqpray .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YJPSoakIBxqqpray .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YJPSoakIBxqqpray .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YJPSoakIBxqqpray :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 分离前
Maven 工程
前后端混合
打成一个 jar
部署到 Tomcat
Tomcat 同时处理
静态资源 + 动态请求
#mermaid-svg-RyPHa0SF0drQ0fz8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RyPHa0SF0drQ0fz8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RyPHa0SF0drQ0fz8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RyPHa0SF0drQ0fz8 .error-icon{fill:#552222;}#mermaid-svg-RyPHa0SF0drQ0fz8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RyPHa0SF0drQ0fz8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RyPHa0SF0drQ0fz8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RyPHa0SF0drQ0fz8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RyPHa0SF0drQ0fz8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RyPHa0SF0drQ0fz8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RyPHa0SF0drQ0fz8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RyPHa0SF0drQ0fz8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RyPHa0SF0drQ0fz8 .marker.cross{stroke:#333333;}#mermaid-svg-RyPHa0SF0drQ0fz8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RyPHa0SF0drQ0fz8 p{margin:0;}#mermaid-svg-RyPHa0SF0drQ0fz8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RyPHa0SF0drQ0fz8 .cluster-label text{fill:#333;}#mermaid-svg-RyPHa0SF0drQ0fz8 .cluster-label span{color:#333;}#mermaid-svg-RyPHa0SF0drQ0fz8 .cluster-label span p{background-color:transparent;}#mermaid-svg-RyPHa0SF0drQ0fz8 .label text,#mermaid-svg-RyPHa0SF0drQ0fz8 span{fill:#333;color:#333;}#mermaid-svg-RyPHa0SF0drQ0fz8 .node rect,#mermaid-svg-RyPHa0SF0drQ0fz8 .node circle,#mermaid-svg-RyPHa0SF0drQ0fz8 .node ellipse,#mermaid-svg-RyPHa0SF0drQ0fz8 .node polygon,#mermaid-svg-RyPHa0SF0drQ0fz8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RyPHa0SF0drQ0fz8 .rough-node .label text,#mermaid-svg-RyPHa0SF0drQ0fz8 .node .label text,#mermaid-svg-RyPHa0SF0drQ0fz8 .image-shape .label,#mermaid-svg-RyPHa0SF0drQ0fz8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-RyPHa0SF0drQ0fz8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RyPHa0SF0drQ0fz8 .rough-node .label,#mermaid-svg-RyPHa0SF0drQ0fz8 .node .label,#mermaid-svg-RyPHa0SF0drQ0fz8 .image-shape .label,#mermaid-svg-RyPHa0SF0drQ0fz8 .icon-shape .label{text-align:center;}#mermaid-svg-RyPHa0SF0drQ0fz8 .node.clickable{cursor:pointer;}#mermaid-svg-RyPHa0SF0drQ0fz8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RyPHa0SF0drQ0fz8 .arrowheadPath{fill:#333333;}#mermaid-svg-RyPHa0SF0drQ0fz8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RyPHa0SF0drQ0fz8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RyPHa0SF0drQ0fz8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RyPHa0SF0drQ0fz8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RyPHa0SF0drQ0fz8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RyPHa0SF0drQ0fz8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RyPHa0SF0drQ0fz8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RyPHa0SF0drQ0fz8 .cluster text{fill:#333;}#mermaid-svg-RyPHa0SF0drQ0fz8 .cluster span{color:#333;}#mermaid-svg-RyPHa0SF0drQ0fz8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RyPHa0SF0drQ0fz8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RyPHa0SF0drQ0fz8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-RyPHa0SF0drQ0fz8 .icon-shape,#mermaid-svg-RyPHa0SF0drQ0fz8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RyPHa0SF0drQ0fz8 .icon-shape p,#mermaid-svg-RyPHa0SF0drQ0fz8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RyPHa0SF0drQ0fz8 .icon-shape .label rect,#mermaid-svg-RyPHa0SF0drQ0fz8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RyPHa0SF0drQ0fz8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RyPHa0SF0drQ0fz8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RyPHa0SF0drQ0fz8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 分离后
后端工程
打包成 jar
前端工程
Webpack 打包
dist 目录
部署到 Tomcat/Spring Boot
部署到 Nginx
只处理动态请求
只处理静态资源
后端代码打包部署到 Tomcat 服务器 (或 Spring Boot 内置容器);前端工程打包后部署到 Nginx。
前端产物(HTML/CSS/JS)本质就是静态资源,而 Nginx 处理静态资源的能力远强于 Tomcat(sendfile 零拷贝、epoll 高并发、不占 JVM)。这正是上一篇 Nginx 部署静态资源要解决的问题。
注意 :分离后前端请求后端会涉及跨域问题,需要配 CORS 或 Nginx 反向代理。这是下一篇 Swagger 与后续部署章节要处理的。
三、协作流程
完整流程
分离后最核心的问题是:前后端人员如何配合。标准流程五步:
第一步:定制接口
前后端坐下来(或通过接口管理平台)约定每个功能的请求细节。
第二步:并行开发
双方按约定各自写代码,互不阻塞。
第三步:各自自测
- 后端用
Postman、Swagger测试自己的接口 - 前端用
Mock造模拟数据验证页面渲染
第四步:联调
前端请求真正打到后端,主要校验数据格式是否符合约定。
第五步:提测
交给专门的测试人员做完整验证。
为什么"定制接口"是关键
课程里特别强调了这个环节------如果接口没定义好就开始开发,很可能联调时对接不上:请求地址不对、参数不符合要求,导致返工。
联调阶段的典型事故:约定返回 JSON,后端实际返回了 XML 或纯文本,前端完全无法解析。这类问题在接口约定清晰时根本不会发生。
接口的具体含义
这里的"接口"不是 Java 的 interface ,而是一个 HTTP 请求的完整约定,包括:
| 要素 | 说明 | 示例 |
|---|---|---|
| 接口名称 | 功能描述 | 新增用户 |
| 请求方法 | GET/POST/PUT/DELETE |
POST |
| 请求路径 | URL 路径 |
/user/add |
| 请求头 | Header 要求 |
Content-Type: application/json |
| 请求参数 | 请求体或 Query 参数 |
name(String)、age(Integer) |
| 参数约束 | 是否必填、类型、示例 | name 必填,示例"张三" |
| 响应数据 | 返回结构与示例 | {"code":1,"message":"ok"} |
完整接口示例
新增用户接口:
txt
接口名称:新增用户
请求方式:POST
请求路径:/user/add
请求头:
Content-Type: application/json
请求体(application/json):
{
"name": "张三", // String,必填
"age": 25 // Integer,必填
}
响应数据(application/json):
{
"code": 1,
"message": "ok"
}
对应到外卖平台的实际接口,例如菜品分页查询:
txt
接口名称:菜品分页查询
请求方式:GET
请求路径:/dish/page
请求参数(Query):
page Integer 必填 示例 1
pageSize Integer 必填 示例 10
name String 非必填 示例 鱼香肉丝
响应数据(application/json):
{
"code": 1,
"msg": null,
"data": {
"records": [
{
"id": "1397849739276890114",
"name": "鱼香肉丝",
"categoryId": "1397844263642378242",
"categoryName": "川菜",
"price": 3800,
"image": "dish-xxx.jpg",
"status": 1
}
],
"total": 24,
"size": 10,
"current": 1,
"pages": 3
},
"map": {}
}
注意 id 是字符串 而非数字------这正是前面"雪花 ID 精度丢失"那一节的结果:JacksonObjectMapper 把 Long 序列化成 String,避免 JS 的 2^53 截断。
这类细节必须在接口文档里写清楚 ,否则前端按数字类型解析 ID 会踩坑。
前端为什么能用 Mock 并行开发
Mock 即模拟数据。前端开发一个分页列表页面时,不需要后端真的就绪,只要按接口约定造一份假数据:
javascript
// mock/dish.js
export default {
'GET /dish/page': {
code: 1,
msg: null,
data: {
records: [
{
id: '1397849739276890114',
name: '鱼香肉丝',
categoryName: '川菜',
price: 3800,
status: 1
},
{
id: '1397849739276890115',
name: '宫保鸡丁',
categoryName: '川菜',
price: 3600,
status: 1
}
],
total: 24,
size: 10,
current: 1,
pages: 3
},
map: {}
}
}
关键前提 :Mock 数据的结构必须与接口约定完全一致 。这样页面在 Mock 下能正常渲染,切到真实后端时因为数据格式一致,自然也能渲染。
这也是为什么接口约定必须先于开发------Mock 数据就是接口约定的第一份可执行实现。
四、前端技术栈
后端工程师不需要写前端代码,但要了解对方用什么,才能高效协作。
开发工具
| 工具 | 说明 |
|---|---|
Visual Studio Code(VSCode) |
目前最主流的前端编辑器,插件生态丰富 |
HBuilder |
国产前端 IDE,对 uni-app 支持好 |
技术框架
| 技术 | 作用 | 后端类比 |
|---|---|---|
NodeJS |
前端运行基石,类似 JDK |
JDK |
Vue |
前端 MVVM 框架 |
Spring MVC |
ElementUI |
UI 组件库(表格、下拉框、菜单) |
Bootstrap / 各种 UI 库 |
Mock |
造模拟数据,支持并行开发 | Postman Mock Server |
Webpack |
前端打包工具 | Maven |
NodeJS 的定位值得一说 :它之于前端就像 JDK 之于 Java。前端的现代构建工具(Webpack、Vite)、开发服务器、包管理器(npm、yarn)全部运行在 NodeJS 之上。但它只用于开发期和构建期 ------最终产物是纯静态文件,运行时不需要 NodeJS。
Webpack 的产物不是 jar/war ,而是一堆经过压缩、混淆、合并的 JS/CSS/HTML,通常输出到 dist/ 目录。这个 dist 目录就是部署到 Nginx 的东西。
前后端技术对照
| 环节 | 前端 | 后端 |
|---|---|---|
| 运行环境 | NodeJS |
JDK |
| 包管理 | npm / yarn |
Maven / Gradle |
| 依赖声明 | package.json |
pom.xml |
| 主框架 | Vue |
Spring Boot |
UI 组件 |
ElementUI |
--- |
| 构建工具 | Webpack |
Maven |
| 构建产物 | dist/(静态文件) |
*.jar |
| 部署位置 | Nginx |
Tomcat / Spring Boot |
| 自测工具 | Mock |
Postman / Swagger |
五、分离后的跨域问题
分离开发带来一个新问题:前端在 Nginx(80 端口),后端在 Tomcat(8080 端口),域名或端口不同即构成跨域,浏览器的同源策略会拦截请求。
解法一:后端配置 CORS
java
package com.itheima.reggie.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨域配置
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
注意 allowCredentials(true) 与 allowedOrigins("*") 不能同时使用 ------CORS 规范规定,允许携带凭证时 Origin 不能用通配符。所以上面用的是 allowedOriginPatterns("*")(Spring 5.3+ 支持),或者显式列出允许的域名。
解法二:Nginx 反向代理(推荐)
让前后端对外同一个域名同一个端口,从根上消除跨域:
nginx
server {
listen 80;
server_name localhost;
# 前端静态资源
location / {
root html;
index index.html;
try_files $uri $uri/ /index.html;
}
# 后端接口,转发到 8080
location /api/ {
proxy_pass http://192.168.138.101:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
前端所有请求都发到 /api/**(同源),由 Nginx 转发到后端。浏览器看到的是同源请求,不存在跨域。
生产推荐这种方案 ------同源不仅避免了 CORS 的复杂配置,还省去了预检请求(OPTIONS)带来的额外往返。
六、接口文档的三种形态
| 形态 | 工具 | 特点 |
|---|---|---|
| 独立管理平台 | YApi |
需自行部署,支持团队协作、Mock、测试 |
| 代码自动生成 | Swagger / Knife4j |
注解驱动,与代码同步,后端友好 |
| 静态文件 | Markdown / HTML |
离线可看,但易与代码脱节 |
下一篇讲 YApi 的具体使用,再下一篇讲 Swagger------后者是后端工程师更常用的方式,因为它用注解写在代码里,代码改了文档自动跟着变。
API 速览
| 概念 / 工具 | 作用 |
|---|---|
接口(API) |
一个 HTTP 请求的完整约定:方法、路径、参数、响应 |
Mock |
模拟数据,让前端在后端未就绪时也能开发 |
NodeJS |
前端构建期运行环境,类比 JDK |
Vue |
前端 MVVM 框架 |
ElementUI |
Vue 的 UI 组件库 |
Webpack |
前端打包工具,产物在 dist/ 目录 |
VSCode / HBuilder |
前端开发 IDE |
CORS |
跨域资源共享,分离部署后必须处理 |
allowedOriginPatterns |
Spring 5.3+ 的跨域来源匹配,可配合 allowCredentials |
try_files |
Nginx 指令,SPA 前端路由回退到 index.html |
官方文档
Vue.js官方文档:https://cn.vuejs.org/Element UI组件库:https://element.eleme.cn/Node.js官网:https://nodejs.org/Webpack文档:https://webpack.docschina.org/MDNCORS说明:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORSSpringCORS支持:https://docs.spring.io/spring-framework/reference/web/webmvc-cors.html
总结
前后端混合开发的本质问题是耦合:分工、构建、部署、扩缩容全绑在一起。分离后前端工程与后端工程独立演进,各自用最合适的工具链。
分离不只是目录拆分,部署方式也变了 :后端打 jar 上 Tomcat/Spring Boot,前端 Webpack 打包成 dist 上 Nginx。前端产物本质是静态资源,交给 Nginx 处理能获得 sendfile 零拷贝和 epoll 高并发的收益,同时不占用宝贵的 JVM 资源。
协作流程中最关键的是"定制接口" 。这里的接口不是 Java 的 interface,而是一个 HTTP 请求的完整约定------方法、路径、请求头、参数(含是否必填与示例)、响应结构与示例。接口定清楚了,前后端才能并行开发;定不清楚,联调阶段必然返工。
前端能并行开发靠的是 Mock ,而 Mock 数据必须与接口约定结构完全一致------所以接口文档实际上是 Mock 数据、前端代码、后端代码三方共同的输入。
分离后出现跨域问题 ,推荐用 Nginx 反向代理让前后端同源(/api/** 转发到后端),比配置 CORS 更彻底:既不用处理预检请求,也避免了 allowCredentials 与通配符 Origin 的冲突。
下一篇讲 YApi------API 管理平台的部署与使用,包括创建项目、添加分类、定义接口、导出与批量导入接口文档。