这不是一篇只讲概念的八股文,而是一篇能直接跑起来的实战小册 。
我们从一个极简的 Electron 应用------加法计算器(macOS)出发,一路串起:主进程、渲染进程、预加载脚本(preload)和 IPC(进程间通信)。
一、学习第一步,Electron 是什么?
一句话:用 Web 技术写桌面应用 。
一个最小可用的 Electron 应用,至少有这三类角色:
- 主进程(Main Process):Node.js 环境,负责创建窗口、系统 API、生命周期。
- 渲染进程(Renderer Process):一个或多个浏览器窗口,跑的是前端页面,默认不能直接访问 Node 和部分 Electron API。
- 预加载脚本(Preload) :在「主进程」和「渲染进程」之间的桥梁,在页面加载前执行,用来安全地把有限的 API 暴露给页面。
主进程和渲染进程互不越界,彼此也看不到对方的运行环境,所以需要 预加载 + IPC 来安全地打通两边。
二、项目结构与运行步骤
先弄清楚目录里都有什么,再谈实现。加法计算器项目结构如下:
perl
my-electron-app/
├── main.js # 主进程入口
├── preload.js # 预加载脚本(桥接主进程与渲染进程)
├── index.html # 计算器界面
├── renderer.js # 渲染进程逻辑(纯前端)
└── package.json
- 运行步骤 (仅两步):
- 在项目根目录执行:
npm install - 然后执行:
npm start(底层就是electron .)
- 在项目根目录执行:
几秒之后,你会看到一个原生窗口,里面就是我们的加法计算器页面。就是👇🏻这样:

