Files
AI_English/todo.md
T
eddy 438b6cb7bb test: 添加核心服务回归测试
- 为存储、词库、记忆调度、MIME 解析、单词提取、测验生成及备份处理添加 Node.js 测试
- 添加可复用的浏览器环境与 localStorage 测试辅助工具
- 修复移除 HTML 时单词边界丢失的问题,并拒绝无效的测验答案
- 校验导入备份中的日期,并添加 npm 测试脚本
2026-07-19 23:57:27 +08:00

18 KiB
Raw Blame History

src/ 重构 TODO

分支:dev-refactor。每个 Phase 结束时应用可正常运行并单独提交一次 commit。 核心约束:不改变任何用户可见行为、不改变 localStorage 数据结构(老用户数据必须无缝兼容)。

进度说明(2026-07-19 核对更新):代码级目标已全部达成。以下为与本文档原计划的已知差异,均已核对/对齐:

  • 提交粒度:实际合并为 4 个 commit 提交(9e5ddef8686e659c4fc0b56a0040),非逐 Phase/逐页粒度;下文各「提交」项据此勾选。
  • CSS 结构:已补齐 pages/home.css/pages/words.css,并把原 components-extended.css/learn-session.css 拆解归位、新增全局 responsive.css(见「三」已更新的目录树)。
  • 路由core/router.js 采用单个 registerPage(page, renderFn),与「六」示例一致。
  • window 桥接:改为一次性重构,终态无桥接残留(与计划最终目标一致)。
  • 仍未完成Phase 0 的 before-refactor tag 未打;尚未合并回 main;「验收清单」等人工冒烟项需实跑。

一、现状分析

文件 行数 问题
src/script.js 5139 单文件巨石:约 230 个函数全部挂在全局作用域,常量/状态/服务/8 个页面混在一起
src/styles.css 3346 单文件,含 40+ 个注释分节,主题变量与页面样式耦合
src/index.html 84 基本干净,仅需改脚本引入方式

关键技术债:

  • 119 处内联 onclick=(另有 7 处 oninput=/onchange=/onkeydown=)写在 JS 拼接的 HTML 字符串里,强依赖函数全局可见 —— 这是拆分 ES 模块的最大障碍。
  • 全局可变单例 statescript.js:97+ 分散的 saveXxx() 持久化函数,无统一入口。
  • TTS 部分(script.js:356-753,约 400 行)内含多个模块级可变缓存(_ttsRequestSeq、云端音频缓存、有道预取缓存),需整体封装。
  • Chart.js 实例 _studyChart 需在重渲染时销毁,生命周期隐蔽。
  • 手动维护缓存版本号 ?v=20260715-1index.html:17、82)。
  • 无测试、无 package.json、无构建工具。

