Electron学习
一. Electron安装
自己从零开始,方便理解Electron的内部机制
js
// 项目初始化
npm init -y
// 下载Electron 不用 Electron Forge
npm install --save-dev electron@latest
二. 创建一个窗口
添加 index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>hello world</h1>
</body>
</html>
添加 main.js
js
// 导入 Electron 的核心模块
// app: 控制应用程序的生命周期事件
// BrowserWindow: 创建和控制浏览器窗口
const { app, BrowserWindow } = require('electron')
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
// 创建一个新的浏览器窗口实例
const mainWindow = new BrowserWindow({
width: 800, // 设置窗口宽度为 800 像素
height: 600, // 设置窗口高度为 600 像素
// 配置网页功能选项
webPreferences: {
nodeIntegration: true // 允许在渲染进程中使用 Node.js 功能
}
})
// 加载应用程序的主页面
// 这里加载当前目录下的 index.html 文件
mainWindow.loadFile('index.html')
})
在package.json文件下scripts 添加运行命令
json
{
"name": "lectron",
"version": "1.0.0",
"description": "",
"main": "main.js",
"dependencies": {
"electron": "^39.2.6"
},
"devDependencies": {},
"scripts": {
"start": "electron ."
},
"keywords": [],
"author": "",
"license": "ISC"
}
注意: package.json文件下 需要修改为 "main": "main.js", 要不然会报错。

运行命令
bash
npm run start

三. 打开调试者工具
mainWindow.webContents.openDevTools() // 打开开发者工具
js
const { app, BrowserWindow } = require('electron')
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
const mainWindow = new BrowserWindow({
width: 800, // 设置窗口宽度为 800 像素
height: 600, // 设置窗口高度为 600 像素
// 配置网页功能选项
webPreferences: {
nodeIntegration: true // 允许在渲染进程中使用 Node.js 功能
}
})
mainWindow.webContents.openDevTools() // 打开开发者工具
// 这里加载当前目录下的 index.html 文件
mainWindow.loadFile('index.html')
})

四.热更新mian.js文件
因为每次修改mian.js文件 。需要关闭程序,重新运行命令。mian修改过的内容才生效。所以我们需要热更。不需要关闭程序,重新运行命令
nodemon文件监视工具,检测文件变化并自动重启进程
安装 nodemon
bash
npm install nodemon --save-dev
json
// --watch main.js
监视 main.js文件的变化(Electron 主进程入口文件)
// --exec "electron ."
检测到变化时执行的命令: electron .启动当前目录的 Electron 应用
"scripts": {
"start": "electron .",
"start:wacth": "nodemon --watch main.js --exec \"electron .\""
},
五. Electron进程和线程
主进程Main Process
在 Electron应用都有一个单一的主进程,作为应用程序入口点。主进程在Node.js 运行。它具备所以Node.jsAPI的能力。
- 窗口管理
- 应用程序生命周期
- 原生API
渲染进程Renderer Process
每个Electron应用会为每个打开的 BrowserWindow 生成一个单独的渲染进程。
渲染器无权直接访问require或其他Node.js API
进程间通信(IPC)
IPC通道名称
- ipcMain
- ipcRenderer
预加载脚本
为了将electron的不同类型的进程接在一起,我们需要使用被称为预加载preload的特殊脚本。
1.单向通信 - 从渲染器进程到主进程
使用ipcRenderer.send API发送消息,然后使用ipcMain.onAPI接收。
实现通过输入框修改应用标题

