☰
前后端分离任务看板开发复盘:Flask+Vue3联调实战与踩坑记录
2026/10/14 1:22:08 网站建设 项目流程

这周刚把一个前后端分离的任务看板小系统收尾,正好是WEB应用开发设计实训的第三个实验。写实验报告三的时候我自己算了算,这个项目里踩的坑,比前两个实验加起来都多。实验一还在做纯静态页面,实验二开始接触一点JavaScript交互,到了实验三,突然就要接数据库、搞登录、做接口联调,跨度确实不小。

这篇就当实验报告三的完整复盘——从需求拆解、技术选型、数据库设计、后端接口实现,到前端交互和联调阶段折腾到半夜的几次排错经历,全都拆开讲。如果你也正好在准备类似的Web开发实验,或者刚把Vue那套跑起来但不知道怎么和后端对接,这篇应该能帮你省下不少弯路。

1. 实验三要交付什么:从需求说明到功能清单

1.1 验收标准:不是"能跑"而是"可演示"

实验三的任务书一开始就写得和前面不一样,核心要求是:数据必须落到数据库里,刷新页面之后不能丢。前面两个实验,页面长得对、按钮点击有反馈基本就过关了;从实验三开始,所有的交互都得跟真实数据绑定,这决定了整个项目的架构方式。

我做的是一个小型任务看板系统,功能和市面上的看板工具类似但做了大幅简化:用户注册、登录、任务增删改查、任务在"待办—进行中—已完成"三列之间流转、支持优先级和截止日期标注。验收的时候,负责人会按下面这条路径走一遍:

  • 注册一个新用户,然后退出登录
  • 用刚注册的账号登录
  • 新建一个任务,填入标题、优先级、截止日期
  • 把任务从"待办"拖到"进行中"
  • 刷新浏览器,确认任务还在"进行中"这一列
  • 退出登录,再访问需要登录的页面,确认被拦截
  • 重新登录,任务数据完整恢复

这条路径看起来不复杂,但每一步背后都对应一条完整的前后端链路。比如"刷新后数据还在",就要求后端数据库表设计正确、接口返回的数据结构稳定、前端拿到数据之后能正确按状态分组渲染。前两个实验那种写死在页面里的假数据,到这里完全不管用了。

1.2 我给功能边界画的"红线"

拿到需求之后,我第一件事不是翻框架文档,而是先把功能边界划清楚。这一步真的很重要,不然边做边加需求,实验报告根本收不了尾。

我当时主动砍掉的功能有四个:

第一,任务描述不做富文本编辑器,纯文本就够了。实验的考察重点在数据持久化和状态流转,富文本只是锦上添花,却会引入大量编辑器组件的配置成本。

第二,不做拖拽排序的细粒度持久化。也就是说,允许任务在列之间拖拽,但不记录同一列内任务的先后顺序。这个取舍直接省掉了一张排序字段表,也避免了一大堆"拖拽位置如何反写数据库"的麻烦。

第三,不做多人协作,任务归属只认当前登录用户。每个请求都要求带上Token,后端根据Token解析出用户ID,所有查询都用这个ID过滤,逻辑简单且不容易出错。

第四,密码找回、邮箱验证这类流程全部不涉及,注册登录只做最核心的部分。

划完红线之后,整个系统的功能清单就非常清晰了:注册登录、任务CRUD、状态流转、按优先级和截止日期展示、退出登录。这个范围既能覆盖课程要求的所有技术点,又不会因为过度设计把自己拖到报告写不完的境地。

2. 环境准备与选型说明:这套组合踩出来的经验

2.1 开发环境与版本组合

选型这件事,一开始我纠结了很久。班里有同学用Java的Spring Boot,也有用Node.js写Express的,我最后选了Python的Flask,前端配Vue 3。整个开发环境如下:

模块选择说明
后端框架Flask 2.x轻量,路由和请求处理直观,能快速聚焦业务逻辑
ORMSQLAlchemy配合Flask-SQLAlchemy扩展,避免手写SQL
数据库SQLite(开发)/ MySQL(提交)本地调试用SQLite省心,提交时切MySQL验证通用性
前端框架Vue 3 + Vite组合轻快,组件化方式适合看板类交互
UI组件库Element Plus表单、按钮、弹窗都有现成封装,省下不少样式时间
状态管理Pinia管理登录态和用户信息
HTTP客户端Axios统一处理请求拦截、响应拦截
接口调试Apifox调试接口的同时能生成文档,实验报告附件里直接能用

