☰
第一个前后端分离项目复盘:从接口设计到跨域部署避坑指南
2026/10/14 2:41:58 网站建设 项目流程

第一次做前后端分离项目,比我想象中要麻烦得多。我原以为“把前端页面放到一个目录,后端接口写在另一个目录,两边跑起来就行”,结果从接口怎么约定,到跨域报错,再到打包后刷新白屏,几乎每一环都踩了一遍。这篇文章就围绕我这个“第一个前后端分离项目”来做一次完整复盘,把我设计、开发、联调、部署过程中真正有用的经验和教训都写出来。无论你是正在准备第一个完整项目,还是已经做到一半卡在联调阶段,都可以参考这一套思路来避坑。

先说清楚这个项目是什么:我做的是一套设备借用管理系统,功能并不复杂,无非是设备列表展示、新增设备、借用、归还这些基础增删改查。但我刻意用“前后端分离”的方式来做,前端单独跑在一个端口,后端单独跑在另一个端口,两边只通过 HTTP 接口通信。就这套看似简单的流程,让我对 Web 开发的理解从“写页面”提升到了“设计一套完整系统”。

1. 项目整体设计与思路拆解

1.1 前后端分离到底拆的是什么

很多人刚开始接触前后端分离,以为就是把代码目录分成“前端文件夹”和“后端文件夹”,其实这是误解。前后端分离拆的不是目录,而是“关注点”。前端只关心页面怎么渲染、用户操作怎么触发、请求怎么发出去;后端只关心参数校验、业务逻辑、数据库读写。两者之间唯一的沟通方式是接口,这个接口长什么样,决定了两边能不能顺畅协作。

在我这个项目里,前端负责把设备表格、借用表单渲染出来,用户点“借用”按钮时,前端把设备编号和借用信息通过 POST 或 PUT 请求发给后端;后端接收后做校验,判断设备是否空闲,然后更新数据库,最后把结果返回给前端。前端拿到返回结果再决定弹窗提示“借用成功”还是“设备已被借走”。整个过程里,前端不用关心数据到底存在哪张表,后端也不用关心页面弹窗长什么样。

这种分离带来的直接好处是:前端和后端可以独立开发、独立测试、独立部署。如果以后要换一套前端界面,后端接口完全不用动;如果以后要扩展后端功能,只要保证接口返回值不变,前端也不会受影响。对我自己来说,最大的感受是代码逻辑清爽了很多,前端目录里不再出现到处拼 SQL 的“野代码”,后端也不用在 Controller 里写 HTML 标签。

不过也要提醒一点,前后端分离是有学习成本的。它要求你同时理解网络请求、接口设计、数据结构和部署方式,比单纯用模板引擎做服务端渲染要多走几步路。但对新人来说,这个成本非常值得,因为它是现代 Web 开发的主流通用模式。

1.2 项目范围:新手最容易栽在“想做大”上

我见过很多同学做第一个项目时,张嘴就是“做一个电商系统”,然后被购物车、订单状态机、支付回调、库存扣减折磨到放弃。做第一个前后端分离项目,一定要把功能范围控制在“最小可用”水平。

我给自己定的范围是这个清单:

  • 设备列表:分页展示所有设备,字段包括设备名称、编号、状态、借用者、借用时间。
  • 新增设备:通过表单提交新设备信息,后端写入数据库。
  • 借用设备:选择一台空闲设备,填写借用者姓名,将设备状态改为“已借出”。
  • 归还设备:将已借出的设备状态恢复为“空闲”,同时记录归还时间。
  • 登录功能:做一个简化版,输入用户名密码,后端校验后返回一个 token,前端把它存下来,后续请求带上。

这个范围看起来不多,但它已经包含了前端多个页面、多个组件、后端多个接口、数据库两张表(设备表和借用记录表),以及 token 认证这一整套链路。我做完之后最大的感受是:功能刚刚好,既没有简单到学不到东西,也没有复杂到坚持不下去。

