ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

两天全栈开发:从数据库表到前后端联调的任务管理应用

两天全栈开发:从数据库表到前后端联调的任务管理应用 如果你也在用一个带日期的编号来推进项目那一定对这种“day5day6”的记录方式不陌生。这是我一个30天全栈开发计划里的连续两个开发日目标很纯粹把一个已经躺在设计文档里的小型任务管理应用从只有数据库表结构的状态推到前后端真正能跑通、能增删改查、能上线演示的程度。简单说day5交给了后端接口和数据库落地day6交给了前端页面和联调收尾。这篇文章就记录这两天里我实际怎么拆任务、怎么写代码、怎么一步步排除问题也把那些文档里不会写的坑和经验一并整理出来。不管你是正想做类似的全栈练手项目还是刚到联调阶段被各种问题折磨这份记录应该都能让你少走一些弯路。1. 整体任务拆解为什么两天会被我合并成一个冲刺单元1.1 day5和day6的任务边界在开始之前前四天我已经完成了三件事搭好了Node.js环境、确定了需求范围、画出了数据库ER图。到第5天和第6天整个计划的“硬核施工”才真正开始。我把这两天的边界划分得很清晰day5后端日把设计文档转化为可运行的API服务包括建表、写CRUD接口、处理统一返回格式和基本异常捕获。day6前端日用ReactVite搭建任务管理界面对接后端接口完成列表渲染、新增、状态切换、删除和编辑弹窗最后做一次完完整整的联调。这个划分看起来简单但实际操作里有个很关键的心理因素如果把后端接口写得差不多就去碰前端往往会两头都做不深。我刻意把day5钉死在“接口全部跑通用Postman验证过每一个路径”才允许自己进入day6的前端环节。1.2 为什么把两天合并成一个冲刺单元单个day很容易被碎片化的事情打断比如临时改需求、被一个错误卡住一下午、甚至只是环境变量配置反复试错一天就荒废了。把day5和day6合并成一个“开发冲刺段”之后我相当于给自己心理上加了一堵墙这两天之内唯一的目标就是交付一个可运行的全栈应用。这样做还有一个实际好处前后端联调本来就是“你等我一次接口字段调整、我等你看一次页面效果”的节奏如果隔了一天再联调上下文全部断了。连续两天作业我能在晚上就把前端需要的字段格式反馈给后端第二天一早后端已经调完这种沟通效率是隔天拆开完全比不了的。当然合并冲刺的前提是任务量有边界。如果day5和day6叠加起来超过30个小时的工作量那这样的合并就没有意义因为人会陷入疲劳导致的低效。我评估过这个项目的复杂度——一个任务表加上五六个接口和四五个前端组件两天时间是富余的所以我才采用这种节奏。1.3 技术栈与选型理由这次项目我选了 Express SQLite React Vite没有引入任何重的工程化框架或状态管理库。SQLite单文件数据库零配置文件特别适合开发冲刺阶段。你不需要像MySQL那样先建库、建用户、配权限只要有个文件路径就能跑起来。缺点是并发写能力弱但这个阶段的应用根本到不了并发场景。Express轻量、中间件生态成熟路由写法直白。后端只有二十来个文件用Koa或者Nest都显得杀鸡用牛刀。React ViteVite的启动速度和热更新反馈极快写页面的时候几乎不用等待非常适合一天内大量迭代UI。React的组件心智模型也适合任务列表这种“数据驱动界面”的典型场景。不引入Redux/Zustand任务管理app的状态只有tasks数组和当前编辑项用useState就足够。引入状态管理库反而会让第六天的时间被分散。选型背后有一条经验练手项目的高度不在于技术栈够不够先进而在于你能否把业务完整走通。Express和SQLite都是很“朴素”的选择但正因为朴素我才能把注意力集中在数据和接口层而不是被框架细节拖住。2. day5全记录后端接口与数据模型落地2.1 从ER图到物理建表几个我反复确认的取舍前四天我的ER图里有users、tasks两个表还设计了外键关联。但真正落地时我主动砍掉了users表把所有任务归在同一个默认用户下。这不是偷懒而是想明白了一个问题用户体系本身是一个完整的认证模块加入Login和Token会让两天的开发计划直接膨胀成四天而这个任务管理应用的核心场景只是“我自己的待办列表”。最终tasks表结构如下CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT DEFAULT , status TEXT NOT NULL DEFAULT todo, priority TEXT NOT NULL DEFAULT medium, due_date TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP );这里有两个字段我当时反复纠结过due_date为什么不选DATETIME而是TEXTSQLite本身是弱类型数据库存时间戳和存ISO字符串都能工作。但前端处理表单时用input[typedate]生成的天然就是YYYY-MM-DD字符串后端直接存储这个字符串前端展示时不需要任何转换。实测下来这是最不可能出错的方案后面我还专门踩过时区坑下节细说。status为什么不用0/1/2数字而用字符串字符串todo、doing、done的可读性远高于数字。虽然在代码里可以定义枚举但直接查数据库看数据时字符串一眼就知道任务状态调试成本低很多。索引方面我给status和due_date都建了联合索引。这个应用的数据量级很小平时感觉不到索引的价值但养成“查询条件就建索引”的习惯绝对没有坏处。2.2 接口设计文档与统一返回结构接口在设计文档里已经列过一版真正写后端时我又调整了几处字段命名。最终所有接口如下方法路径功能请求体/参数GET/api/tasks任务列表支持按状态筛选query: statusGET/api/tasks/:id查询单个任务无POST/api/tasks新增任务title必填其他可选PUT/api/tasks/:id整体更新任务title、description、status、priority、due_datePATCH/api/tasks/:id/status仅修改状态statusDELETE/api/tasks/:id删除任务无统一返回结构是我从day5一开始就强行规定的这个决定在day6联调时帮了大忙{ code: 0, message: success, data: {} }凡是业务上捕获到的异常code非0message里写清楚原因。千万不要让接口直接把SQLite的原始报错信息抛给前端既能避免信息泄露也让前端拿到错误后可以做统一的toast提示。2.3 后端代码实现过程中的几个实操细节Express项目结构我没有用MVC那套大而全的目录只分了三层routes、db、utils。因为业务足够简单再往上抽象就是过度设计。数据库连接直接用了better-sqlite3这个库最大的好处是同步API不用像sqlite3那样回调嵌回调。CRUD的代码写起来很容易让人犯困真正让我花时间的是这几个地方统一错误处理中间件app.use((err, req, res, next) { console.error([${new Date().toISOString()}], req.method, req.originalUrl, err.message); res.status(err.status || 500).json({ code: 1, message: err.message || server error, data: null }); });写这个中间件时我特意在日志里加上了req.method和req.originalUrl。原因很简单当你同时开着一个以上的接口调试时光看一条错误日志根本想不起是哪个请求触发的带上请求方式和路径能立刻定位。参数校验要写在小函数里不要散落在路由中function parseTaskBody(body) { const { title, description, status, priority, due_date } body || {}; if (!title || !title.trim()) { throw { status: 400, message: title is required }; } const allowedStatus [todo, doing, done]; if (status !allowedStatus.includes(status)) { throw { status: 400, message: invalid status }; } return { title: title.trim(), description: description || , status: status || todo, priority: priority || medium, due_date: due_date || null }; }这个函数保证所有非法输入在进入数据库之前就被拦截。刚开始写代码的时候我总觉得参数校验很麻烦年轻的时候也习惯直接把req.body塞进SQL结果后来被各种脏数据折磨。现在宁可每个接口多两行调用也要把脏数据挡在门外。SQLite的日期坑在写入时就规避CURRENT_TIMESTAMP默认返回UTC时间中国用户看到的是UTC8。2019年我做第一个项目时踩过一次坑导出的“今天创建的任务”数据库里显示昨天这个坑当时查了半天。这次我直接在插入语句里用本地时间const now new Date().toISOString().replace(T, ).slice(0, 19); stmt.run([body.title, body.description, body.status, body.priority, body.due_date, now, now]);注意toISOString()返回的也是UTC时间严格来说应该按Intl.DateTimeFormat处理。但在这个项目里因为后端和数据库跑在同一台机器上我选择了直接使用datetime(now,localtime)来规避问题INSERT INTO tasks (title, description, status, priority, due_date, created_at, updated_at) VALUES (?, ?, ?, ?, ?, datetime(now,localtime), datetime(now,localtime));这样存储的就是机器本地时间对单机应用来说完全符合预期。day5结束时我用Postman把所有接口挨个验证了一遍包括异常情况传空title、传非法status、查询不存在的id。全部通过后我才安心关掉电脑。3. day6全记录前端组件、API封装与联调收尾3.1 页面组件的划分与状态设计第六天早上打开电脑面对的是昨天搭好的Vite项目骨架。前端我按功能拆了四个组件刻意避免了“把管理界面当普通页面写”的毛病组件职责App.jsx全局状态管理者持有tasks列表和所有请求方法TaskList.jsx渲染任务列表分组展示todo/doing/doneTaskItem.jsx单条任务卡片处理状态切换、删除、编辑触发TaskForm.jsx新增/编辑弹窗处理表单输入和提交状态管理很简单所有数据都在App.jsx里通过props传给子组件。这个阶段我用了一个“单向数据流”原则任何子组件都不得直接修改tasks数组只能调用App.jsx下发的函数。比如TaskItem想删除一条任务就调用onDelete(id)而这个函数在App.jsx里通过fetch调DELETE接口并重新拉取列表。一开始我图方便想让TaskItem在删除接口成功后直接修改本地的tasks数组这样能少一次列表请求。但很快发现同一时刻可能有另一处更新改变了排序逻辑界面和数据库就出现了不一致。老老实实每次变更后刷新列表数据虽然多了一次网络请求但是逻辑清晰、绝对不会出现展示脏数据的问题。对两天冲刺而言稳定大于一切。3.2 API请求层的几个封装手法前端如果每个组件里各自写fetch那联调时后端字段一调整至少得改三四处。我在day6一开始就封装了一个请求模块const BASE_URL http://localhost:3001/api; async function request(path, options {}) { const url BASE_URL path; const res await fetch(url, { headers: { Content-Type: application/json }, ...options }); const body await res.json(); if (body.code ! 0) { throw new Error(body.message || request failed); } return body.data; } export const getTasks (status) request(/tasks${status ? ?status${status} : }); export const createTask (data) request(/tasks, { method: POST, body: JSON.stringify(data) }); export const updateTask (id, data) request(/tasks/${id}, { method: PUT, body: JSON.stringify(data) }); export const updateTaskStatus (id, status) request(/tasks/${id}/status, { method: PATCH, body: JSON.stringify({ status }) }); export const deleteTask (id) request(/tasks/${id}, { method: DELETE });这个封装的优点是每个函数只暴露业务语义把“抛错处理”集中在一处。页面里调用时我只要关注data是否存在异常统一由调用方catch后提示。后面联调时还真遇到过一次后端把DELETE接口的状态码从200改成204的情况如果前端每个地方各自写fetch那就是要到处改现在只要改这个request函数里对res.json()的处理即可——虽然204没有返回体但至少这个修改点是被集中管理的。3.3 联调全过程中最有价值的三步走第一天把后端接口写完第二天写前端中间隔了一个晚上但这并不代表联调就要从零开始。我给自己定了三步联调法效率极高第一步先甩掉UI用命令行验证接口连通性。早上起来别急着打开浏览器先在项目根目录下启动后端服务然后直接用curl或Postman确认昨天最后一个接口仍然正常。我自己的习惯是用一条curl命令检查最核心的接口curl -X POST http://localhost:3001/api/tasks \ -H Content-Type: application/json \ -d {title:联调测试,status:todo}这一条命令返回JSON成功之后我再进入前端开发。基础连通性没问题后面写的页面代码根本不需要怀疑是后端还是前端的问题。第二步写页面时用真实数据不要硬编码mock数据。很多教程喜欢在页面阶段先写死一组mock JS数组等UI做完了再“接真实接口”。我强烈不建议这个顺序。因为一旦前端代码里硬编码了字段名比如task.id、task.status联调时后端返回的字段如果有一个对不上你就要满文件找哪里用了这个字段。更好的办法是登录后直接调后端真实数据渲染即使此时UI还没成型哪怕是一个比较粗糙的列表也远比mock数据能提前暴露字段不一致问题。第三步把“修改接口字段”当作正常开发节奏而不是事故。联调过程中我发现一个字段命名问题前端写的是dueDateGET接口返回的是due_date新增任务时后端又要求JSON里传due_date。这个不一致如果硬要强撑只会让代码越来越脏。我的决定是前端继续使用camelCase但在请求层做一次字段映射把dueDate转成due_date把响应的due_date转回dueDate。具体操作很简单function mapTaskFromServer(task) { return { ...task, dueDate: task.due_date, status: task.status, priority: task.priority }; }这样组件里始终用dueDate请求时再转回蛇形命名后端代码不必动前端也保持主流代码风格。这种“映射代替改接口”的做法非常实用尤其是有多个使用者时千万不能让前端迁就后端的命名习惯否则代码很难看。4. 踩坑记录与问题排查速查表4.1 CORS跨域问题不是所有浏览器报错都能被一眼看懂第六天下午浏览器打开前端页面第一次调后端接口时控制台直接报了一个典型的CORS错误。具体含义是浏览器阻止了前端页面localhost:5173对后端localhost:3001的跨域请求。一般的Node后端加个cors中间件就能解决const cors require(cors); app.use(cors());就这么两三行却能卡住很多人因为大家常常分不清“后端没启动”和“CORS被拦截”的报错区别。我总结了一个快速排查法现象后端启动状态根本原因请求pending很长时间然后报net::ERR_FAILED后端没启动服务不可达请求立刻报CORS error且Network显示已发送请求后端已启动跨域拦截前端能拿到数据但控制台警报Mixed Content已启动页面HTTP后端HTTPS这里最直接的判别方式是直接在后端命令行里看有没有打印对应请求日志。如果后端日志里根本没有这个请求的记录那多半是请求就没发出来或者被代理拦截如果有完整日志且返回了200那问题一定在浏览器端的跨域拦截。靠这个思路我很少在CORS问题上浪费超过五分钟。4.2 时间类型和空值的连环坑day5我特意写了本地时间存储但day6联调时还是出了岔子列表中有些任务的due_date显示为2025-06-01 00:00:00而编辑弹窗里这个日期字段完全空白。原因出在input[typedate]需要严格按YYYY-MM-DD格式赋值而数据库返回的2025-06-01 00:00:00多出了空格和时分秒导致浏览器无法识别。更麻烦的是新增任务时如果用户不选日期due_date存进数据库里是NULL但前端拿到NULL后不能直接丢给new Date()不然会得到一个Invalid Date。这些空值问题必须在请求层统一处理function parseDueDateForForm(task) { if (!task.due_date) return ; return String(task.due_date).slice(0, 10); }处理完这个当天遇到的问题就少了一大半。4.3 用日志和状态码节奏排查的速查表常见现象排查方向一次性解决思路页面一直转圈无报错后端接口崩溃看后端控制台堆栈日志POST接口状态码500后端空指针/字段名拼错先看报错行再打印入参bodyDELETE后列表还在更新前端没重新请求列表删除成功后必须再调用getTasksPATCH只传status但还是400参数校验要求必须有body.status检查前端PATCH请求体是否真实携带数据库出现中文乱码终端编码问题统一UTF-8Windows下用chcp 650014.4 开发服务器没重启导致代码失效day5我写到一半给接口加了个query.status筛选参数但没重启Node服务直接跑到前端去调/tasks?statustodo永远得到全量数据。排查了半天最后发现是服务器进程还停留在旧代码。这也是Node开发里最常见的一个低级问题我自己都犯过不下五次。解决办法是用node --watch启动服务器这样每次文件变更都会自动重启。如果你用的版本不支持这个参数就装nodemon效果一样。5. 连续两天的开发节奏与几点个人经验5.1 两天的精力分配和休息策略很多人会把两天的开发拆成“白天写代码晚上debug”但我的经验恰恰相反第一天白天尽量多实现第一天下午预留出集中测试时间第二天上午只做联调配合不做新功能开发。因为联调阶段最需要清醒的头脑一旦发现“前端报字段缺失、后端要改数据格式”人必须在清晰状态下判断应该从哪边修正。我这两天实际的作息是这样的day5上午建表、初始化Express项目、写完前三个GET接口。day5下午写完剩余三个写接口统一错误处理和返回格式用Postman全量回归。day6上午先跑一遍curl确认后端存活然后集中写App.jsx状态管理和TaskForm弹窗。day6下午完成列表与卡片组件进入联调处理CORS、字段映射、日期取值三件事然后补边界情况。这样安排有个隐藏好处晚上留给大脑“后台整理”的时间。很多字段映射、命名冲突其实是在睡眠后想明白的第二天上手自然更快。5.2 代码之外的几个工作习惯实践下来这几条经验对我帮助最大在这里分享给正在做类似连续冲刺的人。第一件每次改动接口先更新接口文档表格。你可能觉得拿文档软件维护接口表很麻烦但实际只需把上文的接口表格复制到一个markdown文件里每次改一个字段顺手更新一次。看起来效率损耗很小一旦第二天或第三天必须查“哪个接口接受什么参数”时这份文档的价值就会翻倍。第二件给自己设定“查询最后一个非预期错误”的硬性时间线。开发中总会出现那种根本想不通的问题可能是环境变量失效也可能是node_modules损坏。我给自己定的规则是单个问题排查超过30分钟仍然无解立即停止深挖检查是不是当前思路本身错了或者干脆尝试重新安装依赖。钻牛角尖是开发效率第一杀手很多问题其实重置一下运行环境就能解决。第三件无论多着急也要在每一个接口返回数据的最外层留好数据结构。我见过很多后端联调时为了快直接返回一个数组或者直接返回true。当时很快但第二天前端拿到数组后想再扩展一个字段时才知道要改的地方有多少。统一返回结构这个决定在项目后半段的收益远大于花费的那几分钟。5.3 连续冲刺之后的复盘要点两天完全投入后会有一个很自然的疲惫期但我不建议立刻休息最好抽30分钟做一次简短复盘。复盘只问三个问题哪类问题占用时间最多重复出现了几次下一步能不能通过工具或规范把这个问题自动挡掉我自己复盘的结果是CORS中间件未装导致浪费了10分钟、日期格式不一致导致浪费了20分钟、服务器未重启导致浪费了15分钟。于是我给项目一次性补上了自动重启、全局日期格式化函数和起始模板这些改进让后面几天的开发顺畅了很多。这两天虽然累但回头看成品的完整度远超预期一个小型任务管理系统前后端从零跑通代码量不大但每一行都有据可循。如果说还有什么建议那就是不要害怕在开发过程中做“删除重来”的决定。比如我第一天写的路由结构在第二天上午发现不够顺果断把五个文件合并成三个损失半小时但换来的维护体验却是立竿见影的。只要记住冲刺的目标是一个人把东西做完做好而不是向任何人证明第一版代码有多么完美。
返回列表