2.13 页面事件
Electron提供了一系列的页面事件,比如我们常用的did-finish-load(页面加载完成后触发)、did-create-window(在页面中通过window.open创建一个新窗口成功后触发)和context-menu(用户在页面中右击唤起右键菜单时触发),本节就以did-finish-load事件为例讲解一下Electron的webContents对象是如何发射这些事件的。
webContents这个对象的C++实现位于类WebContents中(shell\browser\api\electron_api_web_contents.cc),这个类继承自Chromium的content::WebContentsObserver类型,这个类型就代表一个具体的页面,当某个页面的实例运行到一定的环节时,比如页面加载完成,就会执行这个实例的一个虚方法,也就是DidFinishLoad方法。
Electron的WebContents类重写了这个方法,所以在应用程序执行过程中,调用的将是Electron的实现逻辑(这是C++作为一个面向对象的编程语言具备的多态的能力),代码如下所示:
void WebContents::DidFinishLoad(content::RenderFrameHost* render_frame_host, const GURL& validated_url) { bool is_main_frame = !render_frame_host->GetParent(); int frame_process_id = render_frame_host->GetProcess()->GetID(); int frame_routing_id = render_frame_host->GetRoutingID(); auto weak_this = GetWeakPtr(); Emit("did-frame-finish-load", is_main_frame, frame_process_id, frame_routing_id); if (is_main_frame && weak_this && web_contents()) Emit("did-finish-load"); }
在这个方法中,Electron判断当前页面是否为子页面(iframe页面),如果不是,则调用了一个名为Emit的方法,注意这个方法并不是Node.js内置的发射事件的方法,而是Electron自己实现的一个模板方法,代码如下所示(shell\browser\event_emitter_mixin.h):
template <typename... Args> bool Emit(base::StringPiece name, Args&&... args) { v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate(); v8::Locker locker(isolate); v8::HandleScope handle_scope(isolate); v8::Local<v8::Object> wrapper; if (!static_cast<T*>(this)->GetWrapper(isolate).ToLocal(&wrapper)) return false; v8::Local<v8::Object> event = internal::CreateEvent(isolate, wrapper); return EmitWithEvent(isolate, wrapper, name, event, std::forward<Args>(args)...); } template <typename... Args> static bool EmitWithEvent(v8::Isolate* isolate, v8::Local<v8::Object> wrapper, base::StringPiece name, v8::Local<v8::Object> event, Args&&... args) { auto context = isolate->GetCurrentContext(); gin_helper::EmitEvent(isolate, wrapper, name, event, std::forward<Args>(args)...); v8::Local<v8::Value> defaultPrevented; if (event->Get(context, gin::StringToV8(isolate, "defaultPrevented")) .ToLocal(&defaultPrevented)) { return defaultPrevented->BooleanValue(isolate); } return false; }
通过上述代码,我们知道Emit的方法内部又执行了另外一个模板方法EmitWith-Event。在这两个模板方法中,Electron为事件执行准备了JavaScript的执行环境,接着调用了第三个模板方法EmitEvent,代码如下所示(shell\common\gin_helper\event_emitter_caller.h):
template <typename StringType, typename... Args> v8::Local<v8::Value> EmitEvent(v8::Isolate* isolate, v8::Local<v8::Object> obj, const StringType& name, Args&&... args) { internal::ValueVector converted_args = { gin::StringToV8(isolate, name), gin::ConvertToV8(isolate, std::forward<Args>(args))..., }; return internal::CallMethodWithArgs(isolate, obj, "emit", &converted_args); }
在这个方法中,Electron把事件名did-finish-load和事件回调方法需要的参数存储在一个对象中,并执行了一个工具方法CallMethodWithArgs(shell\common\gin_helper\event_emitter_caller.cc),代码如下所示,注意调用此工具方法时传入了一个字符串emit。
v8::Local<v8::Value> CallMethodWithArgs(v8::Isolate* isolate, v8::Local<v8::Object> obj, const char* method, ValueVector* args) { gin_helper::MicrotasksScope microtasks_scope(isolate, true); v8::MaybeLocal<v8::Value> ret = node::MakeCallback( isolate, obj, method, args->size(), args->data(), {0, 0}); v8::Local<v8::Value> localRet; if (ret.ToLocal(&localRet)) { return localRet; } return v8::Boolean::New(isolate, false); }
最终在这个方法中,调用了Node.js的内置函数node::MakeCallback,这个方法实际上就是在webContents对象的实例上执行了emit方法,也就相当于执行了如下一行JavaScript代码:
webContents.emit("did-finish-load",e);
我们知道Electron的webContents对象继承自Node.js的EventEmitter类型,假设用户在webContents对象上注册了did-finish-load事件,那么此时这个事件的回调函数将被执行。关于EventEmitter更多资料请参阅Node.js官方文档https://nodejs.org/dist/latest-v14.x/docs/api/events.html。