1.什么是 Electron?
Electron
是一个跨平台桌面应用开发框架,开发者可以利用 HTML
、CSS
、JavaScript
等Web
技术来构建桌面应用程序。它本质上是结合了 Chromium
和 Node.js
,目前广泛用于桌面应用程序开发。例如,许多桌面应用都采用了 Electron
技术。
Electron
官网https://www.electronjs.org/
以下这些都是使用了 Electron
技术开发的桌面应用程序的例子:
Visual Studio Code
: 一个流行的开源代码编辑器,由 Microsoft
开发。
GitHub Desktop
: GitHub
官方提供的桌面客户端,用于管理 Git
代码仓库。
1Password
: 一个密码管理工具,提供安全的密码保存和管理功能。
新版 QQ
: QQ
的桌面应用程序,使用 Electron
实现跨平台的桌面体验。
2. Electron 的优势
- 可跨平台:同一套代码可以构建出能在:
Windows
、macOS
、Linux
上运行的应用程序。 - 上手容易:使用
Web
技术就可以轻松完成开发桌面应用程序。 - 底层权限:允许应用程序访问文件系统、操作系统等底层功能,从而实现复杂的系统交互。
- 社区支持:拥有一个庞大且活跃的社区,开发者可以轻松找到文档、教程和开源库。(额,这个国内教程很少,不是很赞同~)
3. Electron 技术架构
3.1. 技术架构
Electron = Chromium + Node.js + Native API
这个公式表明了 Electron
框架的核心组成部分:
Chromium
: 提供了现代的 Web
渲染引擎和许多与浏览器相关的功能,用于渲染和显示应用程序的用户界面。
Node.js
: 提供了访问操作系统和文件系统等底层功能的能力,以及在后端执行 JavaScript
代码的环境。
Native API
: 允许 Electron
应用程序直接访问操作系统的原生功能,例如文件系统、系统通知等,从而实现更复杂和更深入的系统交互和集成。
3.2. 进程模型
- 操作系统层
Windows, Linux, Mac
:这三个操作系统是应用程序可以运行的环境。它们提供了底层的系统资源和API
接口。 - 主进程 (
Main Process
)
Main
:这是应用程序的核心部分,通常是一个Node.js
进程。它管理应用的生命周期、窗口创建、以及与渲染进程的通信。 - 渲染进程 (
Render Process
)
Render
:每个渲染进程负责展示用户界面。它运行在Chromium
中,支持HTML
、CSS
和JavaScript
。每个渲染进程与主进程通过IPC
(进程间通信)进行通信。 IPC (Inter-Process Communication)
IPC
:这是主进程与渲染进程之间的通信机制。通过IPC
,主进程可以发送消息给渲染进程,反之亦然。这种机制使得各部分能够独立运行,同时保持数据和命令的流动。Chromium
Chromium
:这是一个开源浏览器项目,渲染进程基于Chromium
引擎,负责处理网页内容的渲染。
4. 搭建一个工程(实战Demo)
- 首先创建一个文件夹并初始化
npm
包。
mkdir my-electron-app && cd my-electron-app
npm init
package.json
修改为如下:
{"name": "my-electron-app","version": "1.0.0","main": "main.js","scripts": {"start": "electron ."},"author": "RanGuMo","description": "this is a electron demo","license": "ISC"
}
有几条规则需要遵循:
entry point
应为 main.js
.(electron
官方推荐叫mian.js
,你也可以用其他名字)
author
与 description
可为任意值,但对于应用打包是必填项。
- 安装
electron
作为开发依赖。
npm install --save-dev electron
或者
npm install -D electron
- 在
main.js
中编写代码,创建一个基本窗口,内容如下:
// main.js运行在应用的主进程上,无法访问web相关API,主要负责:控制生命周期、显示界面
// 控制渲染进程等其他操作。const { app, BrowserWindow } = require("electron");
const path = require("path");// 保持对window对象的全局引用,如果不这么做的话,当JavaScript对象被
// 垃圾回收的时候,window对象将会自动的关闭
let mainWindow;// 1.创建浏览器窗口。
function createWindow() {// 1.1.创建浏览器窗口。mainWindow = new BrowserWindow({width: 800, // 宽度height: 600, // 高度autoHideMenuBar: true, // 自动隐藏菜单栏(默认是false)alwysOnTop: true, // 窗口置顶(类似z-index:9999,永远置于最高层) (默认是false)x: 0, // 窗口左上角x坐标y: 0, // 窗口左上角y坐标});// 1.2.加载一个远程的页面mainWindow.loadURL("http://www.baidu.com");
}// 1.3.当 window 被加载,就执行创建窗口这个函数app.on("ready", () => {createWindow();});// 1.4.当 window 被关闭,这个事件会被触发。app.on("closed", function () {// 取消引用 window 对象,如果你的应用支持多窗口的话,// 通常会把多个 window 对象存放在一个数组里面,// 与此同时,你应该删除相应的元素。mainWindow = null;});
关于 BrowserWindow
的更多配置项,请参考:BrowserWindow实例属性
- 启动应用,查看效果
npm start
5. 加载本地页面
- 创建
pages/index.html
编写内容:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>index</title>
</head>
<body><h1>hello world</h1>
</body>
</html>
- 修改
mian.js
加载本地页面
// 1.2.加载一个远程的页面// mainWindow.loadURL("http://www.baidu.com");// 1.2.加载一个本地的页面 mainWindow.loadFile('./pages/index.html')
Ctrl+Shift+i
进入控制台,此时开发者工具会报出一个安全警告,需要修改index.html
,配置CSP(ContentSecurity-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-inlinestyle-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
6. 完善窗口行为
1.Windows
和 Linux
平台窗口特点是:关闭所有窗口时退出应用
// 1.5.当所有窗口都关闭时,自动退出应用,除非在 macOS 上
app.on("window-all-closed", () => {if (process.platform !== "darwin") {app.quit();}
});
mac
应用即使在没有打开任何窗口的情况下也继续运行,并且在没有窗口可用的情况下激活应用时会打开新的窗口。
// 1.3.当 window 被加载,就执行创建窗口这个函数
app.on("ready", () => {createWindow();// 1.6.在 mac上,点击 Dock 图标且没有其他窗口打开时,重新创建窗口// 当应用被激活时 app.on("activate", () => {// 如果没有窗口打开,则创建一个窗口if (BrowserWindow.getAllWindows().length === 0) {createWindow();}});
});
7. 配置自动重启
- 安装
Nodemon
npm i nodemon -D
- 修改
package.json
启动的命令
"scripts": {"start": "nodemon --exec electron ."},
- 新建
nodemon.json
文件并配置规则
{"ignore": ["node_modules","dist"],"restartable": "r","watch": ["*.*"],"ext": "html,js,css"
}
8.主进程与渲染进程(重点)
在 Electron
中主要控制两类进程: 主进程、渲染器进程。
- 主进程
每个Electron
应用都有一个单一的主进程,作为应用程序的入口点。 主进程在Node.js
环境中运行,它具有require
模块和使用所有Node.js API
的能力,主进程的核心就是:使用BrowserWindow
来创建和管理窗口。 - 渲染进程
每个BrowserWindow
实例都对应一个单独的渲染器进程,运行在渲染器进程中的代码,必须遵守网页标准,这也就意味着:渲染器进程无权直接访问require
或使用任何Node.js
的API
。
问题产生:处于渲染器进程的用户界面,该怎样才与
Node.js
和Electron
的原生桌面功能进行交互呢? 通过Preload
脚本
9.Preload 脚本
预加载(Preload
)脚本是运行在渲染进程中的,但它是在网页内容加载之前执行的,这意味着它具有比普通渲染器代码更高的权限,可以访问 Node.js
的 API
,同时又可以与网页内容进行安全的交互。
简单说:它是 Node.js
和 Web API
的桥梁,Preload
脚本可以安全地将部分 Node.js
功能暴露给网页,从而减少安全风险。
需求:点击按钮后,在页面呈现当前电脑的
Node
版本。
具体文件结构与编码如下:
- 在根目录创建预加载脚本
preload.js
,内容如下:
const { contextBridge } = require('electron');// 暴露数据给渲染进程
contextBridge.exposeInMainWorld('myNodeVersion', {version: process.version
})
- 在主线程中引入
preload.js
const path = require("path");// 1.1.创建浏览器窗口。mainWindow = new BrowserWindow({width: 800, // 宽度height: 600, // 高度autoHideMenuBar: true, // 自动隐藏菜单栏(默认是false)alwysOnTop: true, // 窗口置顶(类似z-index:9999,永远置于最高层) (默认是false)x: 0, // 窗口左上角x坐标y: 0, // 窗口左上角y坐标webPreferences:{preload: path.join(__dirname, "./preload.js")}});
- 在
html
页面中编写对应按钮,并创建专门编写网页脚本的render.js
,随后引入。
index.html
内容如下:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><!-- 内容安全策略 --><meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"><title>index</title>
</head>
<body><h1>hello world!!</h1><button id="btn">获取Node版本号</button><script type="text/javascript" src="./render.js"></script>
</body>
</html>
- 在渲染进程中使用
version
render.js
内容如下:
btn.addEventListener('click', () => {console.log(myNodeVersion.version)document.body.innerHTML += `<h2>${myNodeVersion.version}</h2>`
})
- 整体文件结构如下:
- 效果演示:
10.进程通信(IPC)(重点)
值得注意的是:
上文中的preload.js
,无法使用全部Node.js
的API
,比如:不能使用Node
中的fs
模块,但主进程(main.js
)是可以的,这时就需要进程通信了。简单说:要让preload.js
通知main.js
去调用fs
模块去干活。
关于 Electron
进程通信,我们要知道:
IPC
全称为:InterProcessCommunication
,即:进程通信。IPC
是Electron
中最为核心的内容,它是从UI
调用原生API
的唯一方法!Electron
中,主要使用ipcMain
和ipcRenderer
来定义“通道”,进行进程通信。
10.1.渲染进程==>主进程(单向)
概述:在渲染器进程中 ipcRenderer.send
发送消息,在主进程中使用 ipcMain.on
接收消息。常用于:在 Web
中调用主进程的 API
,例如下面的这个需求:
需求:点击按钮后,在用户的
D
盘创建一个hello.txt
文件,文件内容来自于用户输入。
index.html
页面中添加相关元素,render.js 中添加对应的点击事件
index.html
内容:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><!-- 内容安全策略 --><meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"><title>index</title>
</head>
<body><h1>hello world!!</h1><button id="btn">获取Node版本号</button><br><input type="text" id="content"><button id="btn2">在用户的D盘创建一个hello.txt</button><script type="text/javascript" src="./render.js"></script>
</body>
</html>
render.js
内容:
// 可以不用写const btn = document.getElementById('btn')
btn.addEventListener("click", () => {console.log(myNodeVersion.version);document.body.innerHTML += `<h2>${myNodeVersion.version}</h2>`;
});const btn2 = document.getElementById("btn2");
const content = document.getElementById("content");
btn2.addEventListener("click", () => {console.log(content.value);myAPI.saveFile(content.value);
});
preload.js
中使用ipcRenderer.send('信道',参数)
发送消息,与主进程通信。
preload.js
内容:
const { contextBridge, ipcRenderer } = require('electron');// 暴露数据给渲染进程
contextBridge.exposeInMainWorld('myNodeVersion', {version: process.version
})contextBridge.exposeInMainWorld('myAPI', {saveFile(str){//渲染进程给主进程发送消息事件ipcRenderer.send('create-file', str)}
})
- 主进程中,在加载页面之前,使用
ipcMain.on('信道”,回调)
配置对应回调函数,接收消息。
main.js
内容:
// 引入:app(整个应用)、BrowserWindow(用于创建窗口)、ipcMain(用于进程通信)
const { app, BrowserWindow, ipcMain } = require('electron');
// 引入path模块
const path = require('path');
// 引入fs模块
const fs = require('fs');
// 2.创建文件
function createFile(event,data) {fs.writeFileSync('D:/hello.txt',data)
}// 1.创建浏览器窗口。
function createWindow() {// 1.1.创建浏览器窗口。mainWindow = new BrowserWindow({width: 800, // 宽度height: 600, // 高度autoHideMenuBar: true, // 自动隐藏菜单栏(默认是false)alwysOnTop: true, // 窗口置顶(类似z-index:9999,永远置于最高层) (默认是false)x: 0, // 窗口左上角x坐标y: 0, // 窗口左上角y坐标webPreferences:{preload: path.join(__dirname, "./preload.js")}});// 2.1.主进程注册对应的事件ipcMain.on("create-file",createFile)// 1.2.加载一个本地的页面mainWindow.loadFile("./pages/index.html");
}
完整的main.js
如下:
// main.js运行在应用的主进程上,无法访问web相关API,主要负责:控制生命周期、显示界面
// 控制渲染进程等其他操作。// 引入:app(整个应用)、BrowserWindow(用于创建窗口)、ipcMain(用于进程通信)
const { app, BrowserWindow, ipcMain } = require('electron');
// 引入path模块
const path = require('path');
// 引入fs模块
const fs = require('fs');// 保持对window对象的全局引用,如果不这么做的话,当JavaScript对象被
// 垃圾回收的时候,window对象将会自动的关闭
let mainWindow;// 2.创建文件
function createFile(event,data) {fs.writeFileSync('D:/hello.txt',data)
}// 1.创建浏览器窗口。
function createWindow() {// 1.1.创建浏览器窗口。mainWindow = new BrowserWindow({width: 800, // 宽度height: 600, // 高度autoHideMenuBar: true, // 自动隐藏菜单栏(默认是false)alwysOnTop: true, // 窗口置顶(类似z-index:9999,永远置于最高层) (默认是false)x: 0, // 窗口左上角x坐标y: 0, // 窗口左上角y坐标webPreferences:{preload: path.join(__dirname, "./preload.js")}});// 2.1.主进程注册对应的事件ipcMain.on("create-file",createFile)// 1.2.加载一个远程的页面// mainWindow.loadURL("http://www.baidu.com");// 1.2.加载一个本地的页面mainWindow.loadFile("./pages/index.html");
}// 1.3.当 window 被加载,就执行创建窗口这个函数
app.on("ready", () => {createWindow();// 1.6.在 mac上,点击 Dock 图标且没有其他窗口打开时,重新创建窗口// 当应用被激活时 app.on("activate", () => {// 如果没有窗口打开,则创建一个窗口if (BrowserWindow.getAllWindows().length === 0) {createWindow();}});
});// 1.4.当 window 被关闭,这个事件会被触发。
app.on("closed", function () {// 取消引用 window 对象,如果你的应用支持多窗口的话,// 通常会把多个 window 对象存放在一个数组里面,// 与此同时,你应该删除相应的元素。mainWindow = null;
});// 1.5.当所有窗口都关闭时,自动退出应用,除非在 macOS 上
app.on("window-all-closed", () => {if (process.platform !== "darwin") {app.quit();}
});
- 运行项目,即可实现效果。
10.2.渲染进程<==>主进程(双向)
概述:渲染进程通过ipcRenderer.invoke
发送消息,主进程使用 ipcMain.handle
接收并处理消息。
备注:ipcRender.invoke
的返回值是 Promise
实例。
常用于:从渲染器进程调用主进程方法并等待结果,例如下面的这个需求:
需求:点击按钮从
D
盘读取hello.txt
中的内容,并将结果呈现在页面上。
- 页面中添加相关元素,
render.js
中添加对应脚本
index.html
<button id="btn3">读取⽤户D盘的hello.txt</button>
render.js
const btn3 = document.getElementById("btn3");
btn3.addEventListener("click", async () => {let data = await myAPI.readFile();document.body.innerHTML += `<h2>${data}</h2>`;
});
preload.js
中使用ipcRenderer.invoke('信道',参数)
发送消息,与主进程通信。
preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("myAPI", {saveFile(str) {//渲染进程给主进程发送消息事件,通过 IPC 发送 'file-save' 事件并传递数据ipcRenderer.send("create-file", str);},// 定义读取文件的方法,通过 IPC 调用 'file-read' 事件并返回 Promise 结果readFile: () => {return ipcRenderer.invoke("file-read");},
});
- 主进程中,在加载页面之前,使用
ipcMain.handle('信道',回调)
接收消息,并配置回调函数。
main.js
// 3.读取文件,读取指定文件内容并返回字符串格式的结果
function readFile() {const res = fs.readFileSync('D:/hello.txt').toString();return res;
}// 1.创建浏览器窗口。
function createWindow() {// 1.1.创建浏览器窗口。mainWindow = new BrowserWindow({width: 800, // 宽度height: 600, // 高度autoHideMenuBar: true, // 自动隐藏菜单栏(默认是false)alwysOnTop: true, // 窗口置顶(类似z-index:9999,永远置于最高层) (默认是false)x: 0, // 窗口左上角x坐标y: 0, // 窗口左上角y坐标webPreferences:{preload: path.join(__dirname, "./preload.js")}});// 2.1.主进程注册对应的事件ipcMain.on("create-file",createFile)// 3.1.处理 'file-read' IPC 事件,调用 readFile 函数处理,并返回结果ipcMain.handle('file-read', readFile);// 1.2.加载一个远程的页面// mainWindow.loadURL("http://www.baidu.com");// 1.2.加载一个本地的页面mainWindow.loadFile("./pages/index.html");
}
10.3.主进程到=>渲染进程
概述:主进程使用 win.webContents.send
发送消息,渲染进程通过ipcRenderer.on
处理消息
常用于:从主进程主动发送消息给渲染进程,例如下面的这个需求:
需求:应用加载 6秒钟后,主动给渲染进程发送一个消息,内容是:你好啊!
- 页面中添加相关元素,
render.js
中添加对应脚本
render.js
window.onload = () => {myAPI.getMessage(logMessage)
};function logMessage(event,str){console.log(event,str)alert(str)
}
preload.js
中使用ipcRenderer.on ('信道',回调)
接收消息,并配置回调函数。
preload.js
contextBridge.exposeInMainWorld("myAPI", {getMessage:(callback)=>{return ipcRenderer.on("message",callback)}
});
- 主进程中,在合适的时候,使用
mainWindow.webcontents.send('信道',数据)
发送消息。
main.js
// 1.创建浏览器窗口。
function createWindow() {// 1.1.创建浏览器窗口。mainWindow = new BrowserWindow({width: 800, // 宽度height: 600, // 高度autoHideMenuBar: true, // 自动隐藏菜单栏(默认是false)alwysOnTop: true, // 窗口置顶(类似z-index:9999,永远置于最高层) (默认是false)x: 0, // 窗口左上角x坐标y: 0, // 窗口左上角y坐标webPreferences: {preload: path.join(__dirname, "./preload.js"),},});// 2.1.主进程注册对应的事件ipcMain.on("create-file", createFile);// 3.1.处理 'file-read' IPC 事件,调用 readFile 函数处理,并返回结果ipcMain.handle("file-read", readFile);// 1.2.加载一个远程的页面// mainWindow.loadURL("http://www.baidu.com");// 1.2.加载一个本地的页面mainWindow.loadFile("./pages/index.html");// 创建⼀个定时器setTimeout(() => {mainWindow.webContents.send("message", "你好啊!");}, 6000);
}
11.打包Electron应用
使用 electron-builder
打包应用
- 先安装
electron-builder
:
npm install electron-builder -D
- 在
package.json
中进行相关配置,具体配置如下
备注:
package.json
文件不支持注释,使用时请去掉所有注释.
package.json
内容如下:
{"name": "my-electron-app","version": "1.0.0","main": "main.js","scripts": {"start": "nodemon --exec electron .","build": "electron-builder" },"build": {"appId": "com.electron.demo", "win": {"icon":"./logo.ico","target": [{"target": "nsis", "arch": ["x64"] }]},"nsis": {"oneClick": false, "perMachine": true,"allowToChangeInstallationDirectory": true}},"author": "RanGuMo","description": "this is a electron demo","license": "ISC","devDependencies": {"electron": "^32.0.1","electron-builder": "^24.13.3","nodemon": "^3.1.4"}
}
- 执行打包命令
npm run build
打包失败的话,检查一下当前项目所在路径是不是全英文路径。可以看一下这篇文章:http://t.csdnimg.cn/xV1ex
12.electron-vite
electron-vite
是一个新型构建工具,旨在为 Electron
提供更快、更精简的开发体验。它主要由五部分组成:
-
一套构建指令,它使用
Vite
打包你的代码,并且它能够处理Electron
的独特环境,包括Node.js
和浏览器环境。 -
集中配置主进程、渲染器和预加载脚本的
Vite
配置,并针对Electron
的独特环境进行预配置。 -
为渲染器提供快速模块热替换(
HMR
)支持,为主进程和预加载脚本提供热重载支持,极大地提高了开发效率。 -
优化
Electron
主进程资源处理。 -
使用
V8
字节码保护源代码。
electron-vite
快速、简单且功能强大,旨在开箱即用。
13.项目源码
github 地址:https://github.com/RanGuMo/my-electron-app.git