选择Flask而不是Spring Boot,最直接的原因是时间和资源占用。Spring Boot启动一个项目,光依赖下载和配置就要折腾半天,而Flask把业务逻辑写清楚只需要很少的模板代码。当然,如果你对Java更熟,用Spring Boot完全可以,后端的接口路径和逻辑设计思路没有任何区别,只是把蓝图改成Controller而已。

2.2 为什么采用前后端分离而不是服务端页面渲染

这个实验要求里有一条是"提交接口文档并演示前后端联调过程",这就决定了项目组织形式要选前后端分离。

从实际开发体验来说,任务看板的交互集中在前端的拖拽、状态切换和局部刷新。如果用传统的服务端模板渲染,每做一次拖拽都要提交表单并刷新页面,体验割裂不说,写起来也特别别扭。前后端分离之后,前端只关心怎么组织页面状态,后端只关心怎么把数据安全地增删改查,两边职责清清楚楚。

还有一个现实原因:实验验收的时候,前端是跑在开发服务器上的,后端是另一个进程,两边通过HTTP接口通信。这种结构最接近当前团队协作的主流方式,答辩的时候也更容易把"接口设计""数据格式约定"这些点讲明白。

2.3 数据库建模:三张表够用了吗

我最终设计了三张表:用户表、任务表,外加一张刷新令牌表。下面这张是任务表的核心结构:

CREATE TABLE tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(120) NOT NULL, description TEXT, priority VARCHAR(10) NOT NULL DEFAULT 'medium', status VARCHAR(10) NOT NULL DEFAULT 'todo', due_date DATETIME, user_id INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) );

用户表是标准的id、username、password_hash、created_at结构,密码字段只存哈希值,这个是底线。刷新令牌表用来处理登录态的续期,后面会详细说。

这里稍微解释两个设计决策。第一,优先级字段我直接用字符串存储,取值限定在high、medium、low三个值。有人可能觉得应该建一张优先级字典表,用外键关联。这个设计在业务足够复杂时是有意义的,但对实验场景来说,一张字典表带来的维护成本远大于收益,字符串加CHECK约束完全够用。第二,任务和用户之间用user_id做外键关联,所有查询强制带user_id过滤,从数据层就杜绝了"看到别人任务"的可能,比单纯靠接口层判断要稳妥得多。

有人会问三张表是不是太少了。最初我也想过要不要加一张操作日志表,记录"谁在什么时间把任务从A列拖到了B列"。后来划功能边界时想明白了,日志是审计需求,不是业务需求,实验验收根本不会看这个,加了反而让代码复杂度翻倍。

3. 后端接口开发:认证、任务流转与异常兜底

3.1 注册登录接口与密码哈希的实操细节

用户模块是整个系统的基础,这个模块出了问题,后面所有接口都跟着遭殃。我在这里花的时间最多,但收获也最实在。

注册接口的处理流程大致是:接收JSON请求体,校验用户名是否为空、是否已存在,校验密码长度,然后生成密码哈希写入数据库。下面是密码哈希的核心代码:

from werkzeug.security import generate_password_hash, check_password_hash class User(db.Model): # ... def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password)

我刚写的时候想过偷懒直接用MD5加固定盐,后来被自己否掉了。实验报告答辩环节一定会问"密码怎么存储、如何防撞库",如果答案是MD5,这个问题基本就答砸了。Werkzeug的generate_password_hash默认使用scrypt算法,自带随机盐,既简单又能把安全性讲清楚,没有必要自己造轮子。

登录接口成功后要签发令牌。我用PyJWT生成访问令牌,里面存用户ID和过期时间:

import jwt from datetime import datetime, timedelta, timezone def generate_token(user_id): payload = { "user_id": user_id, "exp": datetime.now(timezone.utc) + timedelta(hours=2) } return jwt.encode(payload, SECRET_KEY, algorithm="HS256")

同时生成一个刷新令牌存到数据库,过期时间设为7天。前端在访问令牌失效时用刷新令牌请求新令牌,避免用户频繁重新登录。这套逻辑在实验里属于加分项,但非常实用。

3.2 任务管理接口的路径设计与逻辑

任务模块是标准的资源型接口,路径设计围绕REST风格展开:

方法路径功能
GET/api/tasks获取当前用户任务列表,支持status筛选
POST/api/tasks创建任务
PUT/api/tasks/<task_id>更新任务标题、描述、优先级、状态、截止日期
DELETE/api/tasks/<task_id>删除任务

以更新任务为例,我踩过的坑是"只校验了任务存在,没有校验任务归属"。后来每个操作里都加了当前用户判断,逻辑如下:

