跳转到主内容

从原生应用到在Electron中使用JavaScript

· 阅读时间:约 4 分钟

C++或Objective-C写的Electron的功能如何被JavaScript访问,以便最终用户可以使用?


背景

Electron 是一个JavaScript 平台,其主要目的是降低门口,让开发人员能够构建强大的桌面应用,而不必担心平台的具体实现情况。 然而,在其核心上,Electron本身仍然需要特定平台的功能以特定的系统语言写入。

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

How does that work, though? C++或Objective-C写的Electron的功能如何被JavaScript访问,以便最终用户可以使用?

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

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')

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

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.

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

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.

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

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().

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)

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)

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)

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

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

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

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

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.