首页 / Electron 入门教程 / 主进程与 Node.js 集成

Electron 入门教程

主进程与 Node.js 集成

本教程共 45 篇 · 第 42 篇 · 更新于 2026-08-03

Electron主进程Node.jschild_processnative modulesnet

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 扩展,比如 sqlite3sharpbcrypt。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 模块发起请求

渲染进程里直接用 fetchXMLHttpRequest 访问远程接口时,常会遇到 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 安全模型的核心:能力集中在主进程,界面留在渲染进程,二者用受控桥接连接。下一章我们深入进程模型本身。