浏览器按键失灵踩坑记录
一、按键失灵
文章目录
- 浏览器按键失灵踩坑记录
- 一、按键失灵
- 1.1 按现象对原因
- 1.2 外部抢键
- 1.3 三步定位
- 1.4 处理
- 二、在被节流的页面里验证动画
- 三、其它
- 3.1 对象方法之间不能按名字互相调用
- 3.2 ES 模块缓存导致新旧代码混跑
- 3.3 `.gitignore` 否定规则
- 3.4 着色器通道与阈值方向
- 3.5 压缩按下游用途分层
- 3.6 交互参数先量化
- 四、验证信号
1.1 按现象对原因
| 现象 | 原因 | 确认方式 |
|---|---|---|
| 所有键都无效 | 焦点不在能接收键的元素上 | 看document.activeElement;鼠标点一下目标区域再试 |
| 某个字母键无效,同页别的字母键正常 | 外部程序抢键(扩展 / 宿主 / 输入法 / 系统工具) | 见 1.3 |
| 某个组合键无效 | 撞上浏览器或系统保留键 | 换组合(Ctrl+0/Ctrl+S会被浏览器拿走) |
| 等价按钮有效,只有键无效 | 与业务逻辑无关,问题在"键"这一层 | 这条本身就是分界线 |
| 开发者侧正常,用户侧无效 | 环境差异(扩展、宿主、输入法、键盘布局) | 让用户回传 1.4 的探针结果 |
| 只在某些时候无效 | 焦点被输入框/弹窗抢走;<dialog>打开时页面收不到键 | 是否刚点过输入框、是否有 modal 开着 |
e.key值异常 | 输入法合成中(isComposing/key: 'Process')、非 QWERTY 布局 | 打印e.key与e.isComposing |
1.2 外部抢键
同一份代码,有的键灵、有的键不灵,按钮好使。keydown 在页面之前就被扩展或宿主消费掉了,addEventListener('keydown')不触发,处理器里加日志也看不到。
| 来源 | 典型被占的键 | 特征 |
|---|---|---|
| 视频倍速类扩展(Video Speed Controller / Global Speed) | AD(减速/加速)、R(重置倍速),部分还有SZX | 按下后页面角落浮出当前倍率数字(如1.6),默认一档 0.1 |
| Vimium / 键盘导航类扩展 | 几乎全部字母 | 有提示条或光标跳转 |
| 翻译 / 截图 / 笔记 / 密码管理类扩展 | 常占TSECtrl+Shift+* | 有浮层或侧栏弹出 |
| IDE 内置浏览器、Electron / CEF 外壳 | 宿主自己绑定的全局键 | 禁用扩展仍复现 |
| 输入法 | 字母键报key: 'Process'或isComposing: true | 切英文输入法正常 |
| 截图 / 录屏 / 显卡驱动 / 游戏辅助 | 不固定 | 换浏览器、换机器仍复现 |
辨认"数字浮层":
- 细步进倍率(1.0→1.1→…→1.6)是视频倍速扩展的特征,浏览器缩放不会出现
1.6这种值。 - 该浮层不在页面 DOM 里(
querySelectorAll('body *')找不到)。扩展常用 closed shadow root,querySelector看不到属于正常现象。 - 确认途径:鼠标悬停看 tooltip,或「扩展管理 → 该扩展 → 快捷键」查是否绑了这个键。
本项目实例:用户报"按 R 复位无效",F 与空格正常,复位按钮有效。把同一功能改绑到C后有效,
说明代码路径无差异;r/R的绑定从首个提交起未变过。真因是用户装了视频倍速扩展,R是它的
“重置倍速”,A/D是"减速/加速"(用户按 A/D 时看到浮出1.6)。
定位过程中的无效改动:把复位语义从"开自动摆动"改成"停住"、加C别名、再换成D——都没触及
原因。期间用截图和像素差异做验证,得出过三次互相矛盾的结论(原因见第二节)。
1.3 三步定位
找等价按钮。按钮有效 → 业务逻辑没问题,问题在键这一层,直接排除"函数写错了"整类原因。
换一个键试。功能改绑另一个字母后有效 → 基本断定外部抢键(代码路径完全相同);仍无效 →
回到 1.1 的"所有键都无效",查焦点。上探针,看页面实际收到了什么。写到 DOM 而不是
console.log(用户不一定开控制台):el.addEventListener('keydown',(e)=>{el.dataset.lastkey=e.key;el.dataset.lastComposing=e.isComposing;// 只在"认得的键"上 preventDefault});控制台读
document.querySelector('...').dataset.lastkey:- 停在上一个成功的键 → 目标键没到页面,外部抢键
- 显示目标键 → 键到了页面,问题在后续逻辑
- 一直
undefined→ 键盘事件没进页面(焦点或宿主层面)
ev.defaultPrevented也可以作为"是否被接受"的信号,一个合成事件就能验证绑定,
不依赖页面可见性和渲染循环(见第四节)。
1.4 处理
- 治本:让用户在「扩展快捷键」里解绑冲突键,或本项目改用不冲突的键。页面侧无法绕过。
- 兜底:重要操作永远保留一条指针路径——按钮、双击、长按。键盘作为附加入口,不作为唯一入口。
- 不把
r/R/c/C/d/D全绑上。别名会掩盖真因,定位清楚后留一个正式键,冲突写进注释。
二、在被节流的页面里验证动画
自动化 / 无头 / 后台标签页里document.hidden会翻转,而渲染循环通常写成:
functionframe(now){if(!document.hidden&&visible)step(now);elselastTime=now;raf=requestAnimationFrame(frame);}step()被跳过时,两类实现的行为不同:
| 行为 | 实现方式 | 隐藏时 |
|---|---|---|
| 拖拽 / 直接赋值 | 直接写最终状态(obj.rotation = ...) | 照常生效 |
| 补间 / 复位 / 翻转 | 只改target,靠step()里的缓动收敛 | 完全无变化 |
结果是"隐藏时拖拽能动、复位不动"被读成"复位坏了"。用同一个方法测已知能用的翻转也测不出来,
说明量具本身失效,此时任何基于画面的结论都不可信。
对策:
- 验证前确认
document.hidden、visible、rAF 是否在跑。 - 改用不依赖渲染循环的信号(见第四节)。
- 动画的观感和手感只能真人验证;自动化只能验逻辑。
三、其它
3.1 对象方法之间不能按名字互相调用
constapi={flip(v){/* ... */},reset(){flip(false);},// ReferenceError: flip is not defined};flip是对象属性,不是作用域里的名字,方法内部裸名引用不到。node --check查不出来——
语法合法,失败发生在运行时名字解析。
抽成闭包函数,两个方法都调它:
functionsetFace(v){/* ... */}constapi={flip(v=!flipped){setFace(v);},reset(){setFace(false);}};"改完没语法错、一调用就炸"的先查名字解析:打错字、作用域外引用、被同名变量遮蔽。只有实跑能发现。
3.2 ES 模块缓存导致新旧代码混跑
index.html/main.js是新的,被import的子模块还是旧的。症状表现为"提示文案写着新键位,
按下去没反应"这类自相矛盾的现象。
- 构建号写进 DOM,比看 Network 面板快:
container.dataset.build="2026-09-30.7";// 控制台:document.querySelector('#app').dataset.build - 改了行为就递增号位,否则标记无意义。
Cache-Control: no-store不保证 ES 模块不缓存,需要强刷(Ctrl+Shift+R)。- 排错先问"这个页面是什么时候加载的"。
3.3.gitignore否定规则
- 最后匹配生效:
!important-file.png必须放在*.png之后,放前面等于没写。 - 父目录被整体排除时,目录内文件无法再否定(
!dir/file无效)。
否定规则统一放文件末尾并注释说明顺序不能动。用git check-ignore -q <path> && echo 仍被忽略
验证,不靠肉眼看。
3.4 着色器通道与阈值方向
线稿辉光一直不亮。着色器是
line = (1. - smoothstep(.06, .25, texture2D(tLine, uv).r))只读红色通道,且越暗越亮。生成脚本把阈值图写进 alpha、RGB 留全白,红通道恒为 1,辉光恒为 0。
- 先找采样语句,确认读哪个通道、阈值区间、方向(
smoothstep还是1 - smoothstep)。 - 需要过渡就把强度直接当遮罩(保留中灰过渡带)。
p > 阈值 ? 0 : 255硬切会扔掉阈值区间内的柔和段。 - 轮廓单独从 alpha 取:深色主体贴透明区时 RGB 落差太小,边缘检测抓不到。
3.5 压缩按下游用途分层
| 用途 | 选择 | 原因 |
|---|---|---|
| 平滑渐变 / 照片 | 有损(WebP q88~90) | 肉眼无差,体积降 5~20 倍 |
| 参与阈值判断的图(mask / 描边 / 深度) | 无损 | 有损会把数值糊过阈值区间产生斑块;分辨率可降到一半——按归一化 UV 采样,与分辨率无关 |
| 平色 + 柔和 alpha | 无损 | 有损在边缘产生色晕 |
3.6 交互参数先量化
“拖拽手感差、旋转不顺手”,凭感觉改容易越改越玄。先量出可指认的数字:
- 灵敏度
0.006 rad/px× 限位±0.65 rad→ 拖 108px 撞墙,之后是死区(实测拖 250px 只转 44°,
后半段效率降到 1/6)。 - 渲染按时间常数
8/s追目标 → 每帧落后0.03~0.07 rad。
据此改成软限位 + 直接写实际值,改完用同样两个数字验收。数值合理不等于体验好,最终仍需真人确认。
四、验证信号
按可信度与成本排序:
| 手段 | 用来验 | 说明 |
|---|---|---|
ev.defaultPrevented | 事件绑定是否生效 | 处理器只对接受的输入调preventDefault();合成事件即可判定,不需要页面可见、不需要渲染循环 |
| DOM 属性信号 | 状态变更 | aria-pressed/dataset.*同步可读,比截图像素可靠 |
| 两条路径对照 | 分层定位 | 按钮 vs 键盘、旧键 vs 新键,差异本身即证据 |
| 亮区重心 / 面积 | 姿态、正反面 | 对流光等高频噪声不敏感;整幅平均亮度通常只能区分明显不同的两态 |
| 跨调用分段读 | 被节流的动画 | 状态挂window,借两次执行之间的墙钟时间让节流的帧跑完 |
| 假外部服务 | 网络链路 | 假 SMTP / mock API 可反复跑,真实服务会限流 |
| 真人操作 | 手感 / 视觉 | 不可替代 |