Electron 主进程、渲染进程与 preload

理解主进程、渲染进程和预加载脚本的职责,为接入 Vue 和安全 IPC 建立基础。

🎯 引言

Electron 应用不是把所有代码都放在同一个环境中运行。管理窗口和调用系统能力的代码运行在主进程,页面代码运行在渲染进程,两者之间还可以通过预加载脚本建立安全的通信入口。

本篇只解决一个问题:不同代码应该运行在哪个进程。窗口的详细生命周期会在下一篇之后单独讲解。

学完本篇,你将能够:

  • 区分主进程、渲染进程和预加载脚本的职责。
  • 理解一个主进程如何管理多个窗口。
  • 观察不同窗口拥有独立的页面状态。
  • 为下一篇接入 Vue 和后续 IPC 学习建立清晰的进程边界。

🧱 Electron 的进程模型

Electron 应用启动时会先运行 package.jsonmain 指向的文件,这段代码所在的环境就是主进程。一个应用只有一个主进程,它负责管理整个应用。

主进程使用 BrowserWindow 创建窗口。窗口加载 HTML 后,页面代码会在渲染进程中运行。开发时可以把每个窗口看成一个独立的浏览器页面:它们拥有自己的 DOM 和 JavaScript 变量,不会自动共享页面状态。

预加载脚本会在页面脚本之前运行。它负责向渲染进程提供少量、明确的桌面能力,本篇先认识它的位置,具体用法会在 IPC 章节讲解。

运行环境主要职责常见内容
主进程管理应用、窗口和系统能力appBrowserWindow
渲染进程显示页面并处理界面交互HTML、CSS、DOM、页面脚本
预加载脚本安全地向页面暴露有限的桌面能力contextBridge、受限 IPC API
不要为了让页面直接使用 Node.js API 而开启 nodeIntegration。保留 Electron 的默认隔离设置,再通过预加载脚本暴露必要能力。

🛠 创建两个独立的页面环境

下面由一个主进程创建两个窗口。两个窗口加载相同的 HTML 和 JavaScript,但各自保存一份独立的计数器状态。

先创建主进程入口:

main.js
const { app, BrowserWindow } = require('electron');
const path = require('node:path');

const windows = new Set();

function createWindow(name) {
    const win = new BrowserWindow({
        width: 460,
        height: 360,
        title: name,
    });

    windows.add(win);
    win.loadFile(path.join(__dirname, 'index.html'), {
        query: { name },
    });

    win.on('closed', () => {
        windows.delete(win);
    });

    return win;
}

function createApplicationWindows() {
    const firstWindow = createWindow('窗口 A');
    const secondWindow = createWindow('窗口 B');
    const [x, y] = firstWindow.getPosition();

    secondWindow.setPosition(x + 60, y + 60);
}

app.whenReady().then(() => {
    createApplicationWindows();

    app.on('activate', () => {
        if (BrowserWindow.getAllWindows().length === 0) {
            createApplicationWindows();
        }
    });
});

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

windows 保存仍然存在的窗口对象。窗口关闭后将它从集合中移除,避免继续保留已经失效的引用。

两个窗口加载同一个页面,通过查询参数取得自己的名称:

index.html
<!doctype html>
<html lang="zh-CN">
    <head>
        <meta charset="UTF-8" />
        <meta
            http-equiv="Content-Security-Policy"
            content="default-src 'self'; script-src 'self'"
        />
        <title>进程模型示例</title>
    </head>
    <body>
        <h1 id="window-name"></h1>
        <p>当前计数:<span id="count">0</span></p>
        <button id="add">加一</button>

        <script src="./renderer.js"></script>
    </body>
</html>

页面交互写在渲染进程中:

renderer.js
const params = new URLSearchParams(window.location.search);
const windowName = params.get('name');
const nameElement = document.querySelector('#window-name');
const countElement = document.querySelector('#count');
const addButton = document.querySelector('#add');

let count = 0;

nameElement.textContent = windowName;
document.title = windowName;

addButton.addEventListener('click', () => {
    count += 1;
    countElement.textContent = count;
});

运行项目:

npm start

页面会打开“窗口 A”和“窗口 B”。在窗口 A 中连续点击“加一”,窗口 B 的数字不会变化,因为两个窗口拥有独立的 DOM 和 count 变量。

这里不需要 IPC:两个窗口只修改自己的页面状态,没有交换数据。需要在窗口之间共享信息时,再由主进程和 IPC 负责传递。


窗口创建、重新激活和退出属于应用生命周期,下一篇会集中讲解。本篇只保留一个重要前提:BrowserWindow 必须在 app.whenReady() 完成后创建。


💡 代码应该放在哪里

判断代码位置时,先看它操作的对象:

  • 创建窗口、退出应用、打开系统对话框,放在主进程
  • 修改文字、监听按钮、更新页面样式,放在渲染进程
  • 页面需要使用桌面能力时,由预加载脚本暴露一个用途明确的接口。

例如,下面这段代码只能放在主进程,因为 BrowserWindow 负责创建桌面窗口:

const win = new BrowserWindow({
    width: 600,
    height: 400,
});

下面这段代码应该放在渲染进程,因为它操作页面 DOM:

document.querySelector('#title').textContent = '你好,Electron';

渲染进程不能直接调用主进程中的函数。需要跨进程协作时,要通过预加载脚本和 IPC 建立明确的通信通道。


🧾 小节总结

  • Electron 应用只有一个主进程,负责管理应用、窗口和系统能力。
  • BrowserWindow 加载的页面运行在独立的页面环境中。
  • 不同窗口不会自动共享 DOM 和 JavaScript 变量。
  • 窗口创建需要等待 app.whenReady(),详细生命周期会在下一篇讲解。
  • 预加载脚本负责连接渲染进程与桌面能力,具体通信方式将在下一篇讲解。

❓ 知识问答

Q1:一个 Electron 应用可以创建多个窗口吗?

A:可以。主进程可以创建多个 BrowserWindow,并分别加载不同页面或同一个页面。

Q2:两个窗口中的 JavaScript 变量会自动共享吗?

A:不会。每个窗口都有独立的页面环境,需要共享数据时应通过主进程和 IPC 传递。

Q3:每个窗口一定对应一个独立的操作系统进程吗?

A:不一定。Chromium 会负责底层进程安排。开发时应该依赖窗口之间的页面隔离,不要依赖具体的操作系统进程数量。

Q4:为什么关闭一个窗口后应用没有退出?

A:因为还有其他窗口存在。window-all-closed 只有在所有窗口都关闭后才会触发。

Q5:预加载脚本属于渲染进程吗?

A:它与指定窗口一起运行,但处在和页面隔离的上下文中,拥有比普通页面更高的权限。它应该只暴露页面确实需要的能力。


🧪 小练习

练习一:补全代码,再创建一个名为“窗口 C”的窗口:

function createApplicationWindows() {
    createWindow('窗口 A');
    createWindow('窗口 B');

    // 请在这里编写代码
}

练习二:补全激活事件,在没有窗口时重新创建窗口:

app.on('activate', () => {
    // 请在这里编写代码
});

完成练习后,分别修改三个窗口中的计数,观察它们的数字是否会互相影响。


🎉 恭喜你已经理解 Electron 的进程分工啦!下一篇我们会把 Vue 页面接入 Electron,之后再使用 preload 和 IPC 请求主进程能力。