一. 添加输入框
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>hello world</h1>
Title: <input id="title" />
<button id="btn" type="button">Set</button>
<h1 id="info"></h1>
</body>
<script src="./renderer.js"></script>
</html>
二. 添加renderer.js 编写 点击按钮事件和渲染Node版本号逻辑
js
// 获取页面上 ID 为 'info' 的元素
const info = document.getElementById('info')
console.log(window.versions,'window.versions')
// 设置该元素的内容为版本信息
info.innerHTML = `Chrome (v${window.versions.chrome}), Node.js (v${window.versions.node})`
// 获取页面上 ID 为 'btn' 的按钮元素
const btn = document.getElementById('btn')
// 获取页面上 ID 为 'title' 的输入框元素
const titleInput = document.getElementById('title')
// 给按钮添加点击事件监听器
btn.addEventListener('click', () => {
// 从输入框中获取用户输入的值
const title = titleInput.value
// 调用预加载脚本暴露的 window.electron.setTitle 方法
// 该方法会通过 IPC 通知主进程设置窗口标题
window.electron.setTitle(title)
})
三. 预加载脚本(preload.js)
它使用contextBridge来安全地暴露一些Node.js和Electron的功能给渲染进程(即网页)。
代码中使用了三个exposeInMainWorld调用,分别暴露了'versions'、'electron'和'require'三个全局变量。
js
// contextBridge: 安全地向渲染进程暴露 API 的桥梁
// ipcRenderer: 渲染进程与主进程通信的模块
const { contextBridge, ipcRenderer } = require("electron");
/**
* 通过 contextBridge 向渲染进程暴露 Node.js 和 Chromium 版本信息
* 安全地将只读数据暴露给 window.versions 对象
*/
contextBridge.exposeInMainWorld('versions', {
node: process.versions.node, // Node.js 运行时版本号
chrome: process.versions.chrome // Chromium 引擎版本号
})
/**
* 向渲染进程暴露自定义的 electron API
* 创建一个安全的 window.electron 对象,包含 setTitle 方法
* @param {string} title - 要设置的窗口标题
*/
contextBridge.exposeInMainWorld("electron", {
setTitle: (title) => ipcRenderer.send("set-title", title),
});
/**
* 危险操作!暴露 Node.js 的 require 函数到渲染进程
* ⚠️ 严重安全警告:这会导致 XSS 漏洞和系统级攻击风险
* 绝对不要在真实项目中这样做!
*/
contextBridge.exposeInMainWorld("require", require);
四.修改main.js
preload: path.join(__dirname, 'preload.js') // 预加载脚本路径 ipcMain.on('set-title',handlsSetTitle) // 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件 handlsSetTitle 修改应用标题
js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const createWindow = () => {
// 创建一个新的浏览器窗口实例
const mainWindow = new BrowserWindow({
width: 800, // 设置窗口宽度为 800 像素
height: 700, // 设置窗口高度为 600 像素
// 配置网页功能选项
webPreferences: {
nodeIntegration: true , // 允许在渲染进程中使用 Node.js 功能
preload:path.join(__dirname, 'preload.js') // 预加载脚本
}
})
mainWindow.webContents.openDevTools() // 打开开发者工具
// 加载应用程序的主页面 这里加载当前目录下的 index.html 文件
mainWindow.loadFile('index.html')
}
/**
* 处理设置窗口标题的 IPC 消息
* @param {Electron.IpcMainEvent} event - IPC 事件对象
* @param {string} title - 要设置的新窗口标题
*/
const handlsSetTitle = (event, title) => {
console.log('title', event, title)
// 获取发送消息的 WebContents 对象
const webContents = event.sender
// 通过 WebContents 找到对应的 BrowserWindow 实例
const win = BrowserWindow.fromWebContents(webContents)
// 设置窗口标题
win.setTitle(title)
}
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
// 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件
ipcMain.on('set-title',handlsSetTitle)
// 创建主窗口
createWindow()
})
2.双向通信 - 从渲染器进程到主进程,再从主进程到渲染器进程
- 使用ipcRender.invoke 进行发送
- 使用ipcMain.handle 来进行响应
- 它的第二个函数被用一个回调。然后返回值将作为Promise返回到最初的invoker调用
实现本地保存输入框内容,并且页面显示文件大小

