单文件 HTML 学习应用
把学习/记忆类工具做成一个 HTML 文件——双击即可离线运行,再发布到静态链接供手机使用。
本技能固化了完整流水线,以及一路踩出来的坑。
何时使用
- 用户想要背单词 / 闪卡 / 刷题 / 听写工具,尤其是「给孩子做的」。
- 用户要求能在手机上用、离线可用、不用注册账号。
- 用户要求扩展现有的单文件学习应用(加新题型、加游戏化、换内容)。
- 任何「背诵 / 默写 / 刷题」类工具,且内容集有边界、提前已知。
核心架构决策(不必再讨论)
| 决策项 | 选择 | 理由 |
|---|---|---|
| 交付形态 | 单个 .html,JS/CSS/词库全部内联 |
双击即用;file:// 下外部请求会被 CORS 拦截,所以一律内联 |
| 数据持久化 | localStorage 单键存储,加载时 Object.assign(fresh(), parsed) |
刷新不丢;新增字段不会弄坏旧存档 |
| 记忆算法 | SM-2 | 标准、实现极小、效果可证优于固定间隔 |
| 内容存放 | 内嵌 JS 对象字面量,不用 JSON 文件 | 彻底避免 fetch |
| 发布方式 | 静态托管一个只放 index.html 的 _site/ 目录 |
手机可访问,无需维护服务器 |
| 进度同步 | 默认只存本地,除非用户明确要求多设备 | 一道账号墙就能毁掉孩子的日常使用 |
绝不为此提议 React / Vite / npm。「单文件」本身就是这个技能的价值所在。
第 0 阶段 — 写代码前先钉死需求
一次性问清,且只问会改变设计的:
- 目标设备 —— 手机 / 平板 / 电脑?(决定触摸目标尺寸、视口、安全区)
- 内容来源 —— 课本词表的照片?表格?已有文本?
- 内容规模 —— 几十条还是几千条?(上千条需要分页 + 懒渲染)
- 谁操作 —— 孩子独立用,还是家长带着用?(决定要不要 PIN 码 / 家长区、是否避免登录)
- 激励手段 —— 积分?等级?解锁物?现实奖励?
如果用户表述含糊,先把你的理解写成编号清单,确认后再动手。
第 1 阶段 — 内容数字化
把来源转成一个结构化数组。单条目标结构:
{id:"u1-03", en:"overworked", zh:"工作过度的", pos:"adj.",
phon:"/ˌəʊ.vəˈwɜːkt/", syll:["o","ver","worked"], colloc:["overworked and underpaid"],
examples:[{en:"I'm overworked and underpaid.", zh:"我干得太多,挣得太少。", src:"cam"}]}
关键规则:
id必须唯一且稳定 —— 它是该卡片记忆状态在 localStorage 里的键。由单元 + 序号派生(u3-07),绝不只靠数组下标。- 例句要标注来源(
src:"cam"来自词典 /src:"ai"为生成),并在界面上显示出来。绝不让生成的例句冒充权威来源。 - 未经核实的内容要明确标注(如「AI 例句 · 待核」),不要默默当真发出。
- 构建前先校验数据:id 唯一、字段不缺、每条例句中英俱全。这些校验要放进冒烟测试(第 3 阶段),而不是写个一次性脚本。
照片转文字时,先 OCR 再人工核对,并检查课本是否跳号(比如没有第 5 / 10 / 15 单元),确保单元列表与现实一致。
第 2 阶段 — 构建应用
单个 <script> 内的标准模块顺序:
BOOK内容对象 → 由它派生ALL_WORDS、UNIT_IDS- 状态层:
LS_KEY、load()、fresh()、save()、today() - SM-2:
card()、isDue()、mastered()、sm2()—— 见references/sm2-and-gamification.md - TTS 层 —— 见
references/mobile-tts.md(务必做对;这是移动端头号 bug) - 音频 / 录音层(MediaRecorder + SpeechRecognition,特性检测 + 优雅降级)
- 视图渲染函数,一个视图一个函数
- 底部标签导航;每个视图都按当前选中单元过滤
不可妥协项:
- 触摸目标 ≥ 44px;主操作放在拇指够得着的位置。长页面的导航放底部,不放顶部。
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">- 用
env(safe-area-inset-bottom)适配 iPhone 底部横条。 -webkit-tap-highlight-color:transparent+@media(hover:none)反馈,因为触摸屏没有 hover。- 绝不在界面上泄露答案:候选词下拉必须显示占位符(
词01),不能显示答案本身。这类 bug 极易引入,而且在单题截图里根本看不出来。
第 3 阶段 — 三层验证(每次改版必做)
三层全跑。跳过第三层就是 UI bug 出货的原因。
| 层级 | 做什么 | 能抓住什么 |
|---|---|---|
| 1 | 抽出 <script>,跑 node --check |
写文件时的字符损坏、语法错误 |
| 2 | 合并 mock + 应用代码 + 断言 → 在 node 里跑 | 逻辑回归(SM-2、队列、计分) |
| 3 | puppeteer-core + 本地 Edge/Chrome,走真实点击路径、截图、收集 console 报错 |
DOM / 渲染 bug、顺序错误、布局问题、静默 JS 错误 |
第 2、3 层的模板与确切的 mock 垫片:references/verification-pipeline.md。
关键:第 3 层必须驱动完整循环,不是只走一步。曾经一个漏掉的 idx++ 让应用永远卡在第 1 题,而因为只测了一次点击,完全看不出来。要断言状态迁移(q1 id != q2 id),再断言能绕回开头。
长 E2E 运行:用流式进度输出(process.stdout.write("\r题 N/M")),不要重定向到日志文件。用户会把静默数分钟的任务合理地理解为卡死。
第 4 阶段 — 上线
- 保留一个
_site/目录,里面只放index.html(应用的一份拷贝)。发布该目录。 - 每次改内容之后:
cp <app>.html _site/index.html然后重新发布。线上那份是快照,否则会悄悄漂移。 - 重新发布每次都要重新征得用户同意 —— 之前同意过不代表授权之后覆盖线上链接。要问。
- 告诉用户在手机上「添加到主屏幕」:图标比浏览器标签页能拿到更持久的存储,且能躲过常规缓存清理。
踩出来的坑
- 写文件的字符串损坏 —— JS 字符串拼接可能丢
+或吞掉|。每次写完务必跑node --check。正则择一分支用字符类,或用chr(124)拼出竖线。 - 测试里的全局作用域 vs 模块作用域 ——
window.foo = x和顶层的let foo是两个不同的绑定。断言里用window.赋值、而应用读的是模块变量,会失败得莫名其妙。要直接给模块变量赋值。 innerHTML会重新序列化属性 —— 浏览器会把class='x'改写成class="x",而 node mock 不会。对标记做字符串断言时必须两种引号都容忍(/class=["']x/)或匹配裸子串。否则冒烟过、E2E 却假失败。replace()是全局替换 —— 用str.replace()删一个行尾的render();,会把函数体里所有同名调用一起删掉。用endswith判断 + 切片。- 测试必须重置状态 —— E2E 开始前清掉 localStorage 并重建
fresh(),否则结果取决于上一次跑剩的东西。 - 别用脚本名当输出名 —— 有个写
_extract.js的脚本曾经把自己覆盖掉了。 - 只剩空白的测试文件 —— 删掉某个辅助函数后要重跑合并步骤;过期的合并文件会给出言之凿凿的错误结果。
参考文件
references/mobile-tts.md—— 自动播放解锁模式、GC / 暂停 / 声线加载的加固,以及如何诊断「手机上没声音」。references/sm2-and-gamification.md—— SM-2 实现、真正能排空的错题池、星级 / 等级计分、连击、货币、皮肤解锁、家长区。references/verification-pipeline.md—— mock 垫片、合并脚本、冒烟测试形态、E2E 骨架、断言写法。CHANGELOG.md—— 演进史:这个技能能做什么、每条规则由哪次真实失败催生、还差什么。在新科目上复用本技能前先读它 —— 它会告诉你哪些部分是已验证的、哪些还只是假设。
维护本技能(强制)
任何对 SKILL.md 或 references/ 的修改,都必须在同一轮同步更新 CHANGELOG.md:
- 在版本表里新增或更新一行。
- 新增一节,写清改了什么、由哪次真实任务或失败触发 —— 绝不写「为了完善」,必须写具体事件。
- 若旧规则被推翻,不要删除,标注
[已被 vX 取代:原因]以保留演进痕迹。 - 更新「已知不足」一节。
- 重新校验并重新打包:
bash python "<skill-creator>/scripts/package_skill.py" "<本 skill 目录>"
该 zip 会落在当前工作目录且不带版本号 —— 记得改名。
版本号规则:规则只经单个项目验证时用 0.x;只有在第二个不同科目上成功复用且无需改规则后,才升到 1.0。
> 注:本技能 2026-10-14 首次发布至 SkillHub 时,为对外发布需要稳定版本号,直接以 1.0.0 发布(未经过第二科目复用验证)。上方规则本身继续有效,仍在等待验证 —— 请勿据此认为该技能已达到规则意义上的稳定版。