二、重构目标与原则

  • 目标:script.js 已按职责拆为 ES 模块(常量 / 状态存储 / 核心 UI / 领域服务 / 页面);大页面进一步拆出自动播放、批量文件、测验会话/音频和设置数据模块,单文件控制在 ~500 行内。
  • 目标:消灭全部内联 onclick=,改为 data-action 事件委托。
  • 目标:styles.css 按现有注释分节拆分为多个 CSS 文件。
  • 原则:保持无构建(no-build,使用浏览器原生 ES Modules(<script type="module">)。项目本来就依赖 fetch('data/*.json'),已要求 HTTP 静态服务,原生模块无额外成本。(备选方案:引入 Vite,见「八、可选增强」,本轮不做。)
  • 原则:小步迁移。终态已达成——改为一次性重构,最终无 window.fn = fn 桥接残留(结果与计划一致,未走渐进桥接过程;现存 window. 均为合法浏览器 API)。
  • 原则:localStorage 键名与值结构(words/records/schedule/favorites/settings/mailEmails/theme/词库前缀键等)一律不动。

三、目标目录结构

src/
  index.html
  styles/
    variables.css        # :root 主题变量 + [data-theme=dark]styles.css:1-63
    base.css             # Reset & Base
    layout.css           # 布局 / Sidebar / Header / Main
    components.css       # 卡片/按钮/表单/徽章/Toast/Modal/分页/空状态/Loading/复选框/动画/滚动条/开关
    responsive.css       # 全局 @media 响应式(必须在页面样式之后、extract/mail-learn/reader 之前加载)
    pages/
      home.css   words.css   learn.css   quiz.css   review.css
      stats.css  settings.css  extract.css  mail-learn.css  reader.css
      # learn.css 含 Flip Card/Setup Card/Learn Controls/Learn V2quiz.css 含 Batch Selectorhome.css 含 Streak
  js/
    main.js              # 入口:init、全局事件绑定、页面注册
    constants.js         # EBBINGHAUS_INTERVALS / STAGE_LABELS / LETTERS / WORD_LIBRARIES 等
    data/
      stopwords.js       # STOP_WORDS、COMMON_NAMES 两个大 Setscript.js:8-83
    core/
      state.js           # state 单例 + loadStatescript.js:89-222
      storage.js         # loadJsonSetting/saveJsonSetting/词库前缀键/saveXxx 系列
      router.js          # navigate/onHashChange/updateNav/renderPage + 页面注册表
      actions.js         # data-action 事件委托分发器(新增,见 Phase 4
    ui/
      theme.js  sidebar.js  toast.js  modal.js
      dom.js             # escapeHtml/formatMarkdown/autoResizeTextarea/throttle/debounce
    services/
      tts.js             # 全部 TTS:云端/有道/WebSpeech 三级回退 + 缓存 + 预取(script.js:356-753
      ai.js              # callAI/generateAIQuiz/translateWord(s)/filterNamesWithAI/deduplicateWithAI
      ebbinghaus.js      # 日期工具/getDueWords/initWordSchedule/updateSchedule/addRecord/trimRecords
      stats.js           # getMastery/getStats/getStreak/getQuizErrorWords/getLast30DaysData/getErrorTopWords
      quiz-generator.js  # shuffle/generateLocalQuiz/sanitizeAiQuestion/extractJsonValue
      extractor.js       # extractEnglishWords/extractWithFrequency/detectProperNouns/deduplicateBasic/findWordContext
      mime.js            # EML 解析:createTextDecoder/decodeQuotedPrintable/decodeBase64/extractMimeText 等(script.js:3566-3684
      library.js         # 词库加载:getWordLibrary/fetchWordLibrary/switchWordLibrary/autoLoadWordsscript.js:5003-5120
      favorites.js       # isFavorite/toggleFavorite/getFavoriteWords
    pages/
      home.js  words.js  learn.js  quiz.js  review.js
      stats.js  extract.js  mail-learn.js  reader.js  settings.js
      filter-words.js    # 过滤词管理(script.js:4813-4947,从 settings 独立)

四、Phase 0 —— 准备与安全网

  • 打 taggit tag before-refactor,保留回滚点。
  • 写一份手工冒烟测试清单存到 docs/smoke-test.md(见「九、验收清单」),每个 Phase 结束跑一遍。
  • 确认本地静态服务器启动方式(如 python -m http.server 或 VSCode Live Server),记录到 README。
  • 导出一份完整数据备份(设置页「导出全部数据」),用于迁移后做导入回归验证。

五、Phase 1 —— 入口切换 + 纯函数先行

先拆无副作用、无 DOM 依赖的部分,风险最低。

  • index.html:已改为 <script type="module" src="js/main.js">;最终结构已删除迁移期 legacy。
  • module 默认 defer,入口在 DOM 可用后显式执行 init()
  • 新建 js/constants.jsjs/data/stopwords.js
  • 新建 ui/dom.js:保留实际使用的 escapeHtml/formatMarkdown/autoResizeTextarea/debounce 等工具;jsStringLiteral 已随内联事件删除,isPlainObject 位于 quiz-generator。
  • 新建 services/ebbinghaus.js + services/stats.js,错题缓存由 saveWords/saveRecords 失效。
  • 新建 services/quiz-generator.js
  • 新建 services/extractor.jsservices/mime.js
  • 最终结构已删除 legacy.js 与临时全局桥接层。
  • 提交:refactor: 引入 ES 模块入口并拆分纯函数模块(已并入合并提交,见顶部进度说明)。

六、Phase 2 —— 核心服务

  • core/storage.js:实现加载/保存、词库前缀键、legacy fallback 与 saveXxx 系列,并通过 STORAGE_KEYS 集中维护键名。
  • core/state.js 导出共享 state 单例;依赖存储的 loadState() 位于 core/storage.js,避免 state 层反向依赖持久化实现。
  • core/router.jsnavigate/onHashChange/updateNav/renderPage。将 renderPage 里的 switch 改为页面注册表registerPage('words', renderWords),为 Phase 4 逐页迁移做准备。
  • ui/theme.jsui/sidebar.jsui/toast.jsui/modal.jsscript.js:287-355)。
  • services/favorites.jsscript.js:230-244)。
  • 提交:refactor: 拆分存储、状态、路由与基础 UI 模块(已并入合并提交)。