一. 添加输入框
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>hello world</h1>
<!-- 标题输入框 -->
Title: <input id="title" />
<button id="btn" type="button">Set</button>
<h1 id="info"></h1>
<!-- 文本输入 -->
Content: <input id="content"/>
<button id="btn2" type="button">Write</button>
<h1 id="counter"></h1>
</body>
<script src="./renderer.js"></script>
</html>
二.在preload.js,添加点击事件
js
const btn2 = document.getElementById('btn2')
const contentInput = document.getElementById('content')
const counter = document.getElementById('counter')
btn2.addEventListener('click', async () => {
const content = contentInput.value
const len = await window.electron.writeFile(content)
console.log(len)
counter.innerHTML = `文件大小: ${len}`
const c = await fs.promises.readFile('test.txt', { encoding: 'utf-8' })
counter.innerHTML += `文件内容: ${c}`
})
三.在预设脚本preload.js ,使用ipcRender.invoke 进行发送
js
contextBridge.exposeInMainWorld("electron", {
setTitle: (title) => ipcRenderer.send("set-title", title),
// 调用主进程的 writeFile 方法,并将内容作为参数传递
writeFile: (content) => ipcRenderer.invoke('write-file', content),
});
四.修改mian.js
pcMain.handle('write-file', handleWriteFile) // 注册 IPC 处理程序:处理渲染进程发送的 'write-file' 请求
handleWriteFile //
js
// 导入 Electron 的核心模块
// app: 控制应用程序的生命周期事件
// BrowserWindow: 创建和控制浏览器窗口
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')
const createWindow = () => {
// 创建一个新的浏览器窗口实例
const mainWindow = new BrowserWindow({
width: 800, // 设置窗口宽度为 800 像素
height: 700, // 设置窗口高度为 600 像素
// 配置网页功能选项
webPreferences: {
nodeIntegration: true , // 允许在渲染进程中使用 Node.js 功能
preload:path.join(__dirname, 'preload.js') // 预加载脚本
}
})
mainWindow.webContents.openDevTools() // 打开开发者工具
// 加载应用程序的主页面 这里加载当前目录下的 index.html 文件
mainWindow.loadFile('index.html')
}
/**
* 处理设置窗口标题的 IPC 消息
* @param {Electron.IpcMainEvent} event - IPC 事件对象
* @param {string} title - 要设置的新窗口标题
*/
const handlsSetTitle = (event, title) => {
console.log('title', event, title)
// 获取发送消息的 WebContents 对象
const webContents = event.sender
// 通过 WebContents 找到对应的 BrowserWindow 实例
const win = BrowserWindow.fromWebContents(webContents)
// 设置窗口标题
win.setTitle(title)
}
/**
*
* @param {*} event
* @param {*} content
* @returns
*/
async function handleWriteFile(event, content) {
console.log('the content', content)
await fs.promises.writeFile('test.txt', content)
const stats = await fs.promises.stat('test.txt')
return stats.size
}
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
// 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件
ipcMain.on('set-title',handlsSetTitle)
// 注册 IPC 处理程序:处理渲染进程发送的 'write-file' 请求
ipcMain.handle('write-file', handleWriteFile)
// 创建主窗口
createWindow()
})
3.单向通信- 从主进程到渲染进程
- 使用win.webContets.send进行发送
- 使用ipcRenderer.on接收
进入页面,页面开始每3秒加3

