页面事件
约 1076 字大约 4 分钟
2026-03-10
常见的页面事件
Electron 中提供了一系列的页面事件:
| 事件名称 | 触发时机 | 说明 |
|---|---|---|
did-finish-load | 页面加载完成后 | 页面导航完成时触发 |
did-create-window | 通过 window.open 创建新窗口时 | 新窗口创建时触发 |
context-menu | 用户右键唤起右键菜单时 | 右键菜单事件 |
这里我们以 did-finish-load 事件为例,来看一下 Electron 的 webContents 是如何发射这些事件的
webContents 的继承关系
webContents 是一个使用 C++ 实现的类,对应的源码位置为 shell/browser/api/electron_api_web_contents.cc
webContents 这个类继承于 Chromium 中的 content::WebContentsObserver。这个类用于观察一个具体页面
当页面运行到特定阶段时(比如页面加载完成),就会执行观察者上的一个名为 DidFinishLoad 的虚方法
事件发射的完整流程
Electron 中重写了 WebContents 的这个方法,在重写逻辑中,会判断当前页面是否为 iframe 子页面,如果不是子页面,那么就会调用 Emit 方法
注意
这里的 Emit 方法也不是 Node.js 原生事件系统里的 emit,而是 Electron 自己封装的一个模板方法
- 调用 Emit 方法
Emit 内部又会调用 EmitWithEvent 的模板方法,这两个模板方法都是在为事件执行准备 JavaScript 运行环境
// shell/browser/api/electron_api_web_contents.cc
// 重写 DidFinishLoad 方法
void WebContents::DidFinishLoad(content::RenderFrameHost* render_frame_host) {
// 判断是否为子页面(iframe)
if (!render_frame_host->GetParent()) {
// 非子页面,触发 did-finish-load 事件
Emit("did-finish-load");
}
}调用 EmitEvent 模板方法
紧接着会调用
EmitEvent的模板方法,源码位置:shell/common/gin_helper/event_emitter_caller.h// shell/common/gin_helper/event_emitter_caller.h // EmitEvent 模板方法,准备 JS 执行环境并触发事件 template <typename... Args> v8::Local<v8::Value> EmitEvent(v8::Isolate* isolate, v8::Local<v8::Object> emitter, base::StringPiece name, Args&&... args) { v8::EscapableHandleScope scope(isolate); v8::Local<v8::Context> context = isolate->GetCurrentContext(); // 将事件名转换为 V8 字符串 v8::Local<v8::String> event_name = gin::StringToV8(isolate, name); // 将参数转换为 V8 值 std::array<v8::Local<v8::Value>, sizeof...(args)> argv = { gin::ConvertToV8(isolate, std::forward<Args>(args))...}; // 调用 CallMethodWithArgs 执行 emit return scope.Escape(CallMethodWithArgs( isolate, emitter, event_name, argv.data(), argv.size())); }存储事件信息
EmitEvent方法内部,将did-finish-load事件名和事件回调方法所需要的参数存储到了一个对象里面调用 CallMethodWithArgs
然后调用
CallMethodWithArgs方法(源码位置:shell/common/gin_helper/event_emitter_caller.cc)// shell/common/gin_helper/event_emitter_caller.cc // CallMethodWithArgs 方法,最终调用 Node.js 的 emit v8::Local<v8::Value> CallMethodWithArgs( v8::Isolate* isolate, v8::Local<v8::Object> emitter, v8::Local<v8::String> method, v8::Local<v8::Value>* argv, int argc) { // 调用 node::MakeCallback 执行 Node.js 的 emit 方法 return node::MakeCallback(isolate, emitter, method, argc, argv, {0, 0}).FromMaybe(v8::Local<v8::Value>()); }执行 Node.js 的 emit
CallMethodWithArgs方法内部,最终调用了 Node.js 内置函数node::MakeCallback方法,而这个内置函数的作用就是在webContents对象实例上面执行 Node.js 的emit方法
调用链总结
页面加载完成
↓
DidFinishLoad 虚方法
↓
判断是否为 iframe 子页面
↓
调用 Emit 方法(Electron 重写)
↓
调用 EmitWithEvent 模板方法
↓
调用 EmitEvent 模板方法
↓
存储事件名和参数
↓
调用 CallMethodWithArgs
↓
调用 node::MakeCallback
↓
执行 Node.js 的 emit 方法注
整个事件发射过程的核心是:Chromium 的页面生命周期事件,通过 Electron 重写的方法,最终转换为 Node.js 的事件系统,使得开发者可以使用熟悉的 on / once 来监听这些页面事件。
JavaScript 使用示例
了解了底层实现后,开发者在使用时只需要简单的 JavaScript 代码:
// renderer.js
// 监听页面加载完成事件
window.electronAPI.onDidFinishLoad(() => {
console.log('页面加载完成')
// 可以在这里执行页面加载完成后的逻辑
})
// preload.js
// 通过 contextBridge 暴露事件监听能力
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onDidFinishLoad: (callback) => {
ipcRenderer.on('did-finish-load', () => callback())
}
})
// main.js
// 主进程中也可以直接监听
const { BrowserWindow } = require('electron')
const win = new BrowserWindow()
win.webContents.on('did-finish-load', () => {
console.log('窗口页面加载完成')
})理解事件流
底层的 C++ 实现对开发者是透明的,开发者只需要关心 JavaScript 层面的事件监听。这就是 Electron 的设计哲学:底层复杂性由框架承担,上层提供简洁的 API
