跳转到主要内容

Electron 中从原生到 JavaScript

·阅读时长 4 分钟

Electron 中用 C++ 或 Objective-C 编写的功能是如何被暴露给 JavaScript,从而可供最终用户使用的?


背景

Electron 是一个 JavaScript 平台,其主要目的是降低开发者构建健壮桌面应用程序的门槛,而无需担心特定平台的实现。然而,在其核心,Electron 本身仍然需要用给定系统语言编写特定于平台的函数。

实际上,Electron 会为你处理原生代码,这样你就可以专注于单一的 JavaScript API。

但这又是如何工作的呢?Electron 中用 C++ 或 Objective-C 编写的功能是如何被暴露给 JavaScript,从而可供最终用户使用的?

为了追踪这个路径,让我们从 app 模块 开始。

通过打开我们的 lib/ 目录中的 app.ts 文件,你会发现代码顶部有以下行

const binding = process.electronBinding('app');

这一行直接指向 Electron 将其 C++/Objective-C 模块绑定到 JavaScript 以供开发者使用的机制。这个函数由 ElectronBindings 类的头文件和 实现文件 创建。

process.electronBinding

这些文件添加了 process.electronBinding 函数,其行为类似于 Node.js 的 process.bindingprocess.binding 是 Node.js 的 require() 方法的较低级别实现,但它允许用户 require 本机代码,而不是用 JS 编写的其他代码。这个自定义的 process.electronBinding 函数赋予了加载 Electron 本机代码的能力。

当一个顶层 JavaScript 模块(如 app)需要这个原生代码时,该原生代码的状态是如何确定的和设置的?方法是如何暴露给 JavaScript 的?属性呢?

native_mate

目前,关于这个问题的答案可以在 native_mate 中找到:它是 Chromium 的 gin 的一个分支,它使得在 C++ 和 JavaScript 之间传递类型更容易。

native_mate/native_mate 内部有一个 object_template_builder 的头文件和实现文件。这允许我们以原生代码的形式构建模块,其结构符合 JavaScript 开发者的预期。

mate::ObjectTemplateBuilder

如果我们把每个 Electron 模块看作一个 object,那么就更容易理解为什么我们要使用 object_template_builder 来构建它们。这个类是建立在 V8 暴露的一个类之上的,V8 是 Google 的开源高性能 JavaScript 和 WebAssembly 引擎,用 C++ 编写。V8 实现了 JavaScript (ECMAScript) 规范,因此其本机功能实现可以直接关联到 JavaScript 中的实现。例如,v8::ObjectTemplate 给了我们没有专用构造函数和原型的 JavaScript 对象。它使用 Object[.prototype],在 JavaScript 中相当于 Object.create()

要看到这一点,请查看 app 模块的实现文件,atom_api_app.cc。在底部是以下内容

mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
.SetMethod("getGPUInfo", &App::GetGPUInfo)

在上面的行中,.SetMethod 被调用在 mate::ObjectTemplateBuilder 上。.SetMethod 可以被调用在 ObjectTemplateBuilder 类的任何实例上,以设置 JavaScript 中 Object 原型 上的方法,语法如下

.SetMethod("method_name", &function_to_bind)

这是 JavaScript 中等价于

function App{}
App.prototype.getGPUInfo = function () {
// implementation here
}

这个类还包含用于在模块上设置属性的函数

.SetProperty("property_name", &getter_function_to_bind)

.SetProperty("property_name", &getter_function_to_bind, &setter_function_to_bind)

这些反过来将是 Object.defineProperty 的 JavaScript 实现

function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
})

function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
set(newPropertyValue) {
_myProperty = newPropertyValue
}
})

可以创建由原型和属性组成的 JavaScript 对象,就像开发者期望的那样,并且可以更清晰地推理出在这个较低系统级别实现的功能和属性!

关于在哪里实现任何给定模块方法的决定本身是一个复杂且常常是非确定性的问题,我们将在未来的帖子中介绍。