task = db.session.query(Task).filter( Task.id == task_id, Task.user_id == current_user_id ).first() if not task: return jsonify(code=404, message="任务不存在", data=None), 404

这个判断必须加在接口第一行。原因是任务ID是自增数字,很容易被猜到。如果不做归属校验,只要遍历ID就能把别人的任务全改掉,这在验收时属于严重安全漏洞。

状态流转我做了简化处理,没有校验"从待办只能到进行中"这类状态机规则。一方面实验的核心是数据持久化,另一方面状态流转的黑白名单可以放到前端控制,后端允许任意合法字符串更新反而更灵活。当然也可以写一个STATUS_TRANSITIONS字典做严格校验,但那样会拖慢进度。这个度要看实验要求怎么定。

3.3 统一返回包装:让前端少写很多判断

前后端联调时最怕的就是各写各的,前端等一个字段叫message,后端返回的是msg,这种事一旦发生就得来回改。我在最开始就定了统一的返回结构:

{ "code": 0, "message": "ok", "data": {} }

code为0表示成功,非0表示各种错误。为了不让自己在写每个接口时都重复拼这个结构,我封装了统一的响应函数和全局异常处理器:

from flask import jsonify def ok(data=None): return jsonify(code=0, message="ok", data=data) def fail(code, message, http_status=200): return jsonify(code=code, message=message, data=None), http_status @app.errorhandler(404) def not_found(e): return jsonify(code=404, message="接口不存在", data=None), 404

统一结构的好处是,前端在Axios的响应拦截器里只需要判断code字段,等于0就走成功分支,否则统一弹错误提示。不用每个页面都单独处理请求失败的逻辑。我见过不少项目,接口返回格式五花八门,有的直接返回列表、有的返回对象、有的把错误信息嵌在HTML里,前端不光写着累,出了问题还分不清是格式问题还是业务问题。

4. 前端页面与交互:从静态原型到真实联调

4.1 登录态管理与路由拦截

前端这块,首先是登录态管理。我用了Pinia来存token和用户信息,同时借助Vue Router的全局前置守卫实现页面访问控制:

router.beforeEach((to, from, next) => { const authStore = useAuthStore() if (to.meta.requiresAuth && !authStore.token) { next({ path: '/login' }) } else { next() } })

这里遇到一个关键点:单纯在路由守卫里判断有没有token还不够,因为token可能已经过期。更好的做法是在Axios响应拦截器里统一处理401状态码:

axios.interceptors.response.use( response => response, error => { if (error.response && error.response.status === 401) { const authStore = useAuthStore() authStore.clear() router.push('/login') } return Promise.reject(error) } )

401统一跳登录页的逻辑,是联调阶段才真正理解透的。早期我每个接口都单独写错误处理,代码大量重复,后来才统一到拦截器里处理。这个修改让整个前端代码量减少了大约四分之一,而且不会再出现"某个页面忘记处理登录过期"的情况。

4.2 看板三列的数据组织与拖拽更新

任务看板的核心是"同一份任务数据,三种不同视角的展示"。后端接口返回的是扁平的任务数组,前端需要按status字段分成三组:

const todoList = computed(() => tasks.value.filter(t => t.status === 'todo')) const doingList = computed(() => tasks.value.filter(t => t.status === 'doing')) const doneList = computed(() => tasks.value.filter(t => t.status === 'done'))

拖拽交互我用了HTML5原生拖放API,没有引入拖拽库。drop事件里拿到拖拽的任务ID和目标状态列,然后调用PUT接口更新状态。这里我踩了一个不大不小的坑,后面专门讲。

另外,优先级和截止日期的展示我直接复用Element Plus的标签组件。优先级字段就三个值,硬编码好对应的颜色,不需要写复杂的渲染逻辑。

4.3 联调阶段的传参方式与格式统一

前端和后端第一次真正联调,出现最多的问题就是传参格式不匹配。我把遇到过的整理成了三类:

第一类是请求体的格式不统一。Axios默认会把对象序列化成JSON,但如果后端接口用request.form取数据,就会一直取到空值。解决办法是后端统一从request.get_json()读数据,双方约定好"所有POST/PUT请求体一律是JSON"。

第二类是路径参数和查询参数混用。比如获取任务列表,有人写成/api/tasks/status/todo,有人写成/api/tasks?status=todo。我最后统一按查询参数处理,因为status是筛选条件,不是资源ID,放路径里语义不对。

第三类是字段命名风格。数据库字段用了snake_case,前端JavaScript习惯用camelCase。我的解决办法是后端接口返回和接收时都坚持snake_case,前端在代码里显式写字段名,不做自动转换。虽然不如写一个驼峰转换工具那么优雅,但胜在直观,出问题了一眼就能看到。

