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的实现方案。