深入浅出Electron:原理、工程与实践
上QQ阅读APP看书,第一时间看更新

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