如果你还想再加点功能,我建议优先加“搜索”和“筛选”,而不是加什么报表统计、消息通知、权限菜单。搜索可以让接口从单表查询变成带条件的查询,锻炼的机会正好;权限菜单则会把你拖进路由守卫、角色索引这些深水区,不适合第一个项目。

1.3 技术选型:别让框架抢走你练接口思路的时间

我这个项目的前端用的是 Vue 3 配合 Vite 构建工具,后端用的是 Java Spring Boot,数据库用的 MySQL,前端发请求用的是 axios。这套组合是社区教程里最常见的搭配,资料多、案例多,遇到问题基本都能找到现成答案。

选型逻辑其实很简单:Vue 3 的组件化思路清晰,适合用来理解“页面是组件拼出来的”;Spring Boot 配置简单,写 REST 接口非常方便,天然适合练习前后端分离;MySQL 是关系型数据库,能锻炼表结构设计能力。就算你换成 React + Node.js、或者 Vue 2 + Express,思路也是一样的,前后端分离的核心不是某一个框架,而是“接口契约”和“数据流”的清晰定义。

这里要刻意提醒一句:不要在第一项目里追新。新框架、新构建工具、新技术往往文档不全、社区踩坑记录少。你希望学到的是通用能力,比如如何设计接口、如何处理跨域、如何排错定位,这些能力在所有技术栈里都通用。框架只是载体,选一个稳定的、你会用得顺手的就行。

2. 核心细节解析与实操要点

2.1 数据库表设计:三个字段帮我躲过了重构

第一个项目的表结构不用很复杂,但基础设计一定要对。我的设备表长这样:

字段名类型说明
idbigint主键,自增
namevarchar(64)设备名称
codevarchar(32)设备编号,唯一索引
statustinyint状态:1空闲,2已借出
borrowervarchar(32)借用者姓名,空闲时为空
borrow_timedatetime借用时间,空闲时为空
created_atdatetime创建时间

有同学会问,status 直接用字符串“空闲”“借出”不就行了吗?我当时也是这样想的,但后来发现,字符串状态一长会带来两类问题,一类是前端显示可能不一致,另一类是后端判断可能出现未知值。用 tinyint 配合枚举值来管理,数据和显示就彻底分开了。比如前端拿到 status 为 1,就通过配置好的字典映射成“空闲”,如果后端以后要加一个“维修中”状态,直接在枚举里加一个 3,前端同步配置一下就行,不用动表结构。

还有一个容易被忽略的点:借用记录不要只记一个“borrower”字段,因为如果你想知道某个设备被借过几次、每次谁借的,设备表里只有一个最新借用人是不够的。所以我加了一张借用记录表,每次借用和归还都累加一条记录。设备表里的 borrower 字段只是“最新的状态快照”,真正完整的历史都在记录表里。这个设计我一开始没想通,后来模拟了几次数据,才意识到关联表的必要性。

2.2 后端接口设计:统一返回结构是前端的“救命稻草”

接口写法看似自由,但如果不统一,前端每一步都要特判,项目很快就会失控。我把所有接口的返回格式统一成下面这样:

{ "code": 0, "message": "success", "data": { ... } }

code 为 0 表示成功,非 0 表示业务上的异常,比如“设备不存在”“设备已被借用”。message 是人类可读的描述,data 才是真正要展示给前端的业务数据。这个结构看起来简单,但价值非常大:前端可以在 axios 拦截器里统一处理非 0 状态码,弹出错误提示,不需要每个页面都写一遍错误判断。

接口路径也统一遵循 REST 风格:

  • GET /api/equipment:分页获取设备列表。
  • POST /api/equipment:新增设备。
  • PUT /api/equipment/{id}/borrow:借用设备。
  • PUT /api/equipment/{id}/return:归还设备。