七、Phase 3 —— 领域服务

  • services/tts.js:三级回退、缓存、预取和请求中断状态均已模块私有化;额外导出设置页配置/测试接口与测验预取常量。
  • services/ai.js:AI 调用、出题、翻译、过滤、去重及模块私有冷却状态已迁移;连接测试的 UI 编排保留在 settings 页面。
  • services/library.jsscript.js:5003-5120):词库获取/切换/自动加载。
  • 提交:refactor: 拆分 TTS、AI 客户端与词库服务(已并入合并提交)。

八、Phase 4 —— 页面逐个迁移 + 事件委托(工作量最大)

4.0 事件委托机制(先做)

  • 新建 core/actions.js:在 #page-content#modal-root 上各挂一个 click 委托监听器(另需覆盖 change/input,对应 7 处内联 oninput=/onchange=),按 data-action 分发:
    <!-- 之前 --> <button onclick="deleteWord(3)">
    <!-- 之后 --> <button data-action="word.delete" data-id="3">
    
    registerActions('word', { delete: (el) => deleteWord(Number(el.dataset.id)) });
    
  • 参数一律走 data-*,删除 jsStringLiteral 转义拼接(消灭一类注入/转义 bug)。
  • 命名约定:页面名.动作名,如 quiz.answerlearn.ratesettings.testAI

4.1 逐页迁移(每页一个 commit,模式相同)

每页固定步骤:① 函数搬入页面模块 → ② 内联 onclick 全部改 data-action → ③ 在模块内 registerPage + registerActions → ④ 删除该页函数的 window 桥接 → ⑤ 跑该页冒烟测试。

  • home.jsscript.js:1472-1557):最简单,先拿它验证整套模式。
  • words.jsscript.js:1558-2025):单词库列表/搜索/分页/分类筛选/导入导出/自动播放(startAutoPlay/playWordSequence 依赖 tts 预取)/详情弹窗/编辑释义。注意搜索框 oninput 的 debounce。
  • learn.jsscript.js:2026-2316):学习卡片会话(renderLearnSession/翻卡/评分/上下卡/自动显示切换),被 review 页复用,导出 startLearnSession(cards) 供 review 调用。键盘快捷键监听(script.js:4956-5001)搬入此模块,仅在会话激活时生效。
  • quiz.jsscript.js:2317-2908):批次选择器/本地与 AI 出题/答题流程/音频预取窗口/结果页。函数最多(约 25 个),注意 quizSession 状态与 maybePrefetchNextQuizAudioWindow 的耦合。
  • review.jsscript.js:2909-3057):错题列表 + 调 learn.js 启动复习会话。
  • stats.js(页面)script.js:3058-3302):图表渲染。Chart 实例改为模块内私有,renderStudyChart 前先 destroy() 旧实例(保持现有行为)。
  • extract.jsscript.js:3303-3831):邮件提词/词 chips 选择/批量 EML 解析(依赖 services/mime.js/AI 翻译入库。
  • mail-learn.jsscript.js:3832-3992):已存邮件管理/AI 分析/全文翻译/高亮。
  • reader.jsscript.js:3993-4304):全文阅读模式(分句/朗读/逐句翻译/生词 tooltip)。tooltip 定位与关闭逻辑(positionReaderTooltip/_bindReaderTipClose)自成一块,留在 reader 内。
  • settings.jsscript.js:4305-4812):设置表单/TTS 与 AI 连接测试/数据导入导出/清空数据/强制刷新缓存。
  • filter-words.jsscript.js:4813-4947):自定义过滤词管理弹窗。
  • 全部页面完成后:删除 legacy.js 与整个 window 桥接层;全局 grep -n "onclick=\|window\." src/js 复查,确认无残留。
  • 提交(每页一个):refactor: 迁移 XX 页至独立模块并移除内联事件(实际为合并提交,非逐页粒度)。

