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

2.5 公开API

在讲解Electron如何加载并执行用户的入口脚本文件前,我们必须要知道Electron是如何公开自己的API的,要不然都不知道Electron包内的app对象是怎么来的。

我们知道Electron是一个集成项目开发框架,开发者可以在这个框架内运行自己的Html页面、JavaScript代码还可以调用Node.js的API,这些能力都是Chromium和Node.js提供的,那么Electron自己的API(比如访问剪贴板、创建系统菜单、创建托盘图标等)是如何公开的呢?

在Electron的lib目录下存放了一系列的TypeScript文件,这些文件提供了开发者使用的所有Electron的JavaScript API,比如app、ipcMain、screen等,它们最终会被注入到Node.js的运行环境中,开发者的JavaScript代码可以像使用Node.js的API一样使用这些API,那么Electron是如何做到这一点的呢?

我们知道所有TypeScript文件只有被转义成JavaScript文件后才能被Node.js执行,那么接下去就以此为切入点分析这些TypeScript代码是如何起作用的。

Electron使用webpack对TypeScript文件进行转义的,转义TypeScript文件的工作被定义在Electron的编译脚本中(BUILD.gn),部分代码如下所示:

webpack_build("electron_browser_bundle") {
  deps = [ ":build_electron_definitions" ]
  inputs = auto_filenames.browser_bundle_deps // 这是ts文件路径数组,后面还会解释
  config_file = "// electron/build/webpack/webpack.config.browser.js"
  out_file = "$target_gen_dir/js2c/browser_init.js"
}
webpack_build("electron_renderer_bundle") {
  deps = [ ":build_electron_definitions" ]
  inputs = auto_filenames.renderer_bundle_deps
  config_file = "// electron/build/webpack/webpack.config.renderer.js"
  out_file = "$target_gen_dir/js2c/renderer_init.js"
}
// 省略了几个配置项

通过一系列这样的编译脚本,Electron把lib目录下的TypeScript代码编译成asar_bundle.js、browser_init.js、isolated_bundle.js、renderer_init.js、sandbox_bundle.js、worker_init.js六个JavaScript文件。

接着编译指令再通过一个名为js2c.py的Python的脚本处理这些JavaScript文件,代码如下所示:

action("electron_js2c") {
  deps = [
    ":electron_asar_bundle",
    ":electron_browser_bundle",
    ":electron_isolated_renderer_bundle",
    ":electron_renderer_bundle",
    ":electron_sandboxed_renderer_bundle",
    ":electron_worker_bundle",
  ]
  sources = [
    "$target_gen_dir/js2c/asar_bundle.js",
    "$target_gen_dir/js2c/browser_init.js",
    "$target_gen_dir/js2c/isolated_bundle.js",
    "$target_gen_dir/js2c/renderer_init.js",
    "$target_gen_dir/js2c/sandbox_bundle.js",
    "$target_gen_dir/js2c/worker_init.js",
  ]
  inputs = sources + [ "// third_party/electron_node/tools/js2c.py" ]
  outputs = [ "$root_gen_dir/electron_natives.cc" ]
  script = "build/js2c.py"
  args = [ rebase_path("// third_party/electron_node") ] +
         rebase_path(outputs, root_build_dir) +
         rebase_path(sources, root_build_dir)
}

这个Python脚本把上述几个JavaScript文件的内容转换成ASCII码的形式存放到一个C数组中,最终会生成一个名为electron_natives.cc的文件,当读者学习并实践完第10章后,可以在out\Testing\gen目录下看到这个临时源码文件,前面所述的browser_init.js等脚本文件将被存放在out\Testing\gen\electron\js2c目录下。

我们来看一下electron_natives.cc的关键代码:

namespace node {
namespace native_module {
static const uint8_t electron_js2c_asar_bundle_raw[] = {
47, 42, 42, 42, 42, 42, 42, 47, 32, 40, 102, 117, 110, 99.........// 此处省略了
                                                                  // 很多内容
}
void NativeModuleLoader::LoadEmbedderJavaScriptSource() {
  source_.emplace("electron/js2c/asar_bundle", UnionBytes{electron_js2c_asar_bundle_raw, 37556});
  source_.emplace("electron/js2c/browser_init", UnionBytes{electron_js2c_browser_init_raw, 258328});
  source_.emplace("electron/js2c/isolated_bundle", UnionBytes{electron_js2c_isolated_bundle_raw, 42942});
  source_.emplace("electron/js2c/renderer_init", UnionBytes{electron_js2c_renderer_init_raw, 117341});
  source_.emplace("electron/js2c/sandbox_bundle", UnionBytes{electron_js2c_sandbox_bundle_raw, 231375});
  source_.emplace("electron/js2c/worker_init", UnionBytes{electron_js2c_worker_init_raw, 41808});
}
}  // namespace native_module
}  // namespace node

上述代码中,LoadEmbedderJavaScriptSource方法非常重要,这个方法负责读取ASCII码数组的内容,并执行这些内容代表的JavaScript逻辑,只有当这个方法被执行后,那些TypeScript文件里书写的逻辑才会最终得到执行。那这个方法是在何时被执行的呢?

Electron的编译工具在编译Node.js的源码前,会以补丁的方式把electron_natives.cc注入到Node.js的源码中去,代码如下所示(patches\node\build_add_gn_build_files.patch):

+  sources = node_files.node_sources
+  sources += [
+    "$root_gen_dir/electron_natives.cc",
+    "$target_gen_dir/node_javascript.cc",
+    "src/node_code_cache_stub.cc",
+    "src/node_snapshot_stub.cc",
+  ]

注意每行代码前面都有加号,说明这些代码是要被添加到Node.js的代码中去的。electron_natives.cc的代码虽然被注入到Node.js的源码中去了,但它是如何生效的呢?

这就需要了解Electron为Node.js提供的另一个补丁文件:patches\node\build_modify_js2c_py_to_allow_injection_of_original-fs_and_custom_embedder_js.patch,其中包含这样一段代码:

NativeModuleLoader::NativeModuleLoader() : config_(GetConfig()) {
  LoadJavaScriptSource();
+ LoadEmbedderJavaScriptSource();
}

上述补丁文件为Node.js的NativeModuleLoader类型的构造函数增加了一个函数调用:LoadEmbedderJavaScriptSource()。也就是说当NativeModuleLoader类被实例化时,将执行前面说的TypeScript逻辑。

NativeModuleLoader类会在主进程初始化Node.js环境时被实例化,也就是说,我们上一小节讲的Node.js环境初始化成功后,这些TypeScript逻辑就被执行了。

我们以主进程的app对象为例,来看一下Electron是如何在TypeScript代码中公开这个对象的,关键代码如下所示:

const bindings = process._linkedBinding('electron_browser_app');
const { app } = bindings;
export default app;

这段代码通过process对象的_linkedBinding方法获取到了一个C++绑定到V8的对象(在Node.js原理小节已略有讲解,后面还会有更深入的讲解),然后通过export default的方式把这个对象内的app属性公开给开发者使用。

其他的Electron API,类似IpcMain、webContents等也都是通过类似的方式公开出来的。

这样Electron内置的Node.js就具备了Electron为其注入的API。

如果读者研究过Node.js的原理,一定会觉得这种实现方式非常熟悉,因为Node.js的内置模块也使用了类似的机制来公开API,实际上Electron团队就是参考了Node.js的实现方案。