|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +categories: [杂项] # 文章分类,tags的替代 |
| 4 | +author: abining |
| 5 | +title: 用油猴脚本为 Duome Stories 添加键盘音频控制:从痛点到优化 |
| 6 | +header-style: text |
| 7 | +catalog: true |
| 8 | +tags: |
| 9 | + - Duome |
| 10 | + - 多邻国 |
| 11 | + - Tampermonkey |
| 12 | + - 音频预加载 |
| 13 | +--- |
| 14 | + |
| 15 | +# 🎧 用油猴脚本为 Duome Stories 添加键盘音频控制:从痛点到优化 |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## ✨ 背景:从 Duome 学英语的真实痛点出发 |
| 20 | + |
| 21 | +[Duome.eu/stories](https://duome.eu/stories) 是我在学习英语时频繁使用的非官方资源网站,提供了丰富的语音故事。每个句子都配有音频,理论上是沉浸式学习的绝佳方式,但实际体验却有明显瑕疵。 |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## 😫 我的两个核心痛点 |
| 26 | + |
| 27 | +### 1. **播放音频延迟太久** |
| 28 | + |
| 29 | +页面中每个句子的音频都是通过浏览器 `GET` 请求以 `206 Partial Content` 方式加载。这意味着音频每次播放都会建立新连接,而 **不会使用缓存(除非之前已经加载)**。 |
| 30 | + |
| 31 | +* 实测每次点击音频播放,**平均等待约 1000ms**,最慢时可达 **1.5 秒** 。 |
| 32 | + |
| 33 | + |
| 34 | + |
| 35 | +* 页面没有预加载,导致初次体验非常差,通常点击之后要等待一秒钟才播放音频,频繁打断思路。 |
| 36 | +* 虽然浏览器会缓存 `206` 音频,但仅在 **播放过一次** 后有效。 |
| 37 | + |
| 38 | +📌 **我的目标**是:**在页面加载时预先把所有音频下载好**,后续播放直接使用内存中的音频对象,彻底消除初次播放延迟。 |
| 39 | + |
| 40 | +--- |
| 41 | + |
| 42 | +### 2. **缺乏键盘操作与视觉反馈** |
| 43 | + |
| 44 | +Duome 的页面里布满了几十个音频按钮,全部需要鼠标点击。没有快捷键意味着: |
| 45 | + |
| 46 | +* 无法像播放器一样通过键盘播放/暂停/切换 |
| 47 | +* 不知道当前播放的是哪一句话,没有视觉提示 |
| 48 | + |
| 49 | +📌 我的第二个目标是:**实现快捷键控制 + 播放时高亮当前音频句子**。 |
| 50 | + |
| 51 | +--- |
| 52 | + |
| 53 | +## 🧠 功能设计目标 |
| 54 | + |
| 55 | +于是我动手编写了一个 Tampermonkey 用户脚本,目标是实现以下功能: |
| 56 | + |
| 57 | +### ✅ 实现功能(按重要性排序) |
| 58 | + |
| 59 | +* ✅ **预加载页面中所有音频**,进入页面即可播放,无需等待 |
| 60 | +* ✅ **支持快捷键控制播放**(上下句、重播当前) |
| 61 | +* ✅ **高亮当前播放的音频按钮**,方便跟踪 |
| 62 | +* ✅ 提供图形化设置面板,自定义快捷键 |
| 63 | +* ✅ 控制面板悬浮右上角,鼠标悬停展开,带动画过渡 |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +## 🔍 页面结构分析:Duome 音频标签结构 |
| 68 | + |
| 69 | +我们分析页面结构,找到关键播放按钮的 DOM 元素。 |
| 70 | + |
| 71 | +```html |
| 72 | +<div class="playback voice" data-src="https://.../audio.mp3"></div> |
| 73 | +``` |
| 74 | + |
| 75 | +观察发现: |
| 76 | + |
| 77 | +* 每个音频按钮是一个 `<div class="playback voice">` |
| 78 | +* 音频地址保存在 `data-src` 属性中 |
| 79 | +* 没有用 `<audio>` 标签(点击按钮后才触发下载) |
| 80 | + |
| 81 | +📌 所以我们可以: |
| 82 | + |
| 83 | +1. 用 `querySelectorAll('.playback.voice')` 抓取所有按钮 |
| 84 | +2. 读取 `data-src` 并手动创建 `new Audio()` 对象 |
| 85 | +3. 调用 `.load()` 立即预加载音频 |
| 86 | +4. 将音频挂载到元素上:`el._audio = audio` |
| 87 | + |
| 88 | +这样就能在页面加载阶段提前准备好所有音频。 |
| 89 | + |
| 90 | +--- |
| 91 | + |
| 92 | +## 🛠 开发过程与技术选择 |
| 93 | + |
| 94 | +### 🎧 音频预加载核心逻辑 |
| 95 | + |
| 96 | +```js |
| 97 | +function preloadAudios() { |
| 98 | + return Array.from(document.querySelectorAll('.playback.voice')).map(el => { |
| 99 | + const src = el.dataset.src; |
| 100 | + if (src) { |
| 101 | + const audio = new Audio(src); |
| 102 | + audio.load(); // 主动加载 |
| 103 | + el._audio = audio; // 挂载到元素 |
| 104 | + } |
| 105 | + return el; |
| 106 | + }); |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +📌 这段代码确保每个按钮都带有一份缓存的 `Audio` 对象,后续播放立即生效。 |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +### ⌨️ 快捷键录入方式的取舍 |
| 115 | + |
| 116 | +#### 🅰️ 方案一:使用 `<select>` 下拉框选择按键 |
| 117 | + |
| 118 | +* ✅ 简单易懂 |
| 119 | +* ❌ 只能选择固定键,无法支持 `Shift+N` 等组合键 |
| 120 | + |
| 121 | +#### 🅱️ 方案二:使用 `keydown` 捕捉用户输入 |
| 122 | + |
| 123 | +* ✅ 支持任意按键或组合键 |
| 124 | +* ✅ 操作更自然(直接按下就记录) |
| 125 | +* ❌ 实现略复杂,要处理 `Shift` / `Ctrl` 等修饰符 |
| 126 | + |
| 127 | +最终我选择了方式二,用如下方式处理键盘录入: |
| 128 | + |
| 129 | +```js |
| 130 | +input.addEventListener('focus', () => { |
| 131 | + input.value = '按下键...'; |
| 132 | + const capture = (e) => { |
| 133 | + e.preventDefault(); |
| 134 | + let key = e.key; |
| 135 | + if (e.shiftKey && key !== 'Shift') key = 'Shift+' + key; |
| 136 | + input.value = key; |
| 137 | + window.removeEventListener('keydown', capture); |
| 138 | + }; |
| 139 | + window.addEventListener('keydown', capture); |
| 140 | +}); |
| 141 | +``` |
| 142 | + |
| 143 | +--- |
| 144 | + |
| 145 | +### 💾 快捷键配置的存储方式比较 |
| 146 | + |
| 147 | +#### ✅ 方式一:`localStorage` |
| 148 | + |
| 149 | +```js |
| 150 | +localStorage.setItem('shortcuts', JSON.stringify(config)); |
| 151 | +``` |
| 152 | + |
| 153 | +* ✅ 浏览器原生支持 |
| 154 | +* ❌ 每个域名独立、数据不隔离,Tampermonkey 脚本难管理 |
| 155 | + |
| 156 | +#### ✅ 方式二:Tampermonkey 提供的 `GM_setValue` |
| 157 | + |
| 158 | +```js |
| 159 | +GM_setValue('shortcuts', config); |
| 160 | +``` |
| 161 | + |
| 162 | +* ✅ 配合油猴脚本完美使用,数据独立、跨域共享 |
| 163 | +* ✅ 可用于任何 Tampermonkey 页面脚本 |
| 164 | + |
| 165 | +📌 最终采用 `GM_setValue` / `GM_getValue`,文档参考:[Tampermonkey 文档](https://www.tampermonkey.net/documentation.php#GM_setValue) |
| 166 | + |
| 167 | +--- |
| 168 | + |
| 169 | +## 🎛 悬浮控制面板 + 动画展开 |
| 170 | + |
| 171 | +为了不打扰页面主体,我把控制面板缩成了右上角一个小齿轮图标。 |
| 172 | + |
| 173 | +* 鼠标悬停显示完整控制面板 |
| 174 | +* 鼠标移出自动收起 |
| 175 | +* 使用 `opacity` 和 `transform` 实现动画过渡 |
| 176 | + |
| 177 | +核心 CSS: |
| 178 | + |
| 179 | +```css |
| 180 | +#duome-panel { |
| 181 | + opacity: 0; |
| 182 | + transform: translateY(-10px); |
| 183 | + transition: all 0.3s ease; |
| 184 | +} |
| 185 | +``` |
| 186 | + |
| 187 | +--- |
| 188 | + |
| 189 | +## 🎯 最终效果 |
| 190 | + |
| 191 | +* ✅ 进入页面即自动加载所有音频 |
| 192 | +* ✅ 按 `Tab` 播放下一句,`Shift+Tab` 播放上一句 |
| 193 | +* ✅ 按 `r` 重播当前音频 |
| 194 | +* ✅ 每次播放时自动高亮当前按钮 |
| 195 | +* ✅ 用户可自定义快捷键 |
| 196 | +* ✅ 设置面板带动画、悬浮右上角不打扰阅读 |
| 197 | + |
| 198 | + |
| 199 | + |
| 200 | +--- |
| 201 | + |
| 202 | +## 🔧 后续优化计划 |
| 203 | + |
| 204 | +* 加入播放文字提示(显示当前句子) |
| 205 | +* 添加“记忆播放位置”功能 |
| 206 | +* 支持使用 `Esc` 暂停当前音频 |
| 207 | +* 打卡学习进度(记录每天播放量) |
| 208 | + |
| 209 | +--- |
| 210 | + |
| 211 | +## 🧩 结语 |
| 212 | + |
| 213 | +这个小脚本极大地提升了我在 Duome 学英语的体验,也让我进一步掌握了用户脚本的开发方式、页面结构解析能力,以及预加载和键盘交互等技巧。 |
| 214 | + |
| 215 | +花了小半天的时间写的,也希望能对其他人有作用吧。 |
| 216 | + |
0 commit comments