Electron 鸿蒙快捷键全失灵,我排查了六个小时
先上代码。这是我们 App 里监听快捷键的一段,在 Windows 和 Mac 上跑了半年没出过事:
javascript
// main.js
const { app, globalShortcut, BrowserWindow } = require('electron');
app.whenReady().then(() => {
const win = new BrowserWindow({ width: 800, height: 600 });
win.loadFile('index.html');
globalShortcut.register('CmdOrCtrl+I', () => {
win.webContents.toggleDevTools();
console.log('[OK] DevTools toggled');
});
globalShortcut.register('CmdOrCtrl+N', () => {
console.log('[OK] New window shortcut triggered');
});
});
逻辑简单得不能再简单:Ctrl+I 开 DevTools,Ctrl+N 打日志。直到上周五下午我把这个应用扔到鸿蒙 PC 上跑。
按下 Ctrl+I------毫无反应。连个报错都没有,控制台干净得像刚重装完系统。我扭头看了看时间:14:27。
14:30 它明明注册成功了
第一时间自然是确认 register 返回值。globalShortcut.register() 返回布尔值,true 表示注册成功,false 表示失败(通常是快捷键被别的程序占用了)。
javascript
const registered = globalShortcut.register('CmdOrCtrl+I', handler);
console.log('GlobalShortcut registered:', registered); // → true
true。这事就诡异了------Electron 自己说注册成功,但按键下去事件没触发。难道是因为窗口没焦点?我猛点了十几下窗口标题栏,又试了一遍。还是没有。
等一下,在继续往下说之前我得交代一个重要前提。鸿蒙 PC 上的 Electron 不是官方原版 Electron,是华为团队基于 arkui-x 做的适配层。说白了就是 Electron 的 Chromium 内核跟鸿蒙的窗口系统之间多了一层 arkui-x 做翻译。这层翻译要是对键盘事件的处理有什么偏差,那出任何幺蛾子都不奇怪。
15:10 换一批快捷键试试
我第一反应是 Ctrl+I 可能被鸿蒙系统自己拦截了。毕竟很多 Linux 桌面环境里 Ctrl+I 是打开斜体格式的快捷键,鸿蒙底层是 Linux 内核,抢走也正常。
javascript
// 试一个冷门组合键,理论上不会跟系统冲突
globalShortcut.register('CmdOrCtrl+Shift+Alt+K', () => {
console.log('Cold key combo works!');
});
一个都没触发。不是 Ctrl+I 的问题,是所有快捷键都挂了。
我把能想到的组合全测了一遍:Ctrl+F、Alt+Q、Ctrl+Shift+T、Shift+F5。不是 Ctrl 的问题,不是 Alt 的问题,是 globalShortcut 整个模块在鸿蒙上完全不工作。
说实话这时候我心里已经在骂了------要是某个特定组合被占用,我能理解。整个模块都废了,这算什么?文档上一个字没提。
16:00 绕过 globalShortcut 试试
globalShortcut 不行,那 BrowserWindow 级的键盘事件呢?
javascript
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
win.webContents.on('before-input-event', (event, input) => {
console.log('Key event caught:', input.key, input.type, input.modifiers);
if (input.key === 'i' && (input.control || input.meta)) {
event.preventDefault();
win.webContents.toggleDevTools();
}
});
如果 before-input-event 能拿到键盘事件,至少说明问题在 accelerator 匹配那一步而非更底层。结果呢------这个事件根本没触发。键盘事件压根没传到 Electron 的 JS 层。
16:45 暂停,先确认是谁的锅
为了区分是 Electron 的问题还是鸿蒙的问题,我在同一台机器上快速做了三个对照实验:
- 跑一个纯 Chromium 浏览器 → 键盘事件正常
- 跑一个 arkui-x 原生 demo → 能收到键盘事件
- 跑 Electron 应用 → 键盘事件丢失
结论很明确了:arkui-x → Electron C++ 层 这一段把键盘事件弄丢了。
17:30 往 C++ 层埋日志
走到这步只剩一条路:拉源码、加日志、编译、看输出。这一步本身就极其痛苦------Electron 源码仓库大得离谱,git clone 走了快二十分钟。编译更不用提了,第一次完整编译我等了十五分钟,中间还因为缺少鸿蒙 SDK 的编译链断了一次。
编译通过后,我在 shell/browser/ui/views/electron_views_native_window.cc 里插了几行 LOG(INFO),专门打印收到的键盘事件:
cpp
// electron_views_native_window.cc(简化)
void ElectronViewsNativeWindow::OnKeyEvent(const KeyEvent& event) {
LOG(INFO) << "[electron:key_event] Received: keyCode=" << event.key_code
<< ", modifiers=" << event.modifiers;
if (ShouldHandleKeyboardShortcut(event)) {
LOG(INFO) << "[electron:key_event] Matched accelerator, dispatching...";
HandleKeyboardEvent(event);
} else {
LOG(INFO) << "[electron:key_event] Accelerator NOT matched for combo";
}
}
跑起来后看日志,问题浮出水面了:
[electron:key_event] Received: keyCode=73, modifiers=CTRL
[electron:key_event] Accelerator NOT matched for combo
键盘事件确实传到了 C++ 层,keyCode=73(就是字母 I),修饰键也正确识别为 CTRL。但 accelerator 匹配这一步失败了。
18:40 根因:修饰键标记位对不上
顺藤摸瓜找到匹配逻辑,在 ui/base/accelerators/accelerator_parsing.cc 里。具体来说,CmdOrCtrl 在鸿蒙上被解析为 Ctrl 没错,但 arkui-x 传过来的键盘修饰键位掩码跟 Chromium 内部定义的 EF_CONTROL_DOWN 常量对不上号。
鸿蒙底层虽然是 Linux 内核,但 arkui-x 的输入子系统重新定义了一套修饰键枚举值。Chromium 里的 EF_CONTROL_DOWN = 1 << 2,arkui-x 传过来的可能是 1 << 3 或者其他值,反正不相等。结果就是:Electron 拿着 Ctrl 键的掩码去比较,永远返回 false。
说白了就是一个常量定义不一致导致整个 accelerator 匹配系统全线崩溃。不是什么复杂的 bug,但非常隐蔽------如果不往 C++ 加日志,你永远只能看到"注册成功但没反应"这个表象。
19:30 绕路方案:放弃 globalShortcut
知道了根因,但我没时间等上游修。明天要给合作方做 demo,今晚必须把功能跑起来。
我想了三个方案:
方案 A:改 Chromium 的 accelerator_parsing.cc,把 arkui-x 的修饰键值映射到 Chromium 的标准值。可以,但需要重新编译 Electron 整个 C++ 层,编译一次二十分钟起,改动一处重编全量。pass。
方案 B :在前端用 keydown 事件模拟全局快捷键。问题是 Electron 的渲染进程 keydown 只在窗口获得焦点时触发,做不了真正的全局快捷键。
方案 C :直接用鸿蒙系统的 hidumper 命令从系统层抓键盘事件,绕开 Electron 的 accelerator 系统。
方案 C 最脏,但最快能跑。我选了 C。
javascript
const { spawn } = require('child_process');
class HarmonyShortcutManager {
constructor() {
this.handlers = new Map();
this.watcher = null;
}
register(combo, callback) {
const parts = combo.split('+');
const key = parts.pop().toUpperCase();
const modifiers = new Set(parts);
this.handlers.set(key, { modifiers, callback });
}
start() {
// hdc shell hidumper --keyevent 是鸿蒙的按键事件 dump 工具
this.watcher = spawn('hdc', ['shell', 'hidumper', '--keyevent'], {
stdio: ['ignore', 'pipe', 'pipe'],
});
this.watcher.stdout.on('data', (raw) => {
const event = raw.toString();
this.handlers.forEach((spec, targetKey) => {
if (event.includes(`KEY_${targetKey}`)) {
const hasCtrl = event.includes('CTRL');
const hasAlt = event.includes('ALT');
const hasShift = event.includes('SHIFT');
const matched = [...spec.modifiers].every(m => {
if (m === 'Ctrl') return hasCtrl;
if (m === 'Alt') return hasAlt;
if (m === 'Shift') return hasShift;
return true;
});
if (matched) {
spec.callback();
}
}
});
});
this.watcher.stderr.on('data', (err) => {
console.error('[Shortcut] hidumper error:', err.toString());
});
}
stop() {
if (this.watcher) {
this.watcher.kill();
this.watcher = null;
}
}
}
说真的,我自己都觉得这玩意丑。用 hidumper 监听系统输入然后字符串匹配来模拟快捷键------在正常环境里我绝对不会这么干。但在鸿蒙上,当所有正规途径都走不通的时候,能跑的方案就是好方案。
我把它挂到主进程里跑了一晚上,Ctrl+I 和 Ctrl+N 都能正常触发。延迟大概几十毫秒,可以接受。唯一需要注意的是 hidumper 的权限------它需要在调试模式下运行,生产环境得把权限提前配置好。
写到这儿
这件事给我最大的教训不是什么技术层面的。而是:在一个新平台上跑已有方案时,永远别假设基础 API 能用。我花了两个小时在 globalShortcut 的各种参数和组合键上折腾,如果一开始我用三分钟跑一个快捷键单元测试,当时就能发现问题,省下后面四个小时的排查。
我个人特别讨厌这种"文档里写了但实际不能用的"的情况------它不是一个 bug,它是一个信任问题。你让开发者怎么相信这个平台上的其他 API 是可靠的?
如果你要在鸿蒙上做 Electron 开发,快捷键这部分我的建议就两条:
- 别用
globalShortcut,至少等 arkui-x 下一个大版本看修没修 - 非要用的话,
hidumper绕路能用,但仅限调试/开发阶段
你遇到过类似的情况吗?有更好的方案欢迎留言。反正我是短期不会再碰 globalShortcut 了。
本文遵循 MIT 协议,转载请注明出处。