九、Phase 5 —— CSS 拆分

  • 按「三、目标目录结构」把 styles.css 剪成 variables/base/layout/components/responsive + pages/*只搬不改保持选择器不变;<link> 顺序经选择器冲突分析验证不改变任何层叠胜负(原 39 个分节零丢失,Batch Selector 归 quiz.css、全局 @media 归 responsive.css 并保持靠后加载)。
  • index.html 用多个 <link> 按序引入(无构建方案下 @import 会串行阻塞,不用)。
  • 清理已知废弃样式:styles.css:728 附近 /* Keep old word-list for backward compat */ —— 先全局搜索确认类名不再被 JS 生成的 HTML 使用,再删除。
  • 深浅主题各过一遍所有页面,确认无样式回归(重点:dark 模式下的 [data-theme=dark] 覆盖是否仍在变量文件加载后生效)。
  • 提交:refactor: 按组件与页面拆分样式表(已并入合并提交)。

十、Phase 6 —— 清理与收尾

  • 缓存版本策略:入口 main.js/CSS 保留 ?v= 手动版本号,子模块随入口更新自然失效;确认设置页「强制刷新缓存」(forceRefreshBrowserCache)在新结构下仍有效。
  • 死代码清扫:已检查导出与 action 调用,删除未使用的 throttle,并保留有实际调用方的 AI 冷却查询和批次辅助函数。
  • 命名统一:内部函数去掉 _ 前缀(模块私有性已由 ES 模块保证)。
  • 为每个 services 模块头部补一段 JSDoc 说明(输入/输出/副作用/依赖的 localStorage 键)。
  • 更新 README:新目录结构、本地启动方式、模块职责表。
  • 全量跑一遍「验收清单」,导入 Phase 0 的备份数据验证兼容性。
  • 提交:docs: 更新 README 反映模块化结构,然后合并回 main。(README 已更新;尚未合并回 main,仍在 dev-refactor。)

十一、可选增强(本轮不做,另开任务)

  • 引入 Vite:解决模块数量多时的请求瀑布与缓存指纹问题,vite build 产物仍是纯静态文件。
  • 添加 node:test 单元测试(54 项):覆盖 ebbinghaus / quiz-generator / mime / extractor,以及 storage 数据兼容与异常、复习状态持久化回滚、备份导入导出与清空、词库切换/加载/竞态回滚;测试发现并修复相邻 HTML 标签导致单词粘连、空 AI 答案被误判为索引 0、备份接受无效日历日期的问题。
  • state 写入收口为 action 函数,配合 saveXxx 自动持久化。
  • HTML 模板字符串改为 <template> 或轻量渲染函数,减少字符串拼接。
  • PWAmanifest + Service Worker)替代手动 ?v= 缓存控制。