1.修改mian.js
js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')
const createWindow = () => {
// 创建一个新的浏览器窗口实例
const mainWindow = new BrowserWindow({
width: 800, // 设置窗口宽度为 800 像素
height: 700, // 设置窗口高度为 600 像素
// 配置网页功能选项
webPreferences: {
nodeIntegration: true , // 允许在渲染进程中使用 Node.js 功能
preload:path.join(__dirname, 'preload.js') // 预加载脚本
}
})
mainWindow.webContents.openDevTools() // 打开开发者工具
// 加载应用程序的主页面 这里加载当前目录下的 index.html 文件
mainWindow.loadFile('index.html')
return mainWindow
}
/**
* 处理设置窗口标题的 IPC 消息
* @param {Electron.IpcMainEvent} event - IPC 事件对象
* @param {string} title - 要设置的新窗口标题
*/
const handlsSetTitle = (event, title) => {
console.log('title', event, title)
// 获取发送消息的 WebContents 对象
const webContents = event.sender
// 通过 WebContents 找到对应的 BrowserWindow 实例
const win = BrowserWindow.fromWebContents(webContents)
// 设置窗口标题
win.setTitle(title)
}
/**
*
* @param {*} event
* @param {*} content
* @returns
*/
async function handleWriteFile(event, content) {
console.log('the content', content)
await fs.promises.writeFile('test.txt', content)
const stats = await fs.promises.stat('test.txt')
return stats.size
}
// 当 Electron 完成初始化并准备创建浏览器窗口时触发此事件
app.on('ready', () => {
// 注册 IPC 监听器:监听渲染进程发送的 'set-title' 事件
ipcMain.on('set-title',handlsSetTitle)
// 注册 IPC 处理程序:处理渲染进程发送的 'write-file' 请求
ipcMain.handle('write-file', handleWriteFile)
// 创建主窗口
createWindow()
let counter = 1
const win = createWindow()
// remote.enable(win.webContents)
win.webContents.send('update-counter', counter)
setInterval(() => {
counter += 3
win.webContents.send('update-counter', counter)
}, 3000)
})
-
index.html 添加显示数字元素
html<!-- 显示数字元素 --> <h1 id="time"></h1> -
renderer.js
jsconst time = document.getElementById('time') // window.electron.onUpdateCounter((value) => { time.innerText = value.toString() }) -
preload.js添加 onUpdateCounter
js*/ contextBridge.exposeInMainWorld("electron", { setTitle: (title) => ipcRenderer.send("set-title", title), // 调用主进程的 writeFile 方法,并将内容作为参数传递 writeFile: (content) => ipcRenderer.invoke('write-file', content), // 调用主进程的 readFile 方法,并将回调函数作为参数传递 onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)), });
六. Electron Forge
Electron Forge 是一个用于打包和分发 Electron 应用程序的工具。 它将 Electron 的构建工具生态系统统一到一个可扩展的界面中,这样每个人都可以直接上手制作 Electron 应用。
它的亮点:
- 📦 应用打包和代码签名
- 🚚 Windows、macOS 和 Linux 上的可定制安装程序(DMG、deb、MSI、PKG、AppX 等)
- ☁️ 云提供商(GitHub、S3、Bitbucket 等)的自动化发布流程
- ⚡️ 易于使用的 Webpack 和 TypeScript 样板模板
- ⚙️ 原生 Node.js 模块支持
- 🔌 可扩展的 JavaScript 插件 API
初始化一个新的 Forge 项目
bash
# my-app-electron 项目名称
# --template=vite-typescript 模本用vite+typescript
npm init create-electron-app@latest my-app-electron1 -- --template=vite-typescript
# 进入项目
cd my-app-electron
#运行项目
npm start
结合vue3
bash
# 安装vue
npm install vue
#安装vite识别vue插件
npm install --save-dev @vitejs/plugin-vue
七. 应用打包
macOS打包
需要安装 需通过Electron Forge 的@electron-forge/maker-dmg插件实现
bash
npm install @electron-forge/maker-dmg
操作系统:仅能在macOS上打包DMG(因DMG是macOS专属格式)
forge.config.ts打包文件配置
ts
import type { ForgeConfig } from '@electron-forge/shared-types';
// 每一种打包类型都设计一个单独的npm
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
import { MakerZIP } from '@electron-forge/maker-zip';
import { MakerDeb } from '@electron-forge/maker-deb';
import { MakerRpm } from '@electron-forge/maker-rpm';
// macOS
import { MakerDMG } from '@electron-forge/maker-dmg';
import { VitePlugin } from '@electron-forge/plugin-vite';
import { FusesPlugin } from '@electron-forge/plugin-fuses';
import { FuseV1Options, FuseVersion } from '@electron/fuses';
const config: ForgeConfig = {
// 基础打包配置
packagerConfig: {
asar: true,// 启用 ASAR 归档格式(将应用程序打包为单个文件)
},
rebuildConfig: {},
// 制作包的工具
makers: [
// windows 安装包
new MakerSquirrel({}),
new MakerDMG({
name: 'YourAppName', // DMG文件名(默认与packagerConfig.name一致)
background: 'assets/dmg-background.png', // DMG窗口背景图(推荐1440x900像素)
icon: 'assets/icon.icns', // DMG窗口中的应用图标(与packagerConfig.icon一致)
iconSize: 80, // 图标大小(像素)
format:'ULFO', // 使用ULFO格式,兼容性更好
}
})
new MakerZIP({}, ['darwin']), // ZIP 压缩包生成器(排除 macOS 平台)
new MakerRpm({}),// RPM 包生成器 (RedHat/CentOS/Fedora)
new MakerDeb({}),// DEB 包生成器 (Debian/Ubuntu)
],
plugins: [
new VitePlugin({
build: [
{
entry: 'src/main.ts', // 主进程入口文件
config: 'vite.main.config.ts',// 主进程专用的 Vite 配置文件
target: 'main',// 指定目标为 Electron 主进程
},
{
entry: 'src/preload.ts', // 预加载脚本入口文件
config: 'vite.preload.config.ts',
target: 'preload',
},
],
renderer: [
{
name: 'main_window',
config: 'vite.renderer.config.ts',
},
],
}),
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
};
export default config;
打包命令
bash
# 1. package 命令
npm run package
# 效果:生成可执行程序,但还不是安装包,直接双击就可以用
#2. make 命令
npm run make
# 效果: 生成完整的安装包,需要安装完毕以后使用
可执行程序和安装包区别
| 维度 | 可执行程序(package) | 安装包(make) |
|---|---|---|
| 运行方式 | 直接双击运行(无需安装) | 需运行安装向导,将文件部署到系统目录 |
| 系统影响 | 不修改系统目录(或仅临时缓存) | 写入系统目录、注册元数据、创建快捷方式 |
| 分发形式 | 单文件/文件夹(如 .exe、.app) |
安装包(如 .msi、.pkg、.dmg) |
| 用户体验 | 适合快速测试或便携使用 | 适合正式发布,提供标准化安装/卸载流程 |
package打包文件结构
📂 输出目录结构(以 Windows 为例)
md
out/
└── your-app-name-win32-x64/
├── your-app-name.exe # 主可执行文件(Electron 封装)
├── resources/
│ ├── app.asar # 应用代码(ASAR 归档格式)
│ ├── electron.asar # Electron 运行时(可选)
│ └── ... # 其他资源文件
├── node_modules/ # 生产依赖(仅限被引用的模块)
├── locales/ # 多语言文件(如 zh-CN.pak)
├── swiftshader/ # GPU 渲染备用库(无显卡驱动时)
└── ... # 其他平台相关文件
什么ASAR
一、ASAR 的本质:为什么用它?
传统 Electron 应用打包时,会将代码直接放在 resources/app目录下(明文可见),存在两个问题:
- 源码暴露风险:用户可直接查看/修改 JS/CSS 代码;
- 文件系统性能差:大量小文件读取效率低(尤其 Windows)。
ASAR 解决了这些问题:
- 归档整合 :将分散的文件打包成单个
.asar文件,类似 ZIP 但更高效(无需解压即可随机访问); - 保护源码:虽然非加密(可用工具解压),但增加了直接阅读的门槛;
- 提升性能:减少文件系统调用次数,加快应用启动速度。
二、ASAR 文件结构(以 app.asar为例)
ASAR 文件内部是一个虚拟文件系统 ,结构与你的项目目录一致(但排除了 devDependencies和无关文件)。例如,若你的项目结构如下:
csharp
your-project/
├── package.json # 生产依赖声明(仅保留 dependencies)
├── src/
│ ├── main.js # 主进程代码
│ └── renderer/ # 渲染进程代码
│ ├── index.html
│ └── app.js
├── static/ # 静态资源(图片、字体等)
│ └── logo.png
└── node_modules/ # 仅包含被引用的生产依赖(精简后)
则 app.asar解压后的虚拟结构完全一致(但实际存储为二进制归档):
scss
app.asar (虚拟目录)
├── package.json
├── src/
│ ├── main.js
│ └── renderer/
│ ├── index.html
│ └── app.js
├── static/
│ └── logo.png
└── node_modules/ # 精简后的生产依赖
三、ASAR 的核心操作(开发必备)
- 解压 ASAR 文件(查看/修改源码)
若需调试或修改打包后的代码,可先将 app.asar解压为明文目录:
bash
# 安装 asar 工具(Electron 内置,也可单独安装)
npm install -g @electron/asar
# 解压 app.asar 到 unpacked 目录
asar extract out/your-app-win32-x64/resources/app.asar unpacked/
解压后,unpacked/目录即为明文的项目结构,可直接编辑代码。
- 重新打包为 ASAR
修改完成后,将明文目录重新打包为 app.asar:
shell
asar pack unpacked/ new-app.asar # 将 unpacked/ 打包为 new-app.asar
# 替换原文件:cp new-app.asar out/your-app-win32-x64/resources/app.asar
- 禁用 ASAR(明文目录模式)
若需完全明文(如开发阶段调试),可在 forge.config.js中关闭 ASAR:
java
// forge.config.js
module.exports = {
packagerConfig: {
asar: false, // 禁用 ASAR,生成明文 app 目录(而非 app.asar)
},
};
此时,package命令会在 resources/下生成 app/明文目录(而非 app.asar)。