
这周的作业标题是“第五周作业 0430”我刚看到时只把它当成作业编号翻了一下课程平台才发现 0430 其实是截止日期。前几周我的作业都是靠熬夜踩点才交上的每次都是在最后两个小时里一边改 Bug 一边后悔。这次我决定换个思路提前四天开始拆题先用两个晚上做设计再留两天写代码和自测。结果不仅提前交上了作业还第一次在提交前把全部功能完整跑通。我把整个分析和实操过程完整记录下来尤其适合正在上编程课、每隔几天就要交一次项目作业的同学。整篇内容不会只围绕某一个功能点而是把写作业这件事当成一个小型项目来管理从拆题目、理环境、列功能、写接口到最后提交检查一步步说清楚。1. 先把“第五周作业 0430”看明白这类周作业到底在考什么1.1 标题里藏着的两件事课程节奏与交付时间“第五周”这三个字很多人直接忽略掉了只盯着后面那个日期。但如果你把这几次作业连起来看就会发现问题第五周几乎是整个课程节奏的一个分水岭。前面四周作业多半是单点练习。第一周写一个页面布局第二周写一个表单校验第三周可能让你调一个轮播图第四周开始接触后端接口但基本都是“调用一个现成接口把数据渲染出来”。到第五周课程通常刚好讲完数据库基础、后端路由和前端交互所以作业会从“单点 Demo”突然变成一个“完整链路”的小系统——数据库要连接口要写页面要调数据要能真实地增删改查。这一周跟上不上直接决定你后面几周的项目实训能不能听懂。我见过不少同学前几周分数都不错从第五周作业开始突然掉队原因不是突然变笨了而是还在用前面四周“照着示例改一改”的方式来应对这种综合型作业结果连需求都理不清。至于 0430既有可能是截止日期也有可能是题目编号最稳妥的理解就是按截止日期处理。我当时把 4 月 30 日当成硬性交付时间倒推出了完整的三天计划首日拆题和环境准备次日接口与页面实现最后一天集中自测和整理提交材料。事实证明这个倒排计划比过去“随缘写、写多少算多少”靠谱太多了。1.2 站在出题人角度拆考点你才知道交付标准是什么我花了大概半小时站在讲师的角度重新看这份作业得出的结论是第五周作业想验证的从来不是“你背没背熟某个语法”而是三件事。第一是需求理解能力。题目不管写得长还是短里面永远藏着几个必须完成的动作比如新增、查询、修改、删除偶尔还有搜索、统计、状态流转。能不能把这些动作从一段绕来绕去的描述里准确剥出来决定了你的系统边界对不对。第二是工程组织能力。第五周的作业不再是一两个文件就能搞定的它至少牵扯到前端页面、后端服务、数据库、启动脚本、依赖配置文件。这些文件怎么摆、代码怎么分、启动命令是否清晰讲师运行你项目的前三分钟就能看出你的工程素养。第三是排错能力。运行你的项目时讲师不会按照你脑内的操作顺序来他可能会先点删除再点新增会输入空标题会连续刷新页面。这些边界情况一旦你没处理当场就是功能缺陷。想明白这三点之后我对作业的交付标准就有了一个明确判断不是“把题目要求的功能写出来就行”而是“一个没有任何前置讲解、任何人拿到手都能按 README 跑起来、并且操作过程不容易报错的小系统”。后面的环境准备、需求拆分、代码实现、自测脚本全部围绕这个标准来做。2. 动手之前先把开发环境与依赖理顺避免交付时翻车2.1 建立一套一眼就能看懂的项目目录第五周之前的作业我都是直接在课程自带的工作区里乱写文件散成一堆。这次我一开始就新建了独立的项目目录并且严格分好了前后端。week5-homework-0430/ ├── client/ │ ├── index.html │ ├── css/ │ │ └── style.css │ └── js/ │ └── main.js ├── server/ │ ├── app.js │ ├── routes/ │ │ └── notes.js │ ├── db/ │ │ ├── notes.db │ │ └── init.sql │ └── package.json ├── scripts/ │ └── test_notes.js └── README.md这个结构看起来简单但每个目录都有明确职责。client 是纯前端server 是纯后端scripts 放自测脚本db 目录单独放数据库文件和建表脚本。讲师拿到压缩包后就算不看任何说明也能从目录名猜到每个文件是干嘛的这一眼印象分不能丢。别忘了课程用的是 Node.js Express SQLite如果你用的是 Flask、Django 或者别的技术栈目录名可以换成 templates、static、migrations 之类但“前后端隔离 数据库独立 脚本独立”这个原则不变。把数据库文件放在 db 目录还有一个现实原因提交时你可以决定要不要把数据库文件一起打包而不用在项目根目录里翻找。2.2 从第一天就把版本控制跑起来commit 写得像给未来的自己留纸条很多人觉得周作业就一个项目用不着 Git。我恰恰是在第五周开始坚持用的每次完成一个小功能就提交一次。这不是给讲师看的是给自己兜底的。举个例子我在写新增笔记接口的时候发现参数校验写错了导致前端传过来的空字符串被存进了数据库。如果中间没有那个 commit我可能已经顺手改了三四轮代码早就忘了哪一行是后来加的。但因为我在改动之前做了 commit一条git diff就能看到问题出在哪一行。.gitignore 也一定要提前写好。我前几周犯过一个特别离谱的错误第一次交作业时不知道 node_modules 是什么把整个依赖目录一起压进了压缩包结果压缩包体积快到 50MB提交平台死活传不上去最后只能全部删掉重打折腾了半小时。你如果在 .gitignore 里写上 node_modules、.vscode、.DS_Store 这些目录不仅能避免这种尴尬也给讲师省去了解压后目录深不可测的麻烦。2.3 锁死依赖版本别让自己成为“本地能跑、老师那报错”的案例第五周作业开始引入第三方依赖之后“依赖版本不一致”成了翻车重灾区。我们班就有个同学本地 Express 是最新版运行得好好的交上去之后讲师那边跑起来直接启动报错后来才发现他上传的 package-lock.json 没有包含进来讲师安装时拿到了另一个版本两个版本之间某条路由的写法不兼容。所以提交前一定要确认两件事package-lock.json 在不在项目根目录README 里启动命令是否明确写了npm install。如果你用的是 Python就用 requirements.txt 把每个包的版本号固定住比如flask3.0.0而不是直接写一个flask上去。依赖不锁版本就是给未来埋雷。3. 需求落地三步法从题目里挖出功能清单、数据模型和接口列表3.1 从题干描述中提取功能清单同时把“不做什么”也写清楚这次我们课程的作业题目是“实现一个个人读书笔记管理小系统”要求是用户可以记录阅读过的书籍和短评能查看所有笔记的列表能修改和删除笔记还要求标题和内容不能为空。我把这段描述翻来覆去读了两遍不急着打开编辑器先拿一张纸把所有动词划出来记录、查看、修改、删除、校验。然后整理成一张功能清单表题干关键词对应功能验收标准记录书籍与短评新增笔记填写标题、作者、短评后能提交成功记录阅读状态状态标记新增时可设置“在读/读完/想读”查看所有笔记笔记列表首页能展示全部笔记按时间倒序修改笔记编辑功能点击编辑后能更新内容并生效删除笔记删除功能点击删除能移除记录标题和内容不能为空参数校验空内容提交会被拦截并提示做完这张表我还在后面加了一行“明确不做的事情”不做登录、不做分页、不做搜索、不做标签分类。这不是偷懒而是作业阶段最怕的就是需求蔓延。题目只要求增删改查你非要做个用户系统先不说代码量和复杂度翻倍光是调试和提交材料准备就可能拖垮你。3.2 数据模型设计先想清楚存什么再想界面长什么样第五周作业的学生最容易犯的错就是先照着原型图或者示例页面去抄代码数据库表结构随便拍脑袋来一个。我这次把顺序反过来先设计数据表而且刻意做得很克制只用了一张表。CREATE TABLE notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT NOT NULL DEFAULT , content TEXT NOT NULL, rating INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 在读, created_at TEXT NOT NULL DEFAULT (datetime(now, localtime)), updated_at TEXT NOT NULL DEFAULT (datetime(now, localtime)) );字段看起来很少但每个字段都是思考过的。title 和 content 不必说是业务核心NOT NULL 直接对应题目的非空校验。author 允许为空因为有时候你真的只想记一句话不一定要填作者。status 用了最简单的文本值“在读/读完/想读”而不单独建一张状态表理由是当前阶段只存在一个状态字段没有关联查询需求硬拆成两张表只会增加毫无必要的联表难度。rating 评分字段是我自己加的题干没提但读书笔记附带一个 1 到 5 的评分会让演示效果更丰满。注意加功能的时候要问自己一句它会不会牵扯出额外的复杂逻辑评分只是一个整数不需要额外接口前端点几个按钮就行所以加它安全。created_at 直接用 SQLite 的本地时间省去应用层处理。设计完表之后我顺手插了两条模拟数据。这一步很多人会跳过但我建议你一定做。有了模拟数据写列表接口和前端页面时你就是在一个“已有内容”的状态下开发而不是面对一张空表联调效率完全不同。3.3 接口清单把“增删改查”翻译成浏览器和服务器都能听懂的协议数据表完成之后下一步就是定义接口。这个动作我过去经常省略觉得自己都懂到了真要写路由时才发现路径命名乱七八糟。这次先把接口清单列出方法路径说明POST/api/notes新增一条笔记GET/api/notes获取全部笔记列表GET/api/notes/:id获取单条笔记详情PUT/api/notes/:id更新一条笔记DELETE/api/notes/:id删除一条笔记看到这份清单你就会明白CRUD 其实只有五个动作难点不在数量而在每一处细节新增要不要限制字段长度查询列表要不要排序更新时哪些字段允许修改删除后再次查询会不会报错这些问题提前在接口清单里想一遍代码实现时会省下大量反复修改的时间。4. 实现环节后端、前端与容错处理的完整走查4.1 先把一个最小可运行的后端服务跑起来再谈业务逻辑我见过太多人一上来就把所有路由写完再启动服务测试结果一启动就是一大堆报错根本分不清是依赖问题、路径问题还是代码逻辑问题。我的做法反着来先写一个只返回“hello”的最简服务把它跑通再往里面填业务。// server/app.js const express require(express); const path require(path); const notesRouter require(./routes/notes); const app express(); app.use(express.json()); app.use(express.static(path.join(__dirname, ../client))); app.use(/api/notes, notesRouter); const PORT 3000; app.listen(PORT, () { console.log(Server running at http://localhost:${PORT}); });这段代码里有几个细节你可能没注意到。第一express.static 这行直接把我前端的 client 目录托管出去了这样讲师只需要启动一个后端服务访问 http://localhost:3000 就能看到页面不用再开一个静态服务器也不会有跨域问题。第二app.use(/api/notes, notesRouter) 把路由挂载到一个统一前缀下面后面写子路由时就不用重复写 /api/notes 前缀了。4.2 写路由时把参数化查询和状态码一起考虑进去然后是核心路由。我拿最容易被讲师测出问题的新增接口来做例子// server/routes/notes.js const express require(express); const router express.Router(); const path require(path); const sqlite3 require(sqlite3).verbose(); const db new sqlite3.Database(path.join(__dirname, ../db/notes.db)); router.post(/, (req, res) { const { title, author, content, rating, status } req.body; if (!title || !content || !title.trim() || !content.trim()) { return res.status(400).json({ error: 标题和内容不能为空 }); } const safeRating Math.min(5, Math.max(0, parseInt(rating, 10) || 0)); const safeStatus [在读, 读完, 想读].includes(status) ? status : 在读; db.run( INSERT INTO notes (title, author, content, rating, status) VALUES (?, ?, ?, ?, ?), [title.trim(), author || , content.trim(), safeRating, safeStatus], function (err) { if (err) { return res.status(500).json({ error: err.message }); } res.status(201).json({ id: this.lastID }); } ); });我给这条接口加了三个容易被忽略的东西。第一个是参数校验注意我把 title 和 content 的 trim 判空也加上了否则前端传一个全是空格的字符串照样能存进数据库这就是讲师最容易测出来的隐藏 Bug。第二个是 rating 值的边界收敛把超过 5 或小于 0 的输入强制拉回合法区间防止非法数据污染页面。第三个是 201 状态码新增成功返回 201 而不是 200这是比较规范的语义表达你看很多接口文档会这么标。剩下的列表、详情、更新和删除接口思路完全一致列表接口注意加上ORDER BY created_at DESC详情和删除先查询再操作找不到记录就返回 404。核心原则就一句话每个接口都能在“传入有效参数时正常执行传入无效参数时给出明确的错误状态码”而不是让服务器直接崩掉。4.3 前端怎么接后端把接口地址抽成常量再把错误处理做完整后端写完我进入前端部分。页面结构不复杂一个输入表单加一个列表区域值得展开讲的是 JS 的请求部分。// client/js/main.js const API /api/notes; async function loadNotes() { const listEl document.getElementById(note-list); try { const res await fetch(API); if (!res.ok) throw new Error(加载失败); const notes await res.json(); if (!Array.isArray(notes) || notes.length 0) { listEl.innerHTML li classempty还没有任何笔记先添加一条吧/li; return; } listEl.innerHTML notes.map(note li>// scripts/test_notes.js const BASE http://localhost:3000/api/notes; async function test() { // 新增 const createRes await fetch(BASE, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ title: 测试书, content: 测试内容, status: 在读 }) }); const created await createRes.json(); console.log(CREATE:, createRes.status 201 created.id ? PASS : FAIL); // 列表 const listRes await fetch(BASE); const list await listRes.json(); console.log(LIST:, Array.isArray(list) list.length 0 ? PASS : FAIL); // 详情 const detailRes await fetch(${BASE}/${created.id}); const detail await detailRes.json(); console.log(GET DETAIL:, detail.title 测试书 ? PASS : FAIL); // 更新 const updateRes await fetch(${BASE}/${created.id}, { method: PUT, headers: { Content-Type: application/json }, body: JSON.stringify({ title: 修改后的书名 }) }); console.log(UPDATE:, updateRes.ok ? PASS : FAIL); // 非法提交空内容 const badRes await fetch(BASE, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ title: , content: }) }); console.log(VALIDATION:, badRes.status 400 ? PASS : FAIL); // 删除 const deleteRes await fetch(${BASE}/${created.id}, { method: DELETE }); console.log(DELETE:, deleteRes.ok ? PASS : FAIL); } test();这个脚本表面上只做了六件事但它把整个 CRUD 主链路的正常路径和一条异常路径都覆盖了。每次改完代码我都先node scripts/test_notes.js跑一遍全部 PASS 再继续写下一个功能。它能让你在提交前的半小时里快速确认“核心功能没有被改坏”而不是靠手点页面点到手酸。5.2 提交前十分钟按这份清单逐项过一遍我最后一次提交前按下面这份清单逐项检查每一项都有具体动作而不是凭感觉。第一从零环境复现一遍。我先把项目目录里的 node_modules 和数据库文件全部删掉然后按 README 写的步骤依次执行npm install和npm start确认代码和配置文件没有遗漏。第二检查 README 是否包含启动步骤、依赖环境和访问地址。我见过很多同学 README 只写一句“请运行项目”这不是说明文档这是废话。至少要写清 Node 版本要求、安装命令、启动命令、访问 URL。第三压缩包结构检查。打包后用解压工具打开看一遍确保顶层只有一个目录所有必需文件都在里面没有 node_modules、缓存文件、测试数据备份这些乱七八糟的内容。第四截图佐证。我把新增、列表、详情、编辑、删除五个功能分别截图整理在一个 screenshot 文件夹里。讲师没时间把你的每个功能都点一遍但截图能让他第一时间看到功能全貌同时也算是一个直观的演示指南。第五代码风格统一。我把自己写的代码重新过了一遍修改掉不一致的缩进、混乱的变量命名和多余的 console.log。你不需要用多么花哨的设计模式只要保证同一份文件里的风格稳定讲师看起来就舒服。6. 周作业常见翻车实录与排查方法6.1 本地一切正常交到课程平台却跑不起来这是第五周作业群里最常见的求助信息没有之一。我总结了一下大概有这么几种原因。第一种是写死了本地路径。比如数据库连接用了C:\Users\你的名字\Desktop\week5\notes.db这种绝对路径换一台机器当然直接报错。修复方式是改成相对路径像我的代码里那样用path.join(__dirname, ../db/notes.db)。第二种是数据库文件压根没打包。SQLite 数据库如果是运行时新建的没有提前建表或者没把建表 SQL 放进项目讲师那边启动后看到的是一张空表页面能打开但任何列表都是空。解决方法是把建表语句放进 init.sql并且在 README 里写明初始化数据库的命令。第三种是端口被占用。我自己遇到过两次开了一堆调试进程没关再启动项目时提示EADDRINUSE。修复方式是杀掉占用进程或者把服务端口改成不常用端口后重新启动。6.2 数据明明存进去了页面上却显示乱码或空白乱码问题在笔记系统里尤其容易遇到因为用户输入的几乎全是中文。第一次遇到时我以为数据库坏了后来排查发现是响应头缺少字符集声明。在 Express 中只需要加一行res.setHeader(Content-Type, application/json; charsetutf-8)或者在 app.js 统一设置app.use(express.json())之后加上对应的字符集中间件就能解决大部分乱码情况。还有一种“空白”是前端渲染问题。我在自测时发现如果笔记内容里有换行符直接用 innerHTML 渲染会被浏览器当成普通空格合并掉看起来就像内容挤成一块。后来我在渲染时把内容里的换行符替换成了br同时配合前面说的 escapeHtml才把换行显示修好。这类问题靠肉眼很难发现最好的办法就是专门准备一条带长文本、多换行、特殊符号的数据来测试。6.3 评分里最容易丢分但你又看不到的小事作业打分不是只有“功能能不能跑”我看过讲师对作业的点评后整理出几个加分和扣分项供你参考。代码注释是个典型。不要写“这行代码是新增笔记”这种废话注释但要在路由逻辑比较复杂的地方写清楚“为什么这么做”。比如我写 rating 边界收敛时注释是“防止评分字段被传入非法值导致页面展示异常”这种注释体现的是思考过程而不是复读机。命名规范也很重要。接口路径用复数名词notes不要一会儿 notes 一会儿 note变量名用驼峰还是下划线跟后端返回字段保持一致。前端变量命名也得同步比如后端返回created_at前端读取时就用created_at不要自作聪明转成createdAt前后不一致最容易引发低级 Bug。提交记录的颗粒度也反映态度。Git 提交记录如果是一整堆“final”“final2”“真的最终版”给人的观感会差很多。我这次提交记录用的是“feat: 新增笔记接口”“fix: 修复空列表显示”“docs: 补充README”半小时不到就能从历史记录看出整个项目的发展轨迹这对作业评审来说是一个很好的加分项。7. 第五周之后这套思路还能怎么复用7.1 从作业到真实项目差的只是同一套流程的迁移第五周作业做完我最大的体会不是代码写得多好多稳而是我终于把“先想清楚再做”从口号变成了习惯。以前我拿到任何题目第一反应都是打开编辑器就开始敲敲到一半发现设计有问题再返工返工完又觉得字段不对周而复始。这次我先写三张纸功能清单、数据模型、接口列表写完之后真正敲代码的过程只占了整个作业时间的一半都不到。这个习惯后来也用到了第六周、第七周的项目里甚至用在某个公司的小型内部工具开发上。区别只是周作业的三张纸可能只有几行字真实项目的三张纸可能是一份几页的设计文档但思考路径完全一样——先定交付边界再定数据结构最后填实现细节。你会发现只要边界和数据结构不摇摆代码层面的返工率会低得惊人。7.2 一个小技巧每写完一个接口立刻手动测试一次不要攒到最后我在这次作业里最受益的一个操作是每写完成一个接口就立刻用 curl 或浏览器地址栏测一次。比如刚写完新增接口马上执行一条curl -X POST http://localhost:3000/api/notes -H Content-Type: application/json -d {title:t,content:c}确认返回了 id。再写列表接口刷新一下地址确认新数据出现在列表里。这样做的好处是问题永远是“刚发生的、还记得上下文”的问题定位成本极低。如果你把五个接口全部写完再统一测试一旦报错你根本分不清是路由问题、数据库问题还是前面接口的副作用导致的排查成本直接翻倍。这就像写作文边写边读比一口气写完再回头改错字要轻松得多。第五周作业这件事让我明白拖到截止前才动手的作业永远只能做到“交得上去”而提前几天用流程化方式做的作业才能做到“拿得出手”。下一次你拿到一份周作业题目可以先别急着打开编辑器花半小时做一张功能、数据、接口的三张纸我保证你后面写代码的速度会比以前快一半还多。