十二、风险与注意事项

  • 内联 onclick 是最大雷区:模块化后函数不再全局可见,任何一处漏改都表现为「点了没反应 + console 报 ReferenceError」。迁移期靠 window 桥接兜底,每页迁完立即删桥接暴露漏网之鱼。
  • 函数重名stats 既是页面又是服务、renderStatsgetStats 等,拆分时靠模块路径区分,import 时注意别名。
  • localStorage 兼容getLibraryStorageKey 的词库前缀键逻辑(script.js:167-186,含 legacy fallback)搬移时逐行对照,迁移前后用同一份浏览器 Profile 验证老数据可读。
  • 顶层副作用legacy 里 init() 调用、事件绑定都在顶层执行,拆分时统一收进 main.js 的显式 init(),避免 import 顺序引发的隐式依赖。
  • TTS 并发状态_ttsRequestSeq 的中断语义(快速连点只播最后一个)容易在搬移时弄丢,迁移后专门测「连续快速点击多个单词发音」。
  • file:// 协议不可用ES 模块要求 HTTP 服务;README 需写明(现状 fetch 词库其实已有此要求)。

十三、验收清单(每 Phase 结束执行)

  • 8 个页面(仪表盘/单词库/AI 测试/错题复习/邮件提词/邮件学习/统计/设置)hash 路由均可进入、返回。
  • 单词库:搜索、分类筛选、翻页、收藏、删除、详情弹窗、编辑释义、自动播放(含滚动跟随)、JSON 导入/导出。
  • 测验:本地出题 + AI 出题(配置 key 后)、三种模式切换、批次选择、答题/上一题/下一题、结果页、发音自动预取。
  • 复习:错题列表、开始复习会话、翻卡/评分(1/2/3 键与空格/方向键快捷键)、完成页。
  • 邮件提词:粘贴文本提词、EML 批量导入解析、chips 全选/反选、AI 翻译入库。
  • 邮件学习:保存/载入/删除邮件、AI 分析、全文翻译、全文阅读模式(分句朗读、tooltip、加词)。
  • 统计:卡片数据、30 天图表(深浅主题下重绘正常)。
  • 设置:保存 AI/TTS 配置、连接测试、词库切换、全量导出/导入、清空数据、过滤词管理。
  • 全局:深浅主题切换、移动端侧栏开合、Toast、Esc 关弹窗、刷新后 state 完整恢复。

十四、本次逐任务提交记录

  • Phase 0:准备与安全网(人工项已由用户验证)。
  • Phase 1:引入模块入口并拆分常量、DOM 与纯函数服务。
  • Phase 2:拆分存储、状态、路由与基础 UI 模块。
  • Phase 3:拆分 TTS、AI 与词库领域服务。
  • Phase 4.0:建立 data-action 事件委托机制。
  • Phase 4.1:迁移仪表盘页面。
  • Phase 4.2:迁移单词库页面与自动播放模块。
  • Phase 4.3:迁移学习会话与快捷键。
  • Phase 4.4:迁移测验页面、会话与音频预取。
  • Phase 4.5:迁移错题复习页面。
  • Phase 4.6:迁移统计页面并管理图表生命周期。
  • Phase 4.7:迁移邮件提词与批量文件处理。
  • Phase 4.8:迁移邮件学习页面。
  • Phase 4.9:迁移全文阅读页面。
  • Phase 4.10:迁移设置与数据管理页面。
  • Phase 4.11:迁移自定义过滤词管理。
  • Phase 4.12:注册全部页面并确认无内联事件或全局桥接残留。
  • Phase 5:按组件与页面拆分样式表(人工主题检查已由用户验证)。
  • Phase 6:完成缓存、死代码、命名、服务说明与 README 收尾(人工验收已由用户验证)。
  • 最终审查:修复旧版浏览器因缺少 structuredClone 无法加载词库的问题。
  • 补充审查:修复跨词库清理、空词库进度、备份往返、文件读取竞态与持久化回滚问题。