什么是 Electron
Electron 是⼀个 跨平台 的 桌⾯应⽤ 开发框架,开发者可以使⽤:HTML、CSS、JavaScript 等 Web 技术来构建桌⾯应⽤程序,
它的本质是结合了 Chromium 和 Node.js,现在⼴泛⽤于桌⾯应 ⽤程序开发,例如这写桌⾯应⽤都⽤到了 Electron 技术:
- 一款应用广泛的跨平台的桌面应用开发框架。
- Electron 的本质是结合了
Chromium与Node.js - 使用 HTML、CSS、JS 等 Web 技术构建桌面应用程序。
Electron = Chromium + Node.js + Native API(Electron 原生的 API)
TIPCS架构的软件有个优点:它是存在我们电脑包里面的,你的电脑不被木马黑掉,那么这个软件就是没有问题的,网页的话各种接口、数据、图片我们都是可以看到的;
但是CS架构软件我们可以抓包,我们也可以进行防止抓包,CS软件要比BS更加安全
总的来说就是,速度快,安全(vscode就是electron开发的)
常见的桌面GUI
| 名称 | 语音 | 优点 | 缺点 |
|---|---|---|---|
| QT | C++ | 跨平台、性能好、生态好 | 依赖多,程序包大 |
| PyQT | Python | 底层集成度高、易上手 | 授权问题(收费) |
| WPF | C# | 类库丰富、扩展灵活 | 只支持Windows,程序包大 |
| WinForm | C# | 性能好,组件丰富,易上手 | 只支持Windows,UI差 |
| Swing | Java | 基于AWT,组件丰富 | 性能差,UI一般 |
| NW.js | JS | 跨平台性好,界面美观 | 底层交互差、性能差,包大 |
| Electron | JS | 相比NW拓展更好 | 底层交互差、性能差,包大 |
| CEF | C++ | 性能好,灵活集成,UI美观 | 占用资源多,包大 |
Electron 流程模型
主进程就是个.js文件,这个 js 文件是个纯粹的 node 环境;主进程主要目的就是管理渲染进程;主进程可以管理多个渲染进程,主进程只有一个,渲染进程可以有n个
主进程是可以调用原生 API 的
一个窗口背后对应一个渲染进程,渲染进程就是浏览器环境,需要用 HTML、CSS、JS 来支撑
进程间的通信简称 IPC