三、让我们开始实战吧:
3.1 主进程 main.js
javascript
const { app, BrowserWindow, ipcMain } = require('electron'); // 引入 Electron 主进程 API
const path = require('path'); // 用于拼接预加载脚本路径
let mainWindow; // 保存窗口引用,方便后续操作
function createWindow() {
mainWindow = new BrowserWindow({
width: 600,
height: 300,
resizable: false,
webPreferences: {
nodeIntegration: false, // 关闭渲染进程里的 Node,提升安全性
contextIsolation: true, // 启用上下文隔离,配合 preload 使用
preload: path.join(__dirname, 'preload.js'), // 指定预加载脚本
},
});
mainWindow.loadFile('index.html'); // 加载本地页面
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(() => {
ipcMain.handle('ping', () => 'pong'); // 注册一个简单的 IPC 处理函数
createWindow(); // 应用就绪后创建主窗口
app.on('activate', () => {
// macOS 上点击 Dock 图标时,如果没有窗口就重新创建
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
// macOS 下通常会保持应用和菜单栏;其他平台则直接退出
if (process.platform !== 'darwin') {
app.quit();
}
});
快速理解:
BrowserWindow负责创建桌面窗口。webPreferences里:- 关闭
nodeIntegration:渲染进程看不到 Node,默认更安全。 - 开启
contextIsolation:把页面和 preload 严格隔离。 - 指定
preload:告诉 Electron 在加载页面前先跑preload.js。
- 关闭
ipcMain.handle('ping')提供了一个叫"ping"的 IPC 通道,供 preload 间接调用。
3.2 预加载脚本 preload.js
javascript
const { contextBridge, ipcRenderer } = require('electron'); // 只引入预加载需要的模块
// 通过 contextBridge 安全地向渲染进程暴露有限 API,而非直接暴露 ipcRenderer
contextBridge.exposeInMainWorld('electronAPI', {
versions: {
node: () => process.versions.node, // 读取当前 Node 版本
chrome: () => process.versions.chrome, // 读取当前 Chrome 版本
electron: () => process.versions.electron, // 读取当前 Electron 版本
},
ping: () => ipcRenderer.invoke('ping'), // 封装一次 IPC 调用,内部才使用 ipcRenderer
});
快速理解:
contextBridge.exposeInMainWorld会在页面里挂出window.electronAPI。- 页面只能看到我们决定暴露出来的几个方法:
versions.*():读取当前运行时的 Node / Chrome / Electron 版本。ping():实质是调用ipcRenderer.invoke('ping'),由主进程来返回'pong'。
3.3 渲染进程页面 index.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>加法计算器</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* {
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text',
system-ui, sans-serif;
}
body {
margin: 0;
padding: 16px;
background: #f5f5f7;
}
.container {
max-width: 460px;
margin: 0 auto;
background: #ffffff;
border-radius: 12px;
padding: 16px 20px 20px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
h1 {
font-size: 18px;
margin: 0 0 12px;
text-align: center;
}
.inputs {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
input[type='number'] {
flex: 1;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid #d0d3d8;
outline: none;
font-size: 14px;
}
.inputs span {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 2px;
font-size: 18px;
}
input[type='number']:focus {
border-color: #007aff;
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.15);
}
button {
width: 100%;
padding: 8px 10px;
border-radius: 8px;
border: none;
background: #007aff;
color: #ffffff;
font-size: 15px;
cursor: pointer;
}
button:hover {
background: #0060d1;
}
button:active {
background: #004ea8;
}
.result {
margin-top: 12px;
font-size: 15px;
text-align: center;
}
.result-value {
font-weight: 600;
color: #007aff;
}
.error {
margin-top: 8px;
font-size: 13px;
color: #d0021b;
text-align: center;
}
.env-info {
margin-top: 12px;
font-size: 11px;
color: #86868b;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1>简单加法计算器</h1> <!-- 应用标题 -->
<div class="inputs">
<input id="a" type="number" placeholder="第一个数" /> <!-- 第一个加数 -->
<span>+</span> <!-- 加号视觉元素 -->
<input id="b" type="number" placeholder="第二个数" /> <!-- 第二个加数 -->
</div>
<button id="calcBtn">计算</button> <!-- 触发计算按钮 -->
<div class="result">
结果:
<span class="result-value" id="result">--</span> <!-- 显示结果 -->
</div>
<div class="error" id="error"></div> <!-- 显示输入错误提示 -->
<p class="env-info" id="envInfo"></p> <!-- 显示运行环境版本信息 -->
</div>
<script src="renderer.js"></script>
</body>
</html>
3.4 渲染进程脚本 renderer.js
javascript
window.addEventListener('DOMContentLoaded', () => {
// 获取页面上的关键 DOM 元素
const aInput = document.getElementById('a');
const bInput = document.getElementById('b');
const calcBtn = document.getElementById('calcBtn');
const resultEl = document.getElementById('result');
const errorEl = document.getElementById('error');
// 加法计算核心逻辑
function calculate() {
errorEl.textContent = '';
const a = Number(aInput.value);
const b = Number(bInput.value);
if (aInput.value === '' || bInput.value === '') {
errorEl.textContent = '请输入两个数字。';
resultEl.textContent = '--';
return;
}
const sum = a + b; // 执行加法运算
resultEl.textContent = sum; // 更新结果显示
}
// 点击按钮触发计算
calcBtn.addEventListener('click', calculate);
// 在任一输入框按下 Enter 也触发计算
aInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
calculate();
}
});
bInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
calculate();
}
});
// 预加载脚本暴露的 API:显示运行环境版本(仅当 preload 成功暴露时)
const envEl = document.getElementById('envInfo');
if (typeof window.electronAPI !== 'undefined' && window.electronAPI.versions) {
envEl.textContent = `Chrome ${window.electronAPI.versions.chrome()} · Node ${window.electronAPI.versions.node()} · Electron ${window.electronAPI.versions.electron()}`;
}
});
到这里,你已经拥有一整套可以直接跑起来的 Electron 小应用:UI 在渲染进程,权限在主进程,桥梁在 preload。
四、进一步理解主进程:窗口与安全开关
回到 main.js,现在只看几个关键配置:
-
nodeIntegration: false渲染进程不能直接
require('fs')等 Node 模块,等于把「危险按钮」先关上。 -
contextIsolation: true页面和 preload 不在同一个 JS 环境里运行,恶意脚本更难污染我们在 preload 中暴露的 API。
-
preload: path.join(__dirname, 'preload.js')用
path.join + __dirname,保证不同系统下都能正确找到preload.js。 -
ipcMain.handle('ping', () => 'pong')相当于说:「任何走
'ping'通道来的调用,我都统一回'pong'」。这是我们对 IPC 模式的一次最小化演示。
五、渲染进程:界面与纯前端逻辑
index.html:计算器的 UIrenderer.js:只做前端逻辑:读输入、校验、做加法、更新 DOM;
若存在window.electronAPI.versions,则显示 Chrome/Node/Electron 版本。
渲染进程里不要 写 require('electron') 或 require('fs'),只做两件事:
- 使用常规的 Web API(DOM、fetch 等)搭界面、写交互。
- 通过 preload 暴露的
window.electronAPI向主进程借力。
六、预加载脚本:安全地暴露 API
预加载脚本在「窗口加载网页之前」执行,既能访问到部分 Node/Electron 能力,又与真正的页面隔离。
官方把它类比为 Chrome 扩展的 Content Script:
------提前潜伏进去,往页面塞几个「安全的入口」,而不是把钥匙全给它。
本项目中的 preload.js 使用 contextBridge.exposeInMainWorld,只暴露我们需要的接口,而不是把整个 ipcRenderer 或 Node 暴露出去:
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
versions: {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
},
ping: () => ipcRenderer.invoke('ping'),
});
versions:在 preload 里可以访问process.versions,通过三个函数暴露给页面,页面即可显示「Chrome / Node / Electron 版本」。ping:封装ipcRenderer.invoke('ping'),页面调用window.electronAPI.ping()会发 IPC 到主进程,主进程返回'pong'。
永远不要 把 ipcRenderer 整个对象暴露给渲染进程,否则页面可以随意向主进程乱发 IPC。
正确做法就是现在这样------一刀一刀地挑选出允许调用的函数,封装好再暴露出去。
七、进程间通信(IPC)简述
主进程和渲染进程不能直接访问对方内存或 API,只能通过 IPC 来「递纸条」。在本项目里,纸条的流向是这样的:
- 主进程 :
ipcMain.handle('ping', () => 'pong')------ 负责收纸条并回信。 - 预加载 :
ping: () => ipcRenderer.invoke('ping')------ 负责帮页面把纸条塞进"ping"这个通道。 - 渲染进程 :执行
const pong = await window.electronAPI.ping();,在控制台会看到'pong'。
以后只要你想在渲染进程里做「需要系统权限的事」(读文件、打开系统对话框等),基本都是这个套路的放大版:
主进程用 ipcMain.handle 注册处理函数,preload 用 ipcRenderer.invoke 封装方法,再用 contextBridge 暴露给页面。详见 Electron 进程间通信。
八、小结
| 角色 | 文件/位置 | 作用 |
|---|---|---|
| 主进程 | main.js |
创建窗口、配置 webPreferences、注册 IPC 处理(如 ping) |
| 预加载脚本 | preload.js |
在页面加载前执行,用 contextBridge 安全暴露 versions、ping 等 |
| 渲染进程 | index.html + renderer.js |
纯前端 UI 与逻辑,仅通过 window.electronAPI 使用预加载暴露的 API |