系统底层支持
约 1998 字大约 7 分钟
2026-05-09
应用入口文件的加载
在上一节中提到,Electron 内部的 TypeScript 文件会在构建阶段被打包成 JavaScript bundle,并通过 js2c.py 转换进 electron_natives.cc,最终编译进 Electron 可执行文件。
其中,与主进程相关的初始化逻辑会被打包进 browser_init.js,源码入口为 lib/browser/init.ts,该入口的职责不是业务入口本身,而是负责找到开发者应用目录,并把真正的用户入口脚本交给 Node.js 加载。
真正的入口查找逻辑,是在主进程 Node.js 环境初始化完成后,Electron 加载并执行主进程初始化脚本时发生的。这个主进程初始化脚本会继续查找用户应用目录,并根据用户应用 package.json 中的 main 字段确定真正的业务入口文件。
整体流程可以简化为:
Electron 启动
↓
初始化主进程 Node.js 环境
↓
Node 识别 Electron 注入的内置 JS 源码
↓
加载并执行 browser_init.js
↓
执行 lib/browser/init.ts 中的初始化逻辑
↓
查找应用目录
↓
读取 package.json
↓
确定 main 入口脚本
↓
交给 Node.js 的 Module 系统加载并执行重要
这里需要区分两个入口:
browser_init.js / lib/browser/init.ts是 Electron 自己的主进程初始化脚本。package.json中的main字段指定的main.js(或者index.js)是开发者应用的主进程入口脚本。
查找应用目录
在 lib/browser/init.ts ,存在一个 searchPaths 数组,该数组定义了 Electron 查找入口文件的顺序,它的查找顺序为:
- Electron 首先会在
app子目录中查找入口文件 - 如果没有找到,继续查找
app.asar文件 - 如果还是没有找到,会查找
default_app.asar - 如果以上都没有找到,那么就会报异常并退出
读取 package.json
当 Electron 找到应用目录后,会继续读取该目录下的 package.json,其中最关键的是:
{
"main": "main.js"
}
main字段指定了主进程入口脚本的路径,Electron 会根据这个路径去加载并执行对应的脚本文件。如果main字段没有配置,那么默认会使用index.js作为入口脚本。
除了确定入口脚本之外,Electron 还会根据 package.json 中的信息完成一些应用级配置,例如:
设置应用名称
设置应用版本
设置默认 DesktopName
初始化应用路径相关信息Node.js 加载入口脚本
确定入口脚本后,Electron 并不会自己手动解析和执行这个 JS 文件,而是交给 Node.js 的模块系统处理。这里会用到 Node.js 内部的 Module 模块,核心能力包括:
_resolveFilename:解析入口文件的真实路径_load:加载并执行入口模块
JS API 到系统能力的调用链
前面介绍过,Electron 对外暴露的 API 很多来自 lib 目录下的一系列 TypeScript 文件。这些 TypeScript 文件负责提供 JavaScript 层的 API 形态,例如:
const { app } = require('electron')
app.addRecentDocument('/path/to/file')但是,仅靠 TypeScript / JavaScript 无法直接完成操作系统相关的能力。例如:创建原生窗口、访问系统剪贴、调用系统菜单等等
这些能力最终都需要进入 C++ 层,再由 C++ 调用 Chromium、Node、libuv 或操作系统提供的原生 API。也就是前面提到的 Native Binding
Native Binding
Native Binding 可以理解为 JavaScript 和 C++ 之间的桥。Electron 的很多 API 在 JS 层看起来只是普通方法调用:
app.addRecentDocument('/path/to/file')但这个方法真正访问的是操作系统能力,所以必须进入 C++ 层。Electron 会把一部分 C++ 模块注册到 Node / V8 环境中,然后 JS 层通过内部 binding 机制拿到这些模块。可以理解为:
C++ 模块
↓
注册为 Native Binding
↓
JS 层通过 process._linkedBinding(...) 获取
↓
JS 包装成对外 API例如:
const binding = process._linkedBinding('electron_browser_xxx')RegisterBuiltinModules
在 Electron 初始化 Node.js 环境期间,会执行 NodeBindings::Initialize。这个阶段会调用 RegisterBuiltinModules,把 Electron 自己提供的 C++ binding 模块注册到 Node 的 linked binding 系统中。
可以理解为:
NodeBindings::Initialize
↓
RegisterBuiltinModules
↓
ELECTRON_BUILTIN_MODULES
↓
注册 Electron 内置 C++ binding 模块
↓
JS 层可以通过 process._linkedBinding(...) 获取例如
// shell/common/node_bindings.cc
void NodeBindings::RegisterBuiltinModules() {
ELECTRON_BUILTIN_MODULES
}其中,ELECTRON_BUILTIN_MODULES 可以理解为 Electron 内部 C++ binding 模块的注册列表。这些模块会通过类似下面的宏注册:
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_xxx, Initialize)这里的含义是:把一个 Electron C++ 模块注册成 Node 可以识别的 linked binding。
重要
简单来说,Electron 先把 C++ 能力登记到 Node 内部(RegisterBuiltinModules),然后在 JS 层真正需要某个模块时,再按名称取出对应 binding(process._linkedBinding(...))
app 模块的初始化
以 app 模块为例,用户侧写的是:
const { app } = require('electron')但这个 app 并不是一个纯 JavaScript 对象。它的上层 API 由 Electron 的 JS / TS 代码负责组织和导出,底层能力则来自 Electron 的 C++ 模块。C++ 侧会通过类似下面的宏注册 app 对应的 binding:
// electron_browser_app 是 JS 层获取 binding 时使用的模块名称
// Initialize 是这个 C++ 模块真正初始化时执行的方法
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_app, Initialize)当 JavaScript 层需要使用 app 模块时,会通过内部 binding 机制获取它:
const binding = process._linkedBinding('electron_browser_app')这时 Node 会根据 electron_browser_app 这个名称,在前面注册过的 binding 表中找到对应的 C++ 模块,并调用它的 Initialize 方法。
// shell/browser/api/electron_api_app.cc
// app 模块的初始化方法
void Initialize(v8::Isolate* isolate, v8::Local<v8::Object> exports) {
// 创建 app 对象
v8::Local<v8::Object> app = CreateApp(isolate);
// 将 app 对象绑定到 exports
exports->Set(context, gin::StringToSymbol(isolate, "app"), app);
}平台适配
Electron 是跨平台框架,因此同一个 JS API 在不同操作系统下,底层实现可能完全不同。
这里以 app.addRecentDocument 为例。这个 API 的作用是把某个文件加入系统的 "最近打开的文档" 列表。用户侧调用:
const { app } = require('electron')
app.addRecentDocument('/path/to/file')它的整体调用链可以理解为:
app.addRecentDocument
↓
Electron JS API
↓
Native Binding
↓
Electron C++ Browser 实现
↓
不同操作系统的原生 API不同系统下,Electron 会使用不同文件实现这个能力:
| 操作系统 | 实现文件 | 说明 |
|---|---|---|
| Windows | browser_win.cc | 调用 Windows API 添加最近文档 |
| macOS | browser_mac.mm | 调用 macOS Cocoa API 添加最近文档 |
| Linux | browser_linux.cc | Linux 下没有统一标准,可能是空实现或不支持 |
Windows 实现
在 Windows 下,可以通过系统 API SHAddToRecentDocs 把文件添加到最近文档列表。
// shell/browser/browser_win.cc
void Browser::AddRecentDocument(const base::FilePath& path) {
SHAddToRecentDocs(SHARD_PATHW, path.value().c_str());
}macOS 实现
在 macOS 下,Electron 会调用 Cocoa 相关 API,把文件加入最近文档列表。
// shell/browser/browser_mac.mm
void Browser::AddRecentDocument(const base::FilePath& path) {
NSString* path_string = base::SysUTF8ToNSString(path.value());
NSURL* url = [NSURL fileURLWithPath:path_string];
[[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:url];
}这里的
.mm文件表示 Objective-C++ 文件,可以同时使用 C++ 和 Objective-C / Cocoa API。
Linux 实现
Linux 桌面环境没有一个完全统一的最近文档标准,因此 Electron 在 Linux 下可能没有对应实现,或者只是保留一个空方法。
// shell/browser/browser_linux.cc
void Browser::AddRecentDocument(const base::FilePath& path) {
// Linux 下不支持或无统一实现
}编译配置
前面可以看到,Electron 会把不同操作系统相关的实现拆分到不同文件中:
browser_win.cc
browser_mac.mm
browser_linux.cc这些文件并不是在运行时再动态选择,而是在编译阶段就会根据当前目标平台决定哪些源码参与编译。为了管理这些平台差异,Electron 会在 filenames.gni 中维护不同平台对应的源码列表。
filenames = {
windows_sources = [
"shell/browser/browser_win.cc",
]
mac_sources = [
"shell/browser/browser_mac.mm",
]
linux_sources = [
"shell/browser/browser_linux.cc",
]
common_sources = [
"shell/browser/browser.cc",
]
}前面三项定义了不同操作系统下执行编译时需要参与编译的文件;数组的最后一项表示的是三个系统都需要编译的文件
之后,BUILD.gn 会根据目标平台选择对应源码参与编译。