用 HTTP 语义来表达操作,接口路径清晰,调用方一眼就能看出意图。我见过一些项目把接口写成一堆 RPC 风格,比如 /api/queryEquipmentList、/api/addEquipment,虽然也能工作,但路径一多就杂乱得很了。

还有一个细节:接口返回的 data 里,时间字段建议直接用 ISO 8601 字符串格式,比如 “2025-03-12 14:30:00”,前端拿到后直接展示,不用做二次解析。最开始我后端返回的是时间戳,前端显示还得格式化,白白多了一层转换逻辑。

2.3 前端目录结构:把接口层单独拎出来

前端代码不是随便塞几个文件就行的。我的目录结构是这样:

src/ api/ // 所有接口请求 request.js // axios 实例封装 equipment.js // 设备相关接口 components/ // 通用组件 views/ // 页面 EquipmentList.vue AddEquipment.vue router/ // 路由配置 store/ // 全局状态

关键点是把 api 目录单独拆出来,每个接口都在一个文件里管理。这样做的原因是:如果后端地址变了,或者需要对请求头做统一处理,只需要改 request.js 一个文件;如果设备模块要加一个新接口,只需要在 equipment.js 里加一个函数。页面组件里不会出现几十行 axios 配置,维护起来特别舒服。

很多教程会把 axios 请求直接写在组件里,demo 看起来没问题,但项目一变大就不好收拾。我第一次做完深有感触:当组件里只剩业务逻辑时,阅读和维护效率完全不在一个层次。

3. 实操过程与核心环节实现

3.1 第一步:先搭后端,把第一个接口跑起来

后端工程我是用 Spring Boot 初始化器生成的,依赖选择 Web、MySQL 驱动和 MyBatis。项目结构大致是:

src/main/java/com/example/equipment/ controller/ // 接收请求 service/ // 业务逻辑 mapper/ // 数据库操作 entity/ // 实体类

第一步不是急着写增删改查,而是先写一个最简单的 HealthCheck 接口,确认网络链路是通的。我建了一个 HealthController:

@RestController @RequestMapping("/api/health") public class HealthController { @GetMapping public Result<String> health() { return Result.success("OK"); } }

然后用 Postman 访问 localhost:8080/api/health,看到返回的 JSON 之后,再开始写业务接口。很多新人喜欢直接把所有代码写完再一起测试,一旦报错就很难判断问题出现在哪个环节。先打通最小链路,再逐步扩展,这种加一点测一点的习惯,能省下大把的排查时间。

写设备列表接口的时候,我直接用 MyBatis 写注解 SQL。分页逻辑没有引入复杂的分页插件,而是自己用 LIMIT 实现。前端传 pageNum 和 pageSize 两个参数,后端返回总条数 total 和当前页数据列表 records。返回格式是这样:

{ "code": 0, "message": "success", "data": { "total": 23, "records": [...] } }

分页逻辑虽然很简单,但它逼着我去思考“接口到底应该返回什么”。如果直接把数据库所有记录返回给前端,再让前端做按钮分页,数据量大一点就会卡顿。接口层面做分页,才是前后端分离项目里更合理的设计。

3.2 第二步:搭前端页面,先写静态再接数据

前端开发我建议遵循这样一个流程:先用假数据把页面渲染出来,确保页面和交互都没问题了,再替换成真实接口。这样做的好处是,排查问题时能明确区分“页面问题”和“接口问题”,不会混在一起互相干扰。

我用 Vite 创建项目:

npm create vite@latest frontend -- --template vue cd frontend npm install axios vue-router

设备列表页接真实接口前,我先写了一个 mockRecords 数组放在组件里,把表格列渲染、分页按钮、借用弹窗这些 UI 全部跑通。等 UI 没问题了,我再引入 api/equipment.js 里的函数,把假数据接口替换成真实数据。

axios 实例的封装也很关键。我在 request.js 里做了三件事:设置 baseURL、超时时间、拦截器。拦截器里,当后端返回 code 非 0 时,自动弹出错误提示;当 HTTP 状态为 401 时,清掉本地的 token 跳回登录页。代码大概是这样:

import axios from 'axios'; const request = axios.create({ baseURL: '/api', timeout: 10000 }); request.interceptors.response.use( (response) => { const res = response.data; if (res.code !== 0) { alert(res.message); return Promise.reject(new Error(res.message)); } return res; }, (error) => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(error); } ); export default request;

这里 baseURL 我写的是 /api,而不是完整的后端地址,这个细节后面在跨域和部署部分还会展开。它巧妙的地方在于:开发环境可以通过代理转发到后端,生产环境可以由 Nginx 直接反代到后端,前端代码完全不用改。

3.3 第三步:联调时最重要的一个步骤

当前后端代码都写完后,就要开始联调了。很多新手第一次联调时,会同时打开前端页面和后端接口,然后对着屏幕发呆,不知道从哪里开始验证。

我把联调流程固定为三步。第一步,用 Postman 或浏览器直接调后端的每个接口,确认接口本身没问题;第二步,打开前端页面,打开浏览器开发者工具里的 Network 面板,切到页面操作一遍,观察请求有没有发出,返回值是什么;第三步,如果某个功能报错,先看 Network 里那一行请求的状态码和响应体,再从前端代码往后端排查。

这个顺序很重要。我碰到过一种情况,前端点击“新增设备”按钮没有任何反应,打开 Network 面板发现请求根本没有发出,原来是一个字段名写错了,导致逻辑提前 return 了。如果不看 Network 面板,我可能要去后端打断点找半天,白白浪费很多时间。

4. 常见问题与排查技巧实录

4.1 跨域报错:比预期来得更快

前后端分离项目里,跨域基本是人人都会遇到的第一道坎。现象就是,前端运行在 localhost:5173,后端运行在 localhost:8080,前端发起请求时,浏览器控制台报错:

Access to XMLHttpRequest at 'http://localhost:8080/api/equipment' from origin 'http://localhost:5173' has been blocked by CORS policy

原因并不复杂:浏览器的同源策略规定,一个源的页面默认不能向另一个源发异步请求。判断“源”是否相同,要看协议、域名、端口三者是否一致。前端和后端端口不同,就属于跨域。

跨域解决办法有很多种。我一开始在 Spring Boot 后端直接加了一个全局的 CORS 配置,允许所有来源访问,这样开发环境确实不报错了。但后来我发现更好的方式是在前端 Vite 里配置代理,把 /api 开头的请求转发到后端:

// vite.config.js export default { server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } };

为什么我更推荐代理方案?因为它把跨域问题留在了“开发环境”:后端不需要放开所有来源,生产的接口安全边界更清晰;生产部署时也不需要为 CORS 配置发愁,直接用 Nginx 反代把前后端打到同一域下,浏览器就完全感受不到跨域存在了。相比之下,后端的全局 CORS 配置在生产环境往往要花更多心思去限制允许的域名,新手很容易直接配置成放开一切,留下了安全隐患。

4.2 路由刷新 404:部署后才踩到的坑

开发环境一切正常,部署到 Nginx 之后,发现从首页点击进入子页面没问题,但只要一刷新,比如直接访问 /equipment/add,就会出现 404。

这个问题的根源在于:前端用 Vue Router 的 history 模式时,路由地址并不是真实文件路径。浏览器刷新 /equipment/add,会请求 Nginx 的 /equipment/add 路径,但服务器上根本没有这个目录或文件,所以它默认返回了 404。

解决办法是在 Nginx 配置里加一个 try_files,让所有不存在的路径都回退到 index.html,由前端路由去接管:

location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }

加了这行之后,再刷新子页面,服务器会返回 index.html,前端路由在浏览器端根据 URL 渲染对应页面,问题就消失了。顺带提一句:如果你用的是 history 模式,记得注意后端接口路径不要和前端路由冲突。我习惯把所有接口统一放在 /api 前缀下,就是为了避免这种干扰。