TIPNode 环境(Node.js runtime environment)的内置模块:核心 API,比如: fs(文件系统) http / https(网络) path(路径处理) os(系统信息)等等,还有 JS 执行环境(V8);
Node 环境不包含 DOM / BOM,没有 window、document、alert、localStorage。因为 Node 不运行在浏览器里,它不关心页面。
搭建工程
npm init
# 然后一路回车到底修改package.json文件
{ "name": "electron_test", "version": "1.0.0", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "zzy", "license": "ISC", "description": "this is a electron demo"}安装 Electron
npm i electron -D修改这里:
"scripts": { "start": "electron ." },根目录创建 main.js 文件
const { app, BrowserWindow } = require("electron");
app.on("ready", () => { const win = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, // 是否隐藏顶部菜单栏 alwaysOnTop: false, // 是否一直置顶 }); win.loadFile("./pages/index.html");});关于 BrowserWindow 的更多配置项,请参考:BrowserWindow 实例属性
根目录创建pages/index.html文件以及pages/index.css文件
运行
npm start此时开发者⼯具会报出⼀个安全警告,需要修改index.html,配置 CSP(Content- Security-Policy)
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"/>上述配置的说明
- default-src ‘self’ default-src:配置加载策略,适用于所有未在其它指令中明确指定的资源类型。 self:仅允许从同源的资源加载,禁止从不受信任的外部来源加载,提高安全性。
- style-src ‘self’ ‘unsafe-inline’ style-src:指定样式表(CSS)的加载策略。 self:仅允许从同源的资源加载,禁止从不受信任的外部来源加载,提高安全性。 unsafe-inline:允许在 HTML 文档内使用内联样式。
- img-src ‘self’ data: img-src:指定图像资源的加载策略。 self:表示仅允许从同源加载图像。 data::允许使用 data: URI 来嵌入图像。这种 URI 模式允许将图像数据直接嵌入到 HTML 或 CSS 中,而不是通过外部链接引用。 关于 CSP 的详细说明请参考:MDN-Content-Security-Policy、Electron Security
完善窗口行为
- Windows 和 Linux 平台窗⼝特点是:关闭所有窗⼝时退出应⽤。
- mac 应⽤即使在没有打开任何窗⼝的情况下也继续运⾏,并且在没有窗⼝可⽤的情况下激活 应⽤时会打开新的窗⼝。
在 main.js 中添加如下代码
const { app, BrowserWindow } = require("electron");
function createWindow() { const win = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, // 是否隐藏顶部菜单栏 alwaysOnTop: false, // 是否一直置顶 }); win.loadFile("./pages/index.html");}
app.on("ready", () => { createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });});
app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit();});配置⾃动重启
- 安装 Nodemon
npm i nodemon -D- 修改 package.json 文件
"scripts": { "start": "nodemon --exec electron ."}- 配置 nodemon.json 规则
根目录创建
nodemon.json文件
{ "ignore": ["node_modules", "dist"], "restartable": "r", "watch": ["*.*"], "ext": "html,js,css"}主进程与渲染进程
主进程 每个 Electron 应⽤都有⼀个单⼀的主进程,作为应⽤程序的⼊⼝点。 主进程在 Node.js 环境中运 ⾏,它具有 require 模块和使⽤所有 Node.js API 的能⼒,主进程的核⼼就是:使用BrowserWindow来创建和管理窗口。
渲染进程
每个 BrowserWindow 实例都对应一个单独的渲染器进程,运行在渲染器进程中的代码,必须遵守网页标准,这也就意味着:渲染器进程无权直接访问 require 或使用任何 Node.js 的 API。
问题产生:处于渲染器进程的用户界面,该怎样才与 Node.js 和 Electron 的原生桌面功能进行交互呢?
Preload 脚本
预加载(Preload)脚本是运行在渲染进程中的,但它是在网页内容加载之前执行的,这意味着它具有比普通渲染器代码更高的权限,可以访问 Node.js 的 API,同时又可以与网页内容进行安全的交互。
简单说:它是 Node.js 和 Web API 的桥梁,Preload 脚本可以安全地将部分 Node.js 功能暴露给网页,从而减少安全风险。
预加载脚本应用
主进程:根目录的main.js
const { app, BrowserWindow } = require("electron");const path = require("path");
function createWindow() { const win = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, // 是否隐藏顶部菜单栏 alwaysOnTop: false, // 是否一直置顶 webPreferences: { // 可以在安全的情况下,把 Node.js / Electron 的 API 暴露给网页。 preload: path.resolve(__dirname, "./preload.js"), }, }); win.loadFile("./pages/index.html");}
app.on("ready", () => { createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });});
app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit();});根目录下的perload.js
const { contextBridge } = require("electron"); // 从 Electron 引入 contextBridge,它是 上下文桥接 API。console.log("proload...");
// exposeInMainWorld 的意思是:把对象注入到渲染进程的 全局 window 上。contextBridge.exposeInMainWorld("myAPI", { version: process.version, // // Node.js 的版本号});pages/render.js
const btn1 = document.getElementById("btn1");
btn1.onclick = () => { alert(myAPI.version) console.log(window);};点击按钮后即可看到版本号
TIP
- 脚本的执行顺序:主进程->预加载脚本->渲染进程 (preload.js 是一个 预加载脚本,它会在 渲染进程加载页面之前运行。)
- 预加载脚本可以使用一部分Node的API
进程通信(IPC)
上文中的 preload.js ,无法使用全部 Node 的 API ,比如:不能使用 Node 中的 fs 模块,但主进程(main.js)是可以的,这时就需要进程通信了。简单说:要让 preload.js 通知 main.js 去调用 fs 模块去干活。
关于 Electron 进程通信,我们要知道:
- IPC 全称为:
InterProcess Communication,即:进程通信。 - IPC 是
Electron中最为核心的内容,它是从 UI 调用原生 API 的唯一方法! - Electron 中,主要使用
ipcMain和ipcRenderer来定义“通道”,进行进程通信。
渲染进程->主进程(单项)
概述: 在渲染器进程中 ipcRenderer.send 发送消息,在主进程中使用 ipcMain.on 接收消息。
常用于: 在 Web 中调用主进程的 API,例如下面的这个需求:
需求: 点击按钮后,在用户的 D 盘创建一个
hello.txt文件,文件内容来自于用户输入。
思路肯定是:从渲染进程拿到用户输入的信息,想办法通知给主进程,让主进程拿到用户的输入,写入D盘,hello.txt
页面中添加相关元素,render.js中添加对应脚本
<!DOCTYPE html><html> <head> <title>Electron demo</title> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"/> <link rel="stylesheet" href="./index.css" /> </head> <body> <button id="btn1">按钮</button> <hr> <br> <br> <input id="input" /> <button id="btn2">向D盘写入hello.txt</button>
<script type="text/javascript" src="./render.js"></script> </body></html>const btn1 = document.getElementById("btn1");const btn2 = document.getElementById("btn2");const input = document.getElementById("input");
btn1.onclick = () => { alert(myAPI.version); console.log(window);};
btn2.onclick = () => { myAPI.saveFile(input.value);};preload.js 中使用 ipcRenderer.send('信道', 参数) 发送消息,与主进程通信。
const { contextBridge, ipcRenderer } = require("electron");console.log("proload...");
contextBridge.exposeInMainWorld("myAPI", { version: process.version, saveFile: (data) => { ipcRenderer.send("file-save", data); // 渲染进程给主进程发送⼀个消息 },});主进程中,在加载页面之前,使用 ipcMain.on('信道', 回调) 配置对应回调函数,接收消息。
const { app, BrowserWindow, ipcMain } = require("electron");const fs = require("fs");const path = require("path");
function writeFile(_, data) { fs.writeFileSync("D:/hello.txt", data);}
function createWindow() { const win = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, // 是否隐藏顶部菜单栏 alwaysOnTop: false, // 是否一直置顶 webPreferences: { preload: path.resolve(__dirname, "./preload.js"), }, }); ipcMain.on("file-save", writeFile); win.loadFile("./pages/index.html");}TIP主进程你就引入
ipcMain模块,渲染进程你就引入ipcRenderer模块。
ipcMain.on(channel, listener)– 监听指定通道的消息,当收到时调用 listener 函数。"file-save":通道名,自定义字符串; writeFile:监听器函数(listener),这是一个回调函数。当消息到达时,Electron 会自动调用 writeFile
ipcRenderer.send(channel, ...args)– 向主进程发送异步消息;"file-save":通道名,必须与主进程监听的相同。data:发送的数据;
渲染进程<=>主进程(双向)
概述:渲染进程通过ipcRenderer.invoke发送消息,主进程使用ipcMain.handle接收并处理消息
备注: ipcRender.invoke 的返回值是 Promise 实例。
常用于:从渲染器进程调用主进程方法并等待结果,例如下面的这个需求:
需求:点击按钮从 D 盘读取 hello.txt 中的内容,并将结果呈现在页面上。
思路:渲染进程要想办法告诉主进程要读取hello.txt文件,主进程读取文件后,把文件内容返回给渲染进程,渲染进程再把文件内容呈现在页面上。
页面中添加相关元素,render.js中添加对应脚本
<!DOCTYPE html><html> <head> <title>Electron demo</title> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"/> <link rel="stylesheet" href="./index.css" /> </head> <body> <button id="btn1">按钮</button> <hr> <br> <br> <input id="input" /> <button id="btn2">向D盘写入hello.txt</button> <br> <br> <br> <hr> <button id="btn3">读取D盘中的hello.txt</button>
<script type="text/javascript" src="./render.js"></script> </body></html>preload.js 中使用 ipcRenderer.invoke('信道', 参数) 发送消息,与主进程通信。
const { contextBridge, ipcRenderer } = require("electron");console.log("proload...");
contextBridge.exposeInMainWorld("myAPI", { version: process.version, saveFile: (data) => { ipcRenderer.send("file-save", data); }, readFile: () => { return ipcRenderer.invoke("file-read"); },});主进程中,在加载页面之前,使用 ipcMain.handle('信道', 回调) 接收消息,并配置回调函数。
const { app, BrowserWindow, ipcMain } = require("electron");const fs = require("fs");const path = require("path");
function readFile() { return fs.readFileSync("D:/hello.txt").toString();}
function createWindow() { const win = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, // 是否隐藏顶部菜单栏 alwaysOnTop: false, // 是否一直置顶 webPreferences: { preload: path.resolve(__dirname, "./preload.js"), }, }); ipcMain.on("file-save", writeFile); ipcMain.handle("file-read", readFile); win.loadFile("./pages/index.html");}TIP
const result = await ipcRenderer.invoke(channel, ...args);用于在预加载脚本中发起一个异步请求到主进程,并等待返回 Promise 结果的通信方法。channel (必需): 一个字符串,是消息的通道名称或事件名称。。这个名称必须在主进程中由
ipcMain.handle()进行监听。…args (可选): 一个或多个任意类型的参数,会作为参数传递给主进程中的处理函数。
返回值: 返回一个 Promise。
ipcMain.handle('channel-name', handler)参数1:channel(通道名),类型:string;标识这个处理器监听哪个“频道”。必须与渲染进程 ipcRenderer.invoke(channel, …) 中的 channel 完全一致。
参数2:listener(监听器函数),类型:Function;当对应 channel 的请求到达时,执行这个函数。
WARNING渲染进程与渲染进程之间是不能传东西的,可以用主进程作为中间人
打包应用
使用 electron-builder 打包应用
- 安装 electron-builder:
npm install electron-builder -D- 在 package.json 中进行相关配置,具体配置如下:
备注: json 文件不支持注释, 使用时请去掉所有注释。
{ "name": "video-tools", "version": "1.0.0", "main": "main.js", "scripts": { "start": "electron .", "build": "electron-builder" }, "build": { "appId": "com.atguigu.video", "win": { "icon": "./logo.ico", "target": [ { "target": "nsis", "arch": [ "x64" ] } ] }, "nsis": { "oneClick": false, "perMachine": true, "allowToChangeInstallationDirectory": true } }, "devDependencies": { "electron": "^30.0.0", "electron-builder": "^24.13.3", "author": "tianyu", "license": "ISC", "description": "A video processing program based on Electron" }}注释版本
``` { "name": "video-tools", // 应⽤程序的名称 "version": "1.0.0", // 应⽤程序的版本 "main": "main.js", // 应⽤程序的⼊⼝⽂件 "scripts": { "start": "electron .", // 使⽤ `electron .` 命令启动应⽤程序 "build": "electron-builder" // 使⽤ `electron-builder` 打包应⽤程序,⽣成 安装包 }, "build": { "appId": "com.atguigu.video", // 应⽤程序的唯⼀标识符 // 打包windows平台安装包的具体配置 "win": { "icon":"./logo.ico", //应⽤图标 "target": [ { "target": "nsis", // 指定使⽤ NSIS 作为安装程序格式 "arch": ["x64"] // ⽣成 64 位安装包 } ] }, "nsis": { "oneClick": false, // 设置为 `false` 使安装程序显示安装向导界⾯,⽽不是⼀ 键安装 "perMachine": true, // 允许每台机器安装⼀次,⽽不是每个⽤户都安装 "allowToChangeInstallationDirectory": true // 允许⽤户在安装过程中选择 安装⽬录 } }, "devDependencies": { "electron": "^30.0.0", // 开发依赖中的 Electron 版本 "electron-builder": "^24.13.3" // 开发依赖中的 `electron-builder` 版本 }, "author": "tianyu", // 作者信息 "license": "ISC", // 许可证信息 "description": "A video processing program based on Electron" // 应⽤程 序的描述 } ```拿一个script中的"build": "electron-builder"以及build中内容
最终package.json内容如下
{ "name": "electron_test", "version": "1.0.0", "main": "main.js", "scripts": { "start": "nodemon --exec electron .", "build": "electron-builder" }, "build": { "appId": "com.atguigu.video", "win": { "icon": "./logo.png", "target": [ { "target": "nsis", "arch": [ "x64" ] } ] }, "nsis": { "oneClick": false, "perMachine": true, "allowToChangeInstallationDirectory": true } }, "author": "zzy", "license": "ISC", "description": "this is a electron demo", "devDependencies": { "electron": "^38.1.0", "electron-builder": "^26.0.12", "nodemon": "^3.1.10" }}- 执行打包命令
npm run build如果打包报错,我们可以开启windows的开发人员模式,系统->开发人员模式;然后再执行打包命令就可以;
打包后的exe文件就在dist包下;
electron-vite
electron-vite 是⼀个新型构建⼯具,旨在为 Electron 提供更快、更精简的体验
electron-vite 快速、简单且功能强⼤,旨在开箱即⽤。 官⽹地址:https://cn-evite.netlify.app/
项目实战
初始化项目
npm init vite安装依赖
npm inpm i electron -D删除 package.json 中的 “type”: “module”。改用 CommonJS 规范
根目录创建main.js文件
const { app, BrowserWindow } = require("electron");const createWindow = () => { const win = new BrowserWindow({ width: 1000, height: 800, });};
app.whenReady().then(() => { createWindow();});修改package.json
"main": "main.js","scripts": { "start": "electron ."},安装nodemon
npm i nodemon -D修改package.json
"scripts": { "start": "nodemon --exec electron . --watch ./ --ext .js,.html,.css,.vue"},去掉CSP(Content- Security-Policy)警告,在index.html添加:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"/>安装
npm i electron-win-state -D根目录创建preload目录,再创建index.js
最终根目录下的main.js
const { app, BrowserWindow } = require("electron");const WinState = require("electron-win-state").default;const path = require("path");
const createWindow = () => { const winState = new WinState({ defaultWidth: 1000, defaultHeight: 800, });
const win = new BrowserWindow({ ...winState.winOptions, webPreferences: { preload: path.resolve(__dirname, "./preload/index.js"), },
});
win.loadURL("http://localhost:3000"); win.webContents.openDevTools(); // 打开控制台 winState.manage(win);};
app.whenReady().then(() => { createWindow();
app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });});
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit()})package.json
{ "name": "pin-box", "private": true, "version": "0.0.0", "main": "main.js", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "start": "nodemon --exec electron . --watch ./ --ext .js,.html,.css,.vue" }, "dependencies": { "vue": "^3.5.13" }, "devDependencies": { "@types/electron": "^1.4.38", "@types/nodemon": "^1.19.6", "@vitejs/plugin-vue": "^5.2.1", "electron": "^38.1.0", "electron-win-state": "^1.1.22", "nodemon": "^3.1.10", "vite": "^6.0.1" }}启动项目 启动vite项目
npm run dev启动electron
npm startmain.js中的load地址为vite项目的启动地址
优雅打开窗口
main.js中
const { app, BrowserWindow } = require("electron");const WinState = require("electron-win-state").default;const path = require("path");
const createWindow = () => { const winState = new WinState({ defaultWidth: 1000, defaultHeight: 800, });
const win = new BrowserWindow({ ...winState.winOptions, webPreferences: { preload: path.resolve(__dirname, "./preload/index.js"), }, show: false, });
win.loadURL("http://localhost:5173"); win.webContents.openDevTools(); // 打开控制台 winState.manage(win);
win.on("ready-to-show", () => { win.show(); });};
app.whenReady().then(() => { createWindow();
app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });});
app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit();});整理vue项目
删除components目录下的所有文件
修改App.vue
<template> <div> hello </div></template>
<script setup lang="ts">import { ref, reactive, toRefs, onMounted} from 'vue'
</script>
<style scoped lang="scss"></style>src下创建views文件夹,再创建Home.vue
安装stylus或者安装sass
应用更新
electron 应用更新-基于github
生成github token: https://github.com/settings/tokens/new
修改配置文件package.json
主要是build和release命令
{ "name": "electron_test", "version": "1.0.7", "main": "main.js", "scripts": { "start": "nodemon --exec electron .", "build": "electron-builder", "release": "electron-builder --win -p always" }, "build": { "appId": "com.zzyang.video", "publish": { "provider": "github", "token": "你的token", "owner": "zxyang3636", "private": true, "releaseType": "release", "repo": "electron_test" }, "win": { "icon": "./logo.png", "target": [ { "target": "nsis", "arch": [ "x64" ] } ] }, "nsis": { "oneClick": false, "perMachine": true, "allowToChangeInstallationDirectory": true } }, "author": "zzy", "license": "ISC", "description": "this is a electron demo", "devDependencies": { "electron": "^38.1.0", "electron-builder": "^26.0.12", "nodemon": "^3.1.10" }, "dependencies": { "electron-log": "^5.4.3", "electron-updater": "^6.6.2" }}releaseType: // “draft” | “prerelease” | “release” | “undefined” 默认草稿状态
repo 你的仓库名称
参考:github
评论区
评论区加载中...
如果长时间无法显示,请尝试刷新页面。