5. 调试与修复记录:三个让我折腾到半夜的问题

5.1 跨域放行之后,Token成了摆设

第一次把前端和后端连起来跑,请求直接被浏览器拦了。前端在5173端口,后端在5000端口,跨域了。我当时的解决方案是用Flask-CORS扩展,但第一次配置写得太宽松:

CORS(app, resources={r"/api/*": {"origins": "*"}})

配置完发现普通请求能通了,但带Token的请求依然失败。看浏览器控制台才发现,预检请求OPTIONS返回的响应头里没有Access-Control-Allow-Headers: Authorization。浏览器在预检阶段就发现不允许携带Authorization头,直接把后面的真实请求拦下来了,根本轮不到后端校验逻辑执行。

修复方式是明确指定允许的请求头和来源:

CORS(app, resources={ r"/api/*": { "origins": ["http://localhost:5173"], "allow_headers": ["Content-Type", "Authorization"], "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"] } })

这个坑的教训是,跨域配置不能无脑用通配符。后端既然要解析Token,就必须允许Authorization头通过跨域策略,否则就是一个逻辑不通的配置。

5.2 日期格式化把截止时间整整吞掉了八小时

任务看板里有个截止日期字段,前端用户选择"2025-04-18 23:59"作为截止时间,提交保存后,刷新页面显示的却是"2025-04-18 15:59"。整整少了八个小时。

排查过程很典型。先看数据库原始存储,SQLite里存的是2025-04-18 23:59:00,说明后端接收正确、存储正确。再看接口返回的JSON,变成了2025-04-18T15:59:00Z。问题出在后端的序列化环节:SQLAlchemy默认按UTC时区输出DATETIME字段,存储时是本地时间,输出时被当成了UTC,于是少了八小时。

这个问题的教训是:前后端传输日期时间,永远不要依赖对方去猜时区。我最后的处理方式是后端在返回字段时统一格式化为不带时区的字符串"yyyy-MM-dd HH:mm:ss",前端拿到字符串直接展示,不调用Date自动解析。跨时区转换不是实验需要研究的重点,能避免就尽量避免。

5.3 并发点击下任务看板"越界"的修复

任务看板跑通之后出现了一个隐蔽问题:快速连续点击拖拽时,任务状态偶尔会错乱。比如一个任务刚从"待办"拖到"进行中",紧接着用户又快速点了另一个操作,前端发出的两个请求到达后端的顺序和发出顺序不一致,最后数据库里存的状态和用户看到的状态不一致。

问题根因是任务更新时没有做任何并发控制。我的修复方案是在更新接口里加入乐观锁判断,利用updated_at字段作为版本号:

result = db.session.query(Task).filter( Task.id == task_id, Task.user_id == current_user_id, Task.updated_at == request_json.get("updated_at") ).update({ "title": request_json.get("title"), "status": request_json.get("status"), "updated_at": datetime.now() }) if result == 0: return jsonify(code=409, message="数据已在其他地方被修改,请刷新页面", data=None), 409

对应的前端逻辑是:拖拽任务时先拿到任务当前的updated_at值一起提交;如果收到409响应,说明本次修改基于的数据已经过期,立刻重新拉取任务列表覆盖本地状态。这套机制不加也能跑,但加完之后,快速连续操作再也不会出现状态错乱,答辩时还能往"并发控制"这个方向多聊几句。

5.4 实验验收的完整过程和最终效果

修复完上面三个问题之后,整个系统的稳定性才算真正达标。验收前我按最初的需求清单完整走了一遍流程:注册新用户、退出登录、重新登录、新建任务、拖拽流转、刷新页面数据保留、退出后访问受限接口被拦、重新登录数据恢复。每一步都在预期内,没有出现需要刷新两次才能看到正确数据的情况。

完成之后我自己最大的感受是:实验三真正考察的不是某个框架的API背诵,而是把前端页面、后端接口、数据库存储串成一条完整链路的能力。之前学的东西都是孤立的,到了这个实验才第一次有了"一个请求从前端组件出发,穿过网络、经过后端校验、写入数据库、再原路返回渲染成页面"的整体感。

最后分享一个小组内的排查技巧:联调阶段不要盯着浏览器页面猜问题,直接把接口调用的完整请求和响应打出来,用Aplayer这类调试工具看原始报文,基本上百分之八十的问题出在某个字段名写错、某个请求头没带上这类低级错误。把联调流程固定下来,后面的实验会轻松很多。

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

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

立即咨询