Skip to main content

从原生到 Electron 中的 JavaScript

· 7 min read

Electron 中用 C++ 或 Objective-C 编写的功能是如何转化为 JavaScript 供终端用户使用的?

🌐 How do Electron's features written in C++ or Objective-C get to JavaScript so they're available to an end-user?


背景

🌐 Background

Electron 是一个 JavaScript 平台,其主要目的是降低开发者构建强大桌面应用的门槛,而无需担心特定平台的实现细节。然而,从本质上讲,Electron 本身仍然需要使用某个系统语言来编写平台特定的功能。

实际上,Electron 会为你处理原生代码,以便你可以专注于单个 JavaScript API。

🌐 In reality, Electron handles the native code for you so that you can focus on a single JavaScript API.

那是如何工作的呢?Electron 中用 C++ 或 Objective-C 编写的功能是如何传递到 JavaScript 中,从而让终端用户可以使用的?

🌐 How does that work, though? How do Electron's features written in C++ or Objective-C get to JavaScript so they're available to an end-user?

要追踪这条路径,让我们从 app 模块 开始。

🌐 To trace this pathway, let's start with the app module.

通过打开我们 lib/ 目录中的 app.ts 文件,你会在顶部附近看到以下这行代码:

🌐 By opening the app.ts file inside our lib/ directory, you'll find the following line of code towards the top:

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

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

🌐 This line points directly to Electron's mechanism for binding its C++/Objective-C modules to JavaScript for use by developers. This function is created by the header and implementation file for the ElectronBindings class.

process.electronBinding

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

🌐 These files add the process.electronBinding function, which behaves like Node.js’ process.binding. process.binding is a lower-level implementation of Node.js' require() method, except it allows users to require native code instead of other code written in JS. This custom process.electronBinding function confers the ability to load native code from Electron.

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

🌐 When a top-level JavaScript module (like app) requires this native code, how is the state of that native code determined and set? Where are the methods exposed up to JavaScript? What about the properties?

native_mate

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

🌐 At present, answers to this question can be found in native_mate: a fork of Chromium's gin library that makes it easier to marshal types between C++ and JavaScript.

native_mate/native_mate 中有一个 object_template_builder 的头文件和实现文件。这使我们能够在本地代码中形成模块,其结构符合 JavaScript 开发者的预期。

🌐 Inside native_mate/native_mate there's a header and implementation file for object_template_builder. This is what allow us to form modules in native code whose shape conforms to what JavaScript developers would expect.

mate::ObjectTemplateBuilder

如果我们把每个 Electron 模块都看作一个 object,就更容易理解为什么我们希望使用 object_template_builder 来构建它们。这个类是基于 V8 提供的一个类构建的,V8 是谷歌的开源高性能 JavaScript 和 WebAssembly 引擎,用 C++ 编写。V8 实现了 JavaScript(ECMAScript)规范,因此它的原生功能实现可以直接与 JavaScript 中的实现对应。例如,v8::ObjectTemplate 给我们提供了没有专用构造函数和原型的 JavaScript 对象。它使用 Object[.prototype],在 JavaScript 中等同于 Object.create()

🌐 If we look at every Electron module as an object, it becomes easier to see why we would want to use object_template_builder to construct them. This class is built on top of a class exposed by V8, which is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++. V8 implements the JavaScript (ECMAScript) specification, so its native functionality implementations can be directly correlated to implementations in JavaScript. For example, v8::ObjectTemplate gives us JavaScript objects without a dedicated constructor function and prototype. It uses Object[.prototype], and in JavaScript would be equivalent to Object.create().

要查看其实际效果,请查看应用模块的实现文件 atom_api_app.cc。在底部有如下内容:

🌐 To see this in action, look to the implementation file for the app module, atom_api_app.cc. At the bottom is the following:

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

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

🌐 In the above line, .SetMethod is called on mate::ObjectTemplateBuilder. .SetMethod can be called on any instance of the ObjectTemplateBuilder class to set methods on the Object prototype in JavaScript, with the following syntax:

.SetMethod("method_name", &function_to_bind)

这是 JavaScript 等效的:

🌐 This is the JavaScript equivalent of:

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

此类还包含用于设置模块属性的函数:

🌐 This class also contains functions to set properties on a module:

.SetProperty("property_name", &getter_function_to_bind)

or

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

这些将反过来成为 Object.defineProperty 的 JavaScript 实现:

🌐 These would in turn be the JavaScript implementations of Object.defineProperty:

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

and

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

现在,我们可以按照开发者的期望,创建由原型和属性构成的 JavaScript 对象,并且更清晰地推断在这个较低的系统级别实现的函数和属性!

🌐 It’s possible to create JavaScript objects formed with prototypes and properties as developers expect them, and more clearly reason about functions and properties implemented at this lower system level!

关于在何处实现任何给定模块方法的决定本身就是一个复杂且常常不确定的问题,我们将在以后的文章中讨论。

🌐 The decision around where to implement any given module method is itself a complex and oft-nondeterministic one, which we'll cover in a future post.