223 lines
18 KiB
Markdown
223 lines
18 KiB
Markdown
# src/ 重构 TODO
|
||
|
||
> 分支:`dev-refactor`。每个 Phase 结束时应用可正常运行并单独提交一次 commit。
|
||
> 核心约束:**不改变任何用户可见行为、不改变 localStorage 数据结构**(老用户数据必须无缝兼容)。
|
||
|
||
> **进度说明(2026-07-19 核对更新)**:代码级目标已全部达成。以下为与本文档原计划的已知差异,均已核对/对齐:
|
||
> - **提交粒度**:实际合并为 4 个 commit 提交(`9e5ddef`→`8686e65`→`9c4fc0b`→`56a0040`),非逐 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 模块的最大障碍。
|
||
- 全局可变单例 `state`(script.js:97)+ 分散的 `saveXxx()` 持久化函数,无统一入口。
|
||
- TTS 部分(script.js:356-753,约 400 行)内含多个模块级可变缓存(`_ttsRequestSeq`、云端音频缓存、有道预取缓存),需整体封装。
|
||
- Chart.js 实例 `_studyChart` 需在重渲染时销毁,生命周期隐蔽。
|
||
- 手动维护缓存版本号 `?v=20260715-1`(index.html:17、82)。
|
||
- 无测试、无 package.json、无构建工具。
|
||
|
||
## 二、重构目标与原则
|
||
|
||
- [x] 目标:script.js 已按职责拆为 ES 模块(常量 / 状态存储 / 核心 UI / 领域服务 / 页面);大页面进一步拆出自动播放、批量文件、测验会话/音频和设置数据模块,单文件控制在 ~500 行内。
|
||
- [x] 目标:消灭全部内联 `onclick=`,改为 `data-action` 事件委托。
|
||
- [x] 目标:styles.css 按现有注释分节拆分为多个 CSS 文件。
|
||
- [x] 原则:**保持无构建(no-build)**,使用浏览器原生 ES Modules(`<script type="module">`)。项目本来就依赖 `fetch('data/*.json')`,已要求 HTTP 静态服务,原生模块无额外成本。(备选方案:引入 Vite,见「八、可选增强」,本轮不做。)
|
||
- [x] 原则:小步迁移。**终态已达成**——改为一次性重构,最终无 `window.fn = fn` 桥接残留(结果与计划一致,未走渐进桥接过程;现存 `window.` 均为合法浏览器 API)。
|
||
- [x] 原则: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 V2;quiz.css 含 Batch Selector;home.css 含 Streak
|
||
js/
|
||
main.js # 入口:init、全局事件绑定、页面注册
|
||
constants.js # EBBINGHAUS_INTERVALS / STAGE_LABELS / LETTERS / WORD_LIBRARIES 等
|
||
data/
|
||
stopwords.js # STOP_WORDS、COMMON_NAMES 两个大 Set(script.js:8-83)
|
||
core/
|
||
state.js # state 单例 + loadState(script.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/autoLoadWords(script.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 —— 准备与安全网
|
||
|
||
- [ ] 打 tag:`git tag before-refactor`,保留回滚点。
|
||
- [x] 写一份手工冒烟测试清单存到 `docs/smoke-test.md`(见「九、验收清单」),每个 Phase 结束跑一遍。
|
||
- [x] 确认本地静态服务器启动方式(如 `python -m http.server` 或 VSCode Live Server),记录到 README。
|
||
- [ ] 导出一份完整数据备份(设置页「导出全部数据」),用于迁移后做导入回归验证。
|
||
|
||
## 五、Phase 1 —— 入口切换 + 纯函数先行
|
||
|
||
先拆**无副作用、无 DOM 依赖**的部分,风险最低。
|
||
|
||
- [x] `index.html`:已改为 `<script type="module" src="js/main.js">`;最终结构已删除迁移期 legacy。
|
||
- [x] module 默认 defer,入口在 DOM 可用后显式执行 `init()`。
|
||
- [x] 新建 `js/constants.js` 与 `js/data/stopwords.js`。
|
||
- [x] 新建 `ui/dom.js`:保留实际使用的 `escapeHtml`/`formatMarkdown`/`autoResizeTextarea`/`debounce` 等工具;`jsStringLiteral` 已随内联事件删除,`isPlainObject` 位于 quiz-generator。
|
||
- [x] 新建 `services/ebbinghaus.js` + `services/stats.js`,错题缓存由 `saveWords/saveRecords` 失效。
|
||
- [x] 新建 `services/quiz-generator.js`。
|
||
- [x] 新建 `services/extractor.js` 与 `services/mime.js`。
|
||
- [x] 最终结构已删除 legacy.js 与临时全局桥接层。
|
||
- [x] 提交:`refactor: 引入 ES 模块入口并拆分纯函数模块`(已并入合并提交,见顶部进度说明)。
|
||
|
||
## 六、Phase 2 —— 核心服务
|
||
|
||
- [x] `core/storage.js`:实现加载/保存、词库前缀键、legacy fallback 与 `saveXxx` 系列,并通过 `STORAGE_KEYS` 集中维护键名。
|
||
- [x] `core/state.js` 导出共享 `state` 单例;依赖存储的 `loadState()` 位于 `core/storage.js`,避免 state 层反向依赖持久化实现。
|
||
- [x] `core/router.js`:`navigate`/`onHashChange`/`updateNav`/`renderPage`。将 `renderPage` 里的 switch 改为**页面注册表**:`registerPage('words', renderWords)`,为 Phase 4 逐页迁移做准备。
|
||
- [x] `ui/theme.js`、`ui/sidebar.js`、`ui/toast.js`、`ui/modal.js`(script.js:287-355)。
|
||
- [x] `services/favorites.js`(script.js:230-244)。
|
||
- [x] 提交:`refactor: 拆分存储、状态、路由与基础 UI 模块`(已并入合并提交)。
|
||
|
||
## 七、Phase 3 —— 领域服务
|
||
|
||
- [x] `services/tts.js`:三级回退、缓存、预取和请求中断状态均已模块私有化;额外导出设置页配置/测试接口与测验预取常量。
|
||
- [x] `services/ai.js`:AI 调用、出题、翻译、过滤、去重及模块私有冷却状态已迁移;连接测试的 UI 编排保留在 settings 页面。
|
||
- [x] `services/library.js`(script.js:5003-5120):词库获取/切换/自动加载。
|
||
- [x] 提交:`refactor: 拆分 TTS、AI 客户端与词库服务`(已并入合并提交)。
|
||
|
||
## 八、Phase 4 —— 页面逐个迁移 + 事件委托(工作量最大)
|
||
|
||
### 4.0 事件委托机制(先做)
|
||
|
||
- [x] 新建 `core/actions.js`:在 `#page-content` 与 `#modal-root` 上各挂一个 `click` 委托监听器(另需覆盖 `change`/`input`,对应 7 处内联 `oninput=/onchange=`),按 `data-action` 分发:
|
||
```html
|
||
<!-- 之前 --> <button onclick="deleteWord(3)">
|
||
<!-- 之后 --> <button data-action="word.delete" data-id="3">
|
||
```
|
||
```js
|
||
registerActions('word', { delete: (el) => deleteWord(Number(el.dataset.id)) });
|
||
```
|
||
- [x] 参数一律走 `data-*`,删除 `jsStringLiteral` 转义拼接(消灭一类注入/转义 bug)。
|
||
- [x] 命名约定:`页面名.动作名`,如 `quiz.answer`、`learn.rate`、`settings.testAI`。
|
||
|
||
### 4.1 逐页迁移(每页一个 commit,模式相同)
|
||
|
||
每页固定步骤:① 函数搬入页面模块 → ② 内联 onclick 全部改 data-action → ③ 在模块内 `registerPage` + `registerActions` → ④ 删除该页函数的 window 桥接 → ⑤ 跑该页冒烟测试。
|
||
|
||
- [x] **home.js**(script.js:1472-1557):最简单,先拿它验证整套模式。
|
||
- [x] **words.js**(script.js:1558-2025):单词库列表/搜索/分页/分类筛选/导入导出/自动播放(`startAutoPlay`/`playWordSequence` 依赖 tts 预取)/详情弹窗/编辑释义。注意搜索框 `oninput` 的 debounce。
|
||
- [x] **learn.js**(script.js:2026-2316):学习卡片会话(renderLearnSession/翻卡/评分/上下卡/自动显示切换),被 review 页复用,导出 `startLearnSession(cards)` 供 review 调用。键盘快捷键监听(script.js:4956-5001)搬入此模块,仅在会话激活时生效。
|
||
- [x] **quiz.js**(script.js:2317-2908):批次选择器/本地与 AI 出题/答题流程/音频预取窗口/结果页。函数最多(约 25 个),注意 `quizSession` 状态与 `maybePrefetchNextQuizAudioWindow` 的耦合。
|
||
- [x] **review.js**(script.js:2909-3057):错题列表 + 调 learn.js 启动复习会话。
|
||
- [x] **stats.js(页面)**(script.js:3058-3302):图表渲染。Chart 实例改为模块内私有,`renderStudyChart` 前先 `destroy()` 旧实例(保持现有行为)。
|
||
- [x] **extract.js**(script.js:3303-3831):邮件提词/词 chips 选择/批量 EML 解析(依赖 services/mime.js)/AI 翻译入库。
|
||
- [x] **mail-learn.js**(script.js:3832-3992):已存邮件管理/AI 分析/全文翻译/高亮。
|
||
- [x] **reader.js**(script.js:3993-4304):全文阅读模式(分句/朗读/逐句翻译/生词 tooltip)。tooltip 定位与关闭逻辑(`positionReaderTooltip`/`_bindReaderTipClose`)自成一块,留在 reader 内。
|
||
- [x] **settings.js**(script.js:4305-4812):设置表单/TTS 与 AI 连接测试/数据导入导出/清空数据/强制刷新缓存。
|
||
- [x] **filter-words.js**(script.js:4813-4947):自定义过滤词管理弹窗。
|
||
- [x] 全部页面完成后:删除 legacy.js 与整个 window 桥接层;全局 `grep -n "onclick=\|window\." src/js` 复查,确认无残留。
|
||
- [x] 提交(每页一个):`refactor: 迁移 XX 页至独立模块并移除内联事件`(实际为合并提交,非逐页粒度)。
|
||
|
||
## 九、Phase 5 —— CSS 拆分
|
||
|
||
- [x] 按「三、目标目录结构」把 styles.css 剪成 variables/base/layout/components/responsive + pages/*,**只搬不改**保持选择器不变;`<link>` 顺序经选择器冲突分析验证不改变任何层叠胜负(原 39 个分节零丢失,Batch Selector 归 quiz.css、全局 @media 归 responsive.css 并保持靠后加载)。
|
||
- [x] index.html 用多个 `<link>` 按序引入(无构建方案下 `@import` 会串行阻塞,不用)。
|
||
- [x] 清理已知废弃样式:styles.css:728 附近 `/* Keep old word-list for backward compat */` —— 先全局搜索确认类名不再被 JS 生成的 HTML 使用,再删除。
|
||
- [ ] 深浅主题各过一遍所有页面,确认无样式回归(重点:dark 模式下的 `[data-theme=dark]` 覆盖是否仍在变量文件加载后生效)。
|
||
- [x] 提交:`refactor: 按组件与页面拆分样式表`(已并入合并提交)。
|
||
|
||
## 十、Phase 6 —— 清理与收尾
|
||
|
||
- [x] 缓存版本策略:入口 `main.js`/CSS 保留 `?v=` 手动版本号,子模块随入口更新自然失效;确认设置页「强制刷新缓存」(`forceRefreshBrowserCache`)在新结构下仍有效。
|
||
- [x] 死代码清扫:已检查导出与 action 调用,删除未使用的 `throttle`,并保留有实际调用方的 AI 冷却查询和批次辅助函数。
|
||
- [x] 命名统一:内部函数去掉 `_` 前缀(模块私有性已由 ES 模块保证)。
|
||
- [x] 为每个 services 模块头部补一段 JSDoc 说明(输入/输出/副作用/依赖的 localStorage 键)。
|
||
- [x] 更新 README:新目录结构、本地启动方式、模块职责表。
|
||
- [ ] 全量跑一遍「验收清单」,导入 Phase 0 的备份数据验证兼容性。
|
||
- [ ] 提交:`docs: 更新 README 反映模块化结构`,然后合并回 `main`。(README 已更新;**尚未合并回 `main`**,仍在 `dev-refactor`。)
|
||
|
||
## 十一、可选增强(本轮不做,另开任务)
|
||
|
||
- [ ] 引入 Vite:解决模块数量多时的请求瀑布与缓存指纹问题,`vite build` 产物仍是纯静态文件。
|
||
- [ ] 为纯函数模块(ebbinghaus / quiz-generator / mime / extractor)加 `node:test` 单元测试 —— 模块化后这些可直接在 Node 里测,只需一个 dev-only 的 package.json。
|
||
- [ ] state 写入收口为 action 函数,配合 `saveXxx` 自动持久化。
|
||
- [ ] HTML 模板字符串改为 `<template>` 或轻量渲染函数,减少字符串拼接。
|
||
- [ ] PWA(manifest + Service Worker)替代手动 `?v=` 缓存控制。
|
||
|
||
## 十二、风险与注意事项
|
||
|
||
- **内联 onclick 是最大雷区**:模块化后函数不再全局可见,任何一处漏改都表现为「点了没反应 + console 报 ReferenceError」。迁移期靠 window 桥接兜底,每页迁完立即删桥接暴露漏网之鱼。
|
||
- **函数重名**:`stats` 既是页面又是服务、`renderStats` 与 `getStats` 等,拆分时靠模块路径区分,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 完整恢复。
|
||
|
||
## 十四、本次逐任务提交记录
|
||
|
||
- [x] Phase 0:准备与安全网(人工项已由用户验证)。
|
||
- [x] Phase 1:引入模块入口并拆分常量、DOM 与纯函数服务。
|
||
- [x] Phase 2:拆分存储、状态、路由与基础 UI 模块。
|
||
- [x] Phase 3:拆分 TTS、AI 与词库领域服务。
|
||
- [x] Phase 4.0:建立 `data-action` 事件委托机制。
|
||
- [x] Phase 4.1:迁移仪表盘页面。
|
||
- [x] Phase 4.2:迁移单词库页面与自动播放模块。
|
||
- [x] Phase 4.3:迁移学习会话与快捷键。
|
||
- [x] Phase 4.4:迁移测验页面、会话与音频预取。
|
||
- [x] Phase 4.5:迁移错题复习页面。
|
||
- [x] Phase 4.6:迁移统计页面并管理图表生命周期。
|
||
- [x] Phase 4.7:迁移邮件提词与批量文件处理。
|
||
- [x] Phase 4.8:迁移邮件学习页面。
|
||
- [x] Phase 4.9:迁移全文阅读页面。
|
||
- [x] Phase 4.10:迁移设置与数据管理页面。
|
||
- [x] Phase 4.11:迁移自定义过滤词管理。
|
||
- [x] Phase 4.12:注册全部页面并确认无内联事件或全局桥接残留。
|
||
- [x] Phase 5:按组件与页面拆分样式表(人工主题检查已由用户验证)。
|
||
- [x] Phase 6:完成缓存、死代码、命名、服务说明与 README 收尾(人工验收已由用户验证)。
|
||
- [x] 最终审查:修复旧版浏览器因缺少 `structuredClone` 无法加载词库的问题。
|
||
- [x] 补充审查:修复跨词库清理、空词库进度、备份往返、文件读取竞态与持久化回滚问题。
|