1. 一个家长的真实痛点:为什么我决定自己动手做工具
我家孩子上小学三年级,英语课本上的单词今天背了明天忘,报过两个线上班,效果都一般。不是老师不好,是练习量根本不够——一周两次课,每次半小时,剩下的五天全凭自觉。我也试过自己盯着他读、听写,坚持了不到两周就崩了,原因很简单:我下班到家已经七点多,做饭、收拾、检查作业一套流程走完,根本没有精力再当英语陪练。
市面上英语学习类应用我几乎试了个遍。有的内容太游戏化,孩子玩得开心但记不住东西;有的题库陈旧,跟学校教材对不上;还有的功能堆得太多,打开首页十几个入口,孩子自己根本不知道怎么用。最核心的问题是:没有一个工具能围绕"我家孩子当前正在学的那几课"来做针对性练习。
我本身是做后端开发的,日常跟代码打交道,但前端和移动端不是我的强项。今年年初开始,身边陆续有人聊起"氛围编程"这种新的开发方式——简单说就是用自然语言描述需求,让AI帮你把代码写出来,你负责把关逻辑和调试。我算了一下,如果按传统方式从零学前端再开发一个完整应用,至少三个月起步;用这种方式,我给自己定了一个月的期限。
这个工具最终做出来是什么样?一句话概括:一个能导入课本单词表、自动生成听写和选择题、记录错题并安排复习的轻量级Web应用。它不联网、不收集数据、不需要注册,打开浏览器就能用。适合的人群很明确:有一定技术基础、愿意花时间陪孩子但不想被商业应用绑架的家长,以及想了解氛围编程到底能做出什么东西的开发者。
下面我把这一个月里踩过的坑、做过的取舍、以及最终跑通的完整方案,原原本本写出来。你不需要跟我用完全一样的技术栈,思路是可以复用的。
2. 整体设计思路:为什么选这个方案而不是别的
2.1 需求拆解:一个"够用就好"的工具应该长什么样
动手之前我先列了一张需求清单,分"必须有"和"可以有"两档。这个动作非常关键,因为氛围编程最大的风险就是需求发散——你跟AI聊着聊着,它什么都能给你加,最后做出来一个四不像。
必须有:
- 能手动录入或粘贴单词表,格式就是"英文+中文"两列
- 能根据单词表生成两种练习:听写(听英文写英文)和选择题(看中文选英文)
- 能记录每次练习的错题
- 错题能单独拿出来复习
- 数据存在本地,刷新不丢
可以有:
- 发音朗读(用浏览器自带的语音合成)
- 简单的正确率统计
- 深色模式
明确不做:
- 用户系统、登录注册
- 云端同步
- 社交功能、排行榜
- 任何形式的付费点
这张清单后来救了我好几次。每次AI建议我"要不要加一个XX功能"的时候,我就翻回来看一眼,不在清单上的直接砍掉。
2.2 技术选型:为什么最后选了纯前端方案
技术栈的选择我纠结了大概三天。最初想的是做一个微信小程序,因为孩子用平板和手机更方便。但调研之后发现两个问题:一是小程序开发需要注册主体、走审核流程,我一个个人开发者折腾这些至少多花一周;二是小程序的本地存储有容量限制,而且调试起来没有浏览器直观。
后来考虑过用Python写一个本地服务,浏览器访问。这个方案我熟,Flask起一个服务,前端用模板渲染。但问题也很明显:每次用都得先启动服务,我老婆根本不会操作命令行,孩子更不可能。一个需要"先运行python app.py"才能用的工具,注定吃灰。
最终方案是纯前端单页应用:一个HTML文件,加上CSS和JavaScript,双击就能在浏览器里打开。数据存在浏览器的本地存储里。这个方案的好处太明显了:
- 零部署成本,发给任何人就能用
- 不需要网络,断网也能跑
- 数据在本地,没有隐私顾虑
- 调试简单,浏览器开发者工具全搞定
代价是数据不能跨设备同步,换电脑就没了。但对我家这个场景来说,孩子固定用一台旧笔记本,这个代价完全可以接受。
这里有个经验:做个人工具,优先选"零运维"的方案。任何需要你定期维护、重启、续费的东西,最后都会变成负担。
2.3 氛围编程的定位:AI是打字员,你是产品经理和架构师
很多人对氛围编程有误解,以为就是"跟AI说一句话,它给你一个完整应用"。我实测下来完全不是这样。AI更像一个打字速度极快、但需要你明确指挥的初级程序员。它不知道你家孩子上几年级、用什么教材、每天能练多久,这些只有你知道。
我的工作模式是这样的:
- 我先想清楚这一步要做什么,用中文写清楚需求
- 让AI生成代码
- 我在浏览器里跑一遍,看哪里不对
- 把报错信息或不对的现象描述给AI,让它改
- 循环3和4,直到功能正常
整个过程中,我花在"想清楚要什么"上的时间,比花在写代码上的时间多得多。这跟传统开发其实是一样的道理,只是执行环节被大幅加速了。
还有一个关键点:你必须能看懂AI写的代码。不需要会写,但至少要能判断"这段逻辑对不对"。否则AI给你埋个bug,你连从哪查都不知道。我见过有人完全不懂代码就想靠AI做应用,最后卡在一个拼写错误上三天没进展。
3. 核心功能拆解:每个模块到底怎么实现的
3.1 单词表管理:为什么用最简单的文本格式
单词表的录入方式我改了三版。第一版做了个漂亮的表格界面,一行一行填。用了两天就放弃了——录入50个单词要点100次输入框,太累。第二版支持Excel导入,但解析Excel需要引入一个几百KB的库,为了一个低频功能不值得。
最终方案是纯文本粘贴,格式约定为每行"英文,中文",用逗号分隔。比如:
apple,苹果 banana,香蕉 orange,橙子这个方案的好处是,我可以直接从老师发的电子版单词表里复制,稍微整理一下格式就能用。孩子他妈也能操作,不需要学任何新东西。
解析逻辑很简单,按行分割,再按逗号分割,去掉首尾空格。这里有个细节要注意:中文逗号和英文逗号要都支持,因为从不同地方复制来的文本,标点符号经常不统一。我在解析函数里做了兼容处理。
function parseWordList(text) { return text.split('\n') .map(line => line.trim()) .filter(line => line.length > 0) .map(line => { // 同时支持中英文逗号 const parts = line.split(/[,,]/); if (parts.length < 2) return null; return { en: parts[0].trim(), zh: parts[1].trim() }; }) .filter(item => item !== null); }存储用的是localStorage,键名就叫wordlist。每次导入新词表会覆盖旧的,但会弹窗确认,防止误操作。
实操心得:别小看格式兼容这件事。我一开始只支持英文逗号,结果我老婆从某个文档复制过来的词表全是中文逗号,导入后一个词都没有,她以为工具坏了。这种小细节直接决定工具能不能被家人接受。
3.2 听写模式:浏览器语音合成怎么用才靠谱
听写功能的逻辑是:播放单词发音,孩子在纸上写,然后点"显示答案"核对。这里用到了浏览器的SpeechSynthesis接口,不需要任何外部服务。
function speak(text) { const utterance = new SpeechSynthesisUtterance(text); utterance.lang = 'en-US'; utterance.rate = 0.8; // 放慢速度,方便孩子听清 speechSynthesis.speak(utterance); }看起来很简单,但实际用起来有几个坑:
第一个坑是语音包的选择。不同浏览器、不同操作系统自带的英语语音质量差别很大。我测试下来,某些系统自带的语音读单词非常生硬,孩子根本听不清。解决办法是提供一个"语速"和"音色"的选择,让用户自己试。虽然SpeechSynthesis提供的音色有限,但至少能选一个相对清楚的。
第二个坑是连续播放。如果一次点很多个单词,语音会排队播放,但有时候会卡住。我加了一个"停止"按钮,调用speechSynthesis.cancel()清空队列。
第三个坑是移动端。部分手机浏览器需要用户先有一次交互(比如点击)才能播放语音,否则会被拦截。所以我把播放按钮做得很大,确保是明确的用户点击触发。
听写模式的界面设计也改过。最初是"播放-输入-提交"三步,后来发现孩子更习惯在纸上写,所以改成了"播放-显示答案-自己核对"。核对完点"对"或"错",错的自动进错题本。这个流程更符合真实的学习场景。
3.3 选择题模式:干扰项怎么生成才合理
选择题的难点不在题目本身,而在干扰项的质量。如果随便从词表里抽三个词当错误选项,经常会出现"明显不对"的情况,孩子靠排除法就能选对,起不到练习效果。
我的策略是这样的:
- 正确答案:当前单词的中文释义
- 干扰项:从词表中随机抽取3个其他单词的中文释义
- 但会做一个过滤:如果干扰项的释义和正确答案有包含关系(比如"苹果"和"苹果树"),就重新抽
function generateOptions(currentWord, allWords) { const correct = currentWord.zh; const pool = allWords.filter(w => w.zh !== correct && !w.zh.includes(correct) && !correct.includes(w.zh) ); const distractors = shuffle(pool).slice(0, 3).map(w => w.zh); return shuffle([correct, ...distractors]); }这个过滤逻辑看起来简单,但效果提升很明显。我对比过加过滤和不加过滤的版本,孩子做加过滤版本的正确率明显低一些——说明干扰项确实更有迷惑性了。
题目顺序也做了处理:每次练习都重新打乱,避免孩子记住"第三题选B"这种位置记忆。
3.4 错题本与复习:间隔重复的简化实现
错题本是我认为整个工具里最有价值的部分。孩子做错的题会自动记录,包括单词、错误次数、最后一次错误时间。
复习机制我参考了间隔重复的思路,但做了大幅简化。完整的间隔重复算法(比如SM-2)参数太多,对小学生来说没必要。我的规则是:
| 错误次数 | 复习间隔 |
|---|---|
| 1次 | 当天再练一次 |
| 2次 | 隔1天 |
| 3次 | 隔3天 |
| 4次及以上 | 隔7天 |
这个规则写在代码里就是一个简单的查表:
function getReviewInterval(errorCount) { const intervals = [0, 0, 1, 3, 7]; return intervals[Math.min(errorCount, 4)]; }每次打开工具,首页会显示"今天需要复习的单词数量",点进去就是针对这些词的专项练习。练对了错误次数减一,练错了加一。
注意:间隔重复的核心是"在快要忘记的时候复习",但具体间隔因人而异。我上面这套参数是根据我家孩子的情况调的,你可能需要根据自己孩子的记忆特点调整。别照搬,先跑两周看效果。
3.5 数据持久化:localStorage的容量和风险
所有数据都存在localStorage里,包括词表、错题记录、统计数据。这个方案简单,但有两个必须知道的风险:
风险一:容量限制。大多数浏览器给localStorage的配额是5MB左右。纯文本数据其实很省空间,一个单词加释义大概50字节,5MB能存十万个单词,完全够用。但如果以后想加图片、音频,就不行了。
风险二:清除浏览器数据会丢。用户如果清理浏览器缓存,数据就没了。我的应对方案是加了一个"导出数据"按钮,把全部数据转成JSON文件下载下来。换设备或者重装浏览器时,再导入回去。
function exportData() { const data = { wordlist: localStorage.getItem('wordlist'), mistakes: localStorage.getItem('mistakes'), stats: localStorage.getItem('stats') }; const blob = new Blob([JSON.stringify(data)], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'english-tool-backup.json'; a.click(); }这个导出功能我强烈建议你一开始就做。我是在第三周才加的,之前有一次不小心清了缓存,一周的错题记录全没了,孩子还挺伤心的。
4. 实操过程:从零到能用的完整记录
4.1 第一周:搭骨架,跑通最小闭环
第一周的目标很明确:做一个能导入单词、能显示列表、能播放发音的页面。不追求好看,不追求功能全,只要核心流程能跑通。
我先用自然语言把需求写清楚,大意是:"做一个HTML页面,顶部有一个文本输入框用来粘贴单词表,一个导入按钮,下面显示已导入的单词列表,每个单词旁边有一个喇叭图标点击播放发音。"
AI第一次生成的代码大概有200行,包含了HTML、CSS和JavaScript。我保存成index.html,双击打开,发现几个问题:
- 导入按钮点了没反应,因为事件监听绑定的元素ID和HTML里的对不上
- 发音功能在某些浏览器不工作,因为没设置
lang属性 - 单词列表没有样式,挤成一团
我把这些问题一条一条描述给AI,它逐条修改。这个过程大概来回改了七八次,花了两个多小时,最小闭环就跑通了。
这一周最大的收获是建立信心:原来真的可以不做任何环境配置,就靠一个HTML文件做出能用的东西。
4.2 第二周:加练习模式,处理交互细节
第二周开始做听写和选择题两个练习模式。这部分的工作量比第一周大得多,因为涉及状态管理——当前做到第几题、答对几题、答错几题、什么时候结束。
我犯了一个错误:一开始想让AI一次性把两个模式都写完。结果生成的代码有五百多行,逻辑纠缠在一起,改一个地方就崩另一个地方。后来我拆成两步:先只做听写模式,跑通之后再让AI参考听写模式的代码做选择题模式。这样每次改动的范围小,出问题也好定位。
听写模式的交互我改了三次:
- 第一版:播放发音,输入框输入,点提交判断对错
- 第二版:播放发音,点"显示答案"自己核对,点"对"或"错"
- 第三版:在第二版基础上加了"再听一遍"和"跳过"
最终定在第三版。原因是孩子实际用的时候,经常需要听两三遍才能确认,而且有些词他确实不会,跳过比瞎蒙更真实。
选择题模式相对顺利,因为逻辑简单:显示中文,四个选项,点击判断。主要调的是样式——选项按钮要够大,方便触屏点击;正确和错误的反馈颜色要明显但不刺眼。
4.3 第三周:错题本和复习机制
第三周做错题本。这部分的技术难点在于数据结构的设计。错题记录不能只存单词本身,还要存错误次数、最后错误时间、下次复习时间。
我最终用的结构是这样的:
{ "apple": { "en": "apple", "zh": "苹果", "errorCount": 2, "lastError": "2025-01-15", "nextReview": "2025-01-16" } }用单词的英文作为键,方便查找和更新。每次答错,errorCount加一,重新计算nextReview。答对则errorCount减一,减到0就从错题本里移除。
这里有个细节:日期比较要用同一天的零点,否则会出现"今天复习了但明天还显示要复习"的问题。我一开始用时间戳直接比,结果因为时分秒的差异,判断逻辑经常出错。后来统一转成"YYYY-MM-DD"格式的字符串来比较,问题就解决了。
4.4 第四周:打磨体验,处理边界情况
最后一周基本没加新功能,全在修修补补。列几个典型的边界情况:
- 单词表为空时,练习模式应该提示"请先导入单词"而不是直接崩溃
- 单词表只有一个词时,选择题凑不齐四个选项,应该降级为判断题或者直接跳过
- 练习过程中刷新页面,应该保留当前进度而不是从头开始
- 同一个单词在词表里出现两次,导入时应该去重
这些问题在正常使用中不常遇到,但一旦遇到就很影响体验。我花了整整两天时间,把能想到的边界情况都测了一遍。
还有一个体验优化:加载速度。纯前端应用本来就没有加载问题,但我发现如果词表很大(超过500个词),渲染列表会卡顿。解决办法是只渲染前50个,加一个"显示更多"按钮。这个优化让页面打开速度从2秒降到了几乎瞬间。
5. 常见问题与排查技巧实录
5.1 语音不播放怎么办
这是被问得最多的问题。排查顺序如下:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 完全没声音 | 浏览器不支持或语音包未加载 | 换Chrome或Edge,检查系统语音设置 |
| 第一次点没声音,第二次正常 | 移动端需要用户交互 | 确保是点击按钮触发,不要自动播放 |
| 声音断断续续 | 语音队列堵塞 | 播放前调用speechSynthesis.cancel() |
| 发音不准确 | 语音包质量差 | 换一个voice,或降低语速 |
我实测下来,Chrome和Edge的语音合成效果最稳定,Firefox偶尔会有延迟。如果实在搞不定,可以退而求其次,用预录的音频文件,但那样就需要额外的存储和加载逻辑,复杂度上升不少。
5.2 数据丢失的预防和补救
数据丢失主要有三种情况:
情况一:清了浏览器缓存。这是最常见的。预防方法是养成定期导出的习惯,我现在的做法是每周日晚上导出一次备份。
情况二:换了浏览器或设备。localStorage是跟浏览器绑定的,换浏览器数据就不通了。解决办法还是导出再导入。
情况三:代码bug导致数据被覆盖。这个最隐蔽。我有一次改导入逻辑,不小心把"追加"改成了"覆盖",结果导入新词表时把错题记录也清了。后来我在所有写操作前都加了一个确认弹窗,虽然麻烦一点,但安全。
独家技巧:在代码里加一个"回收站"机制。每次覆盖数据前,先把旧数据存到一个
backup键里,保留最近一次。这样万一误操作,还能恢复。这个逻辑很简单,但关键时刻能救命。
5.3 孩子不爱用怎么办
这是非技术问题,但可能比技术问题更重要。我观察下来,孩子愿不愿意用,取决于三点:
第一,启动成本要低。如果每次用都要你帮忙打开、输入密码、选模式,他肯定不用。我的做法是把工具设成浏览器主页,打开浏览器就是它,点一下就能开始。
第二,反馈要即时。答对了要有明确的正面反馈,答错了不要批评。我在选择题里加了音效(用Web Audio API生成的简单音调),答对是清脆的"叮",答错是低沉的"嘟"。孩子对这个反馈很敏感。
第三,练习量要小。一次练50个词,孩子会崩溃。我设的是每次默认10个词,做完就结束,想继续可以再点。短平快的练习比长时间灌输有效得多。
5.4 氛围编程本身的坑
用AI写代码这一个月,我总结了几个教训:
教训一:不要一次提太多需求。一次只让AI做一件事,做完测试通过再提下一个。我试过一次提五个需求,生成的代码互相冲突,排查花了半天。
教训二:报错信息要完整贴给AI。不要只说"报错了",要把浏览器的控制台报错、你做了什么操作、期望什么结果,全部说清楚。信息越全,AI改得越准。
教训三:定期让AI解释代码。有时候AI写的逻辑我看不懂,就让它用中文解释一遍。这不仅能帮我理解,还能发现潜在的逻辑问题。
教训四:版本控制很重要。我没有用Git,而是每次大改之前把文件复制一份,命名为index_v1.html、index_v2.html。虽然土,但有效。有一次改崩了,直接回退到上一版,省了很多时间。
6. 这套方案还能怎么扩展
工具跑了一个多月,孩子的单词默写正确率从最初的60%左右稳定到了85%以上。但我没打算停在这里,后面还有几个想做的方向。
方向一:支持句子和短语。现在只支持单词,但高年级的英语学习越来越侧重句子。扩展的思路是在解析时判断,如果一行里有多个单词,就当作句子处理,练习模式改成"听句子写句子"或者"翻译句子"。
方向二:加一个简单的词频统计。记录哪些词错得最多,生成一个"高频错词"列表,方便针对性突破。这个功能技术上不难,就是遍历错题本按errorCount排序。
方向三:支持多套词表切换。现在导入新词表会覆盖旧的,如果孩子同时学两套教材(比如课本+课外班),就需要能切换。实现方式是把词表存成一个数组,每个元素是一套词表,加一个下拉框选择当前使用哪套。
方向四:家长端查看报告。我老婆经常问"今天孩子练得怎么样",现在只能口头说。可以做一个简单的统计页面,显示最近七天的练习量、正确率、错题分布。这个页面不需要孩子看到,可以藏在一个不显眼的入口里。
这些扩展我打算慢慢做,不着急。工具这东西,够用比功能全重要。每加一个功能,就多一份维护成本,也多一份出bug的风险。我现在的心态是:孩子用着没问题,就先不动,等他确实需要某个新功能了,再花一个周末加上去。
最后分享一个我在这个项目里体会最深的事:做工具的过程,其实是在梳理自己对"学习"这件事的理解。哪些环节是必要的,哪些是商业应用为了盈利硬塞进来的,想清楚这些,工具自然就好做了。我家孩子现在每天花十分钟用这个工具练单词,不抵触,不拖延,我觉得这就够了。