主进程与 Node.js 集成
本教程共 45 篇 · 第 42 篇 · 更新于 2026-08-03
42. 主进程与 Node.js 集成
本节目标
- 理解主进程运行在完整的 Node.js 环境里。
- 会用
child_process调用外部程序。 - 了解原生模块为何需要为 Electron 重新编译。
- 掌握用
net模块发起请求、避开 CORS。 - 清楚主进程与渲染进程之间的隔离边界。
1-1 主进程就是 Node.js
每个 Electron 应用都有唯一的主进程,它是应用的入口,运行在 Node.js 环境里。这意味着主进程可以自由 require 任何 Node 模块,使用全部 Node API:fs 读写文件、path 处理路径、os 读取系统信息、crypto 做加密、http/https 发请求,都不在话下。
主进程的两大职责是:用 BrowserWindow 创建和管理窗口,用 app 模块管理应用生命周期。除此之外,它还是调用操作系统原生能力的唯一正道——菜单、对话框、托盘、通知等模块都只能在主进程使用。
// 主进程 main.js:直接使用 Node.js 的 fs 与 path
const { app, BrowserWindow } = require('electron')
const fs = require('node:fs')
const path = require('node:path')
function readNotes () {
const file = path.join(app.getPath('userData'), 'notes.json')
// 主进程直接读写本地文件,渲染进程则不行
return fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '[]'
}
1-2 用 child_process 调用外部程序
有时你需要启动一个外部命令行程序,比如调用系统工具、运行 Python 脚本、拉起一个本地服务。Node 的 child_process 模块在主进程里完全可用。提醒一句:这和上一章讲的命令注入是同一片雷区——务必用 execFile/spawn 传参数组,不要拼 shell 字符串。
// 主进程:用 spawn 调用外部程序,参数以数组传入
const { spawn } = require('node:child_process')
function runConverter (inputPath, outputPath) {
const child = spawn('ffmpeg', ['-i', inputPath, outputPath])
child.stdout.on('data', (d) => console.log(`输出:${d}`))
child.stderr.on('data', (d) => console.error(`错误:${d}`))
child.on('exit', (code) => console.log(`退出码:${code}`))
}
如果外部程序需要长时间运行、且可能崩溃或占用大量 CPU,更稳妥的做法是把它放进后面章节会讲的 utility process(工具进程),避免拖垮主进程。
调用外部程序时还要做好错误处理与超时控制:spawn 返回的子类进程可以监听 error 事件(例如命令不存在时触发),以及 exit 事件拿到退出码;对于可能卡死的命令,建议配合 setTimeout 在超时后调用 child.kill() 主动终止,防止主进程被一个不响应的外部程序拖死。另外,通过 stdio 选项可以控制子进程的标准输入、输出、错误如何与主进程对接——默认它们会继承主进程的终端,若你只想在代码里捕获输出,可把 stdio 设为 ['ignore','pipe','pipe'],再用 child.stdout/child.stderr 流式读取。
1-3 原生模块(Native Modules)
原生模块是用 C/C++ 编写、编译成 .node 文件的 Node 扩展,比如 sqlite3、sharp、bcrypt。Electron 支持它们,但因为 Electron 使用了和官方 Node.js 不同的应用二进制接口(ABI,例如用 BoringSSL 而非 OpenSSL),原生模块必须针对 Electron 重新编译,否则运行时会报 NODE_MODULE_VERSION 不匹配的错误。
借助 @electron/rebuild 可以自动完成重新编译,它会识别当前 Electron 版本、下载头文件并构建。如果你用 Electron Forge,开发态和打包时它都会自动调用这个工具。
# 安装后,每次 npm install 都重新编译原生模块
npm install --save-dev @electron/rebuild
./node_modules/.bin/electron-rebuild
手动指定版本时则这样编译:
# 针对特定 Electron 版本用 node-gyp 重新编译
HOME=~/.electron-gyp node-gyp rebuild --target=43.2.0 --arch=x64 \
--dist-url=https://electronjs.org/headers
提示:升级 Electron 大版本后,通常需要重新编译所有原生模块。在 Windows 上还需注意
win_delay_load_hook要设为true,否则会报「模块未自注册」之类错误。优先选用预编译了 Electron 二进制的包,能省去从源码构建的麻烦。
1-4 用 net 模块发起请求
渲染进程里直接用 fetch 或 XMLHttpRequest 访问远程接口时,常会遇到 CORS 限制。Electron 主进程提供了 net 模块,它基于 Chromium 的网络栈,却没有浏览器的同源策略束缚,适合在主进程侧做 HTTP/HTTPS 请求,再把结果通过 IPC 交给渲染进程。
// 主进程:用 net 模块请求接口,规避 CORS
const { net } = require('electron')
function fetchRemote (url) {
return new Promise((resolve, reject) => {
const request = net.request(url)
request.on('response', (response) => {
let data = ''
response.on('data', (chunk) => { data += chunk })
response.on('end', () => resolve(data))
})
request.on('error', (err) => reject(err))
request.end()
})
}
注意 net 不是浏览器 fetch 的完全替代,它的 API 更接近底层流式请求。对于简单的同源或带代理的场景,net 能很好地配合 session 管理 Cookie 与缓存。
1-5 主进程与渲染进程的隔离边界
主进程拥有完整 Node 能力,渲染进程的界面代码却没有——这不是缺陷,而是安全设计。二者的分界线就是上下文隔离与 IPC 桥接。渲染进程想用任何 Node 能力,都要通过 preload 暴露的受控 API,再由主进程代为执行。
可以用下面这张对照表快速记住边界:
| 能力 | 主进程 | 渲染进程(默认) | 预加载脚本 |
|---|---|---|---|
require Node 模块 | ✅ | ❌ | ✅(沙箱内受限) |
文件读写 fs | ✅ | ❌ | 受沙箱限制 |
BrowserWindow / app | ✅ | ❌ | ❌ |
ipcRenderer | ❌ | ❌(仅经 preload) | ✅ |
| DOM / 前端框架 | ❌ | ✅ | ✅(共享 window) |
这套边界的好处是:即便渲染进程被注入了恶意脚本,它也碰不到主进程的 Node 能力,破坏被限制在页面内。代价是你需要多写一层 IPC,但相比安全性,这点代价值得。
1-6 用流处理大文件与传递大对象
当主进程需要读写大文件时,不要一次性 readFileSync 把内容塞进内存,而应使用 fs.createReadStream / createWriteStream 以流的方式分块处理,这样既能控制内存占用,也能边读边通过 IPC 把进度回报给渲染进程。
// 主进程:以流的方式读取大文件并回报进度
const { ipcMain } = require('electron')
const fs = require('node:fs')
ipcMain.handle('read-big-file', (event, filePath) => {
return new Promise((resolve, reject) => {
const chunks = []
const stream = fs.createReadStream(filePath)
stream.on('data', (c) => chunks.push(c))
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
stream.on('error', reject)
})
})
如果要在主进程和渲染进程之间传递较大的结构化数据,优先用 MessageChannelMain 配合 postMessage 转移 MessagePort,而不是把整个对象反复序列化进 IPC 参数。转移端口后,两端可以像 WebSocket 那样持续收发消息,避免一次性大负载带来的卡顿。记住一条原则:主进程是能力的归宿,但也是单点;任何可能耗时或崩溃的逻辑,都应当考虑下沉到工具进程(下一章会展开)。
小结
主进程是 Electron 里「离 Node.js 最近」的地方,能用全部 Node API、调用外部程序、加载原生模块、用 net 发请求。渲染进程则被刻意隔离,只能通过 preload + IPC 间接使用这些能力。理解了这条边界,你就掌握了 Electron 安全模型的核心:能力集中在主进程,界面留在渲染进程,二者用受控桥接连接。下一章我们深入进程模型本身。