4.3 排查方法与避坑技巧速查表

做完这个项目,我把遇到的典型问题整理成了一张表,后面再写别的项目时也一直在用。这里分享给你:

问题现象可能原因解决思路
前端请求报 CORS 错误端口或域名不同开发环境用代理转发,生产用 Nginx 反代到同域
刷新子页面 404history 路由访问到真实不存在的路径Nginx 配置 try_files 回退 index.html
接口报 401请求头里没带 token在 axios 请求拦截器统一设置 Authorization
接口返回 500后端数据库字段和实体类不一致从 StackTrace 倒着往上找出第一处业务代码报错
请求发出但 Network 里看不到前端代码里请求没有真正执行打断点或加 console.log,检查是否存在字段拼写错误
数据库时间比本机慢了 8 小时时区配置问题在 JDBC 连接串中配置 serverTimezone 参数
POST 请求后端一直收不到参数请求体格式与后端接收类型不匹配确认 JSON、Content-Type 与 @RequestBody 是否对应
接口明明正常但页面数据不刷新状态数据未被重新赋值确认给 ref 赋值时是 .value 而不是直接覆盖

比如时间慢 8 小时这个问题,我在第一次做后端接口时遇到过。数据库中存了一条借用时间,前端显示出来比本地时间少了 8 个小时。排查之后发现,MySQL 连接串里没有配置 serverTimezone,连接默认用了“服务器零时区”,而本地是中国时区,所以正好差 8 小时。加上参数后问题消失。这种问题靠“盯着代码看”很难发现,一定要学会看请求响应体里的原始数据,对比是否存在转换差异。

另外一个排查小技巧是:当“前端报错、后端也报错”时,先打开浏览器开发者工具的 Network 面板,看接口真实的请求地址、请求头、请求体和响应体。比如 POST 请求最常见的错误是后端 @RequestBody 接收的 JSON 字段名和前端传的不一样,这种问题用 Network 面板一眼就能看出来,根本不用前端和后端互相争论。

还有一个细节:新增设备时,我一开始用 GET 请求,把表单参数直接拼在 URL 上,结果遇到中文参数乱码。后来我全部改成 POST 请求 + JSON 请求体,才从根上解决。这个改动不只是为了规避乱码,更重要的原因是 GET 有 URL 长度限制,而且把表单数据放在 URL 里既不安全,也不符合 REST 的习惯。

4.4 关于 token 认证的一个小提醒

这个项目做了简化版登录,所以 token 的传递是我特别重视的一环。前端在登录成功后把返回的 token 存到 localStorage,然后每次请求在拦截器里注入:

request.interceptors.request.use((config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; });

后端如果发现 token 无效,就返回 401。我在后端加了一个简单的拦截器,对 /api/equipment 这个路径做统一校验。这样做之后,我才真正理解了“前端只管把凭证带上,后端负责鉴权”这句话。这个过程中要留心:不要把 token 存到会话里,页面一刷新就丢了;也不要把 token 暴露在前端源码里,因为任何人都能看到。token 的完整权限体系是另一个大话题,第一个项目做到能带能验、过期能跳转,就足够了。

做完这个第一个前后端分离项目,我最大的体会是,“分离”带来的不是代码位置的调整,而是思维方式的转变。前期花最多的时间在定接口和设计数据结构,这块一旦定清楚了,后面写代码其实非常快。如果让我重新做一次,我会在动手之前先画一张接口清单表格,把每个接口的路径、方法、参数、返回结构都写出来,再开始敲代码。最后想起一个特别实用的小经验:遇到“前端报错、后端也报错”这种互相推诿的情况,先看浏览器的 Network 面板,找到接口真实的请求和响应,90% 的问题都能在这里定位。这个习惯我从这个项目开始用,一直用到现在,几乎变成了判断一个 Web 开发者排查思路是否成熟的分水岭。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询