什么是electron

简单来说,electron是使用html,css,js,nodejs,Native Api构建的跨平台的软件开发框架

简介 | Electron (electronjs.org)

要点:

  • 应用广泛的跨平台的桌面应用开发框架
  • electron的本质是结合了Chromium与Nodejs
  • 使用HTML,CSS,JS等Web技术构建桌面应用程序

Chromium可以简单理解为一个简洁版的浏览器

electron的技术架构

electron可以简单理解为如下组合

image-20240827190853419

具体架构如下图所示:

image-20240827185249210
  • 主进程是整个程序的入口,渲染进程由主进程进行管理。
  • 主进程和渲染进程原则上是相互隔离的,只能通过IPC的方式进行通信。
  • Native API是一套通用的操作系统API,这是实现跨平台重要的原因。
  • 主进程使用的是Nodejs环境,渲染进程使用的是Chromium(Web环境),需要注意区分。

程序运行在主进程上,每当打开一个页面就会产生一个渲染进程,渲染进程需要由主进程管理。在主进程中,可以使用Nodejs的所有语法,在渲染进程中,可以使用JS语法,进程与进程之间相互隔离。进程与进程之间可以使用IPC的方式进行通信,主进程与各渲染进程只需直接通过IPC的方式,对于渲染进程和渲染进程之间,需要通过主进程间接实现通信。

项目搭建

快速上手

确保安装了Nodejs

1
2
node -v
npm -v

初始化Node项目

1
npm init -y

package.json中,authordescription对于打包来说是必填项

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "demo",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "gcnanmu",
"license": "ISC",
"description": "a electron demo app"
}

安装electron

1
2
3
npm install --save-dev electron
或者
npm install electron -D

package.json,添加mainstart字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "demo",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "gcnanmu",
"license": "ISC",
"description": "a electron demo app",
"devDependencies": {
"electron": "^32.0.1"
}
}
  • main字段指明了主进程所在js的位置
  • start设置了运行程序的命令,也可以直接使用electron .命令启动程序

在根目录创建main.js,创建app并渲染一个窗口。

1
2
3
4
5
6
7
8
9
10
const { app, BrowserWindow } = require("electron");

// 监听程序 准备好了就创建一个窗口
app.on("ready",()=>{
new BrowserWindow({
// 设置窗口大小
width: 800,
height: 600,
})
})
  • app表示程序对象
  • BrowserWindow表示渲染窗口,可传入多个配置项

在终端运行npm start,未出现报错且跳出一个窗口则运行成功。

image-20240827194028571

从URL加载页面

修改main.js,为渲染窗口指定b站的URL进行加载

1
2
3
4
5
6
7
8
9
10
11
const { app, BrowserWindow } = require("electron");

app.on("ready",()=>{
const win = new BrowserWindow({
width: 800,
height: 600,
// 去除默认菜单栏
autoHideMenuBar: true,
})
win.loadURL("https://www.bilibili.com")
})
image-20240827194330052

打开开发者模式后,在终端可以看到如下的警告信息,这个警告不会影响程序的正常运行,不用理会。

1
2
3
[28648:0827*/*201847.314:ERROR:CONSOLE(1)] "Request Autofill.enable failed. {"code":-32601,"message":"'Autofill.enable' wasn't found"}", source: devtools://devtools/bundled/core/protocol_client/protocol_client.js (1*)*

[28648:0827*/*201847.314:ERROR:CONSOLE(1)] "Request Autofill.setAddresses failed. {"code":-32601,"message":"'Autofill.setAddresses' wasn't found"}", source: devtools://devtools/bundled/core/protocol_client/protocol_client.js (1)

github相关的issue: 议题 #41614 · electron/electron (github.com)

加载本地页面

在根目录创建一个名为render文件夹来存放渲染的页面

1
2
3
4
5
6
7
8
9
.
├─ node_modules
├─ package-lock.json
├─ package.json
├─ main.js
└─ render
└─ index
├─ index.html
└─ index.js

index.html写入我们想要展示的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Electron_Demo</title>
</head>
<body>
<h1>electron学习</h1>
<button id="btn1">点我</button>
<script src="./index.js"></script>
</body>
</html>

index.js中实现相应的操作

1
2
3
4
5
const btn1 = document.getElementById("btn1");

btn1.onclick = () => {
alert("Hello World");
};

main.js中加载index.html

1
2
3
4
5
6
7
8
9
10
11
12
const { app, BrowserWindow } = require("electron");

app.on("ready", () => {
const win = new BrowserWindow({
width: 800,
height: 600,
// 去除默认菜单栏
autoHideMenuBar: true,
});
// win.loadURL("https://www.bilibili.com")
win.loadFile("./render/index/index.html");
});
image-20240827204214669

在启动的electron应用中,可以使用Ctrl + Shift + I打开开发者工具,在Console中会出现如下的安全警告信息

VM4 sandbox_bundle:2 Electron Security Warning (Insecure Content-Security-Policy) This renderer process has either no Content Security
Policy set or a policy with “unsafe-eval” enabled. This exposes users of
this app to unnecessary security risks.

VM4 sandbox_bundle:2 Electron 安全警告(不安全的内容安全策略)该渲染器进程要么没有设置内容安全策略,要么启用了 “不安全-评估 “策略。
策略或启用了 “不安全-评估 “策略。这使此应用程序的用户面临
此应用程序的用户面临不必要的安全风险。

For more information and help, consult
https://electronjs.org/docs/tutorial/security.
This warning will not show up
once the app is packaged.

想要传达的意思是该渲染器进程没有设置内容安全策略Insecure Content-Security-Policy的相关信息可以在内容安全策略(CSP) - HTTP | MDN (mozilla.org)中查到。

按照MDN网站的提示,需要我们在渲染页面index.html中的meta标签中加入如下的信息

1
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';" />
1
2
3
4
5
6
7
8
9
10
11
12
13
<!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'; img-src https://*; child-src 'none';" />
<title>Electron_Demo</title>
</head>
<body>
<h1>electron学习</h1>
<script src="./index.js"></script>
</body>
</html>

添加后警告消失。

完善窗口行为

对于Windows系统,如果当前应用的所有窗口被关闭,那么视为程序运行结束,进程被关闭。但对于MacOS系统来说,只要Docker栏中的应用图标存在,程序就不会被关闭,而是进入一个特殊的休眠状态,再次点击docker栏中的图标,则会激活程序,创建窗口。

基于上述系统之间的进程管理策略的差异性,在electron中使用如下代码来适配相关的策略

1
2
3
4
5
6
7
8
9
//MacOS策略兼容代码
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})

//Win策略兼容代码
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

代码解释:

  • activate- 当程序处于被激活状态时,如果当前渲染窗口的个数为0 则自动创建窗口
  • window-all-closed - 当所有窗口被关闭且当前平台为windows时,程序结束执行

修改后的main.js代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const { app, BrowserWindow } = require("electron");

function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
// 隐藏窗口边框
autoHideMenuBar: true,
});
win.loadFile("./render/index/index.html");
}

app.on("ready", () => {

createWindow();

//MacOS策略兼容代码
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});

//Win策略兼容代码
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});

运行得到的结果如下

image-20240827205937257

自动重启

如果要使主进程修改后自动执行,可使用nodemon

安装

1
npm i nodemon -g

修改package.json中的start命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "demo",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "nodemon --exec electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "gcnanmu",
"license": "ISC",
"description": "a electron demo app",
"devDependencies": {
"electron": "^32.0.1"
}
}

对于渲染进程,每次修改后可使用Ctrl + R刷新页面内容,如果要让nodemon将渲染页面也纳入检测范围,需要在根目录创建nodemon.json文件来设置相关配置。

1
2
3
4
5
6
7
8
9
{
"ignore":[
"node modules",
"dist"
],
"restartable":"r",
"watch":["*.*"],
"ext":"html,js,css"
}

配置解释

  • ignore - 要忽略的文件
  • restartable - 重启的命令 在命令行输入r即可重启程序
  • watch - 监测的文件范围,这里表示处理ignore的所有文件
  • ext - 要监测的文件后缀名

Preload脚本

在前文的技术架构中提到,主进程与渲染进程是相互隔离的,且主进程为Nodejs环境,而渲染进程为Web环境。因此无法在渲染进程中使用__dirname,不能在主进程中使用window对象。这样的好处是保证了进程的运行安全,但对于复杂一些的操作就很难实现。

为了实现IPC通信,主进程与渲染进程之间需要一个桥梁,这个桥梁便是预加载脚本。

预加载脚本很特殊,它能够访问部分Nodejs的api,本质上属于削弱后的Nodejs环境。

image-20240827211856190

现在有一个这样的需求,我需要把Nodejs版本、electron版本显示到当前的渲染页面index.html上。

相应的版本信息只有Nodejs环境中才可以获取,预加载脚本属于Nodejs环境,可以通过预加载脚本获取版本信息,但是展示信息要在渲染进程完成。因此需要主进程将版本信息传递给渲染进程。那么必然涉及到IPC通信

在根目录创建preload.js

1
2
3
4
5
6
7
8
const {contextBridge, ipcRenderer} = require('electron')
contextBridge.exposeInMainWorld('api', {
version: {
Electron: process.versions.electron,
Chrome: process.versions.chrome,
Node: process.versions.node
}
})

contextBridge直译为上下文桥梁,exposeInMainWorld的作用是将其中的信息暴露给渲染进程。暴露后,api对象会出现在window对象中

image-20240827214143149

main.js中需指明预加载脚本的位置(绝对路径),这样之后就建立起了IPC通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const { app, BrowserWindow } = require("electron");
const path = require("path");

function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
// 隐藏窗口边框
autoHideMenuBar: true,
webPreferences: {
// 要传入绝对路径
preload: path.resolve(__dirname, "./preload.js"),
},
});

win.loadFile("./render/index/index.html");
}

// 其他代码省略

index.html中创建一个新的按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!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'; img-src https://*; child-src 'none';" />
<title>Electron_Demo</title>
</head>
<body>
<h1>electron学习</h1>
<button id="btn1">点我</button>
<br><br>
<hr>
<button id="btn2">点我获取版本信息</button>
<script src="./index.js"></script>
</body>
</html>

为新按钮绑定事件,同时index.js中使用api对象

1
2
3
4
5
6
7
8
9
10
const btn1 = document.getElementById("btn1");
const btn2 = document.getElementById("btn2");

btn1.onclick = () => {
alert("Hello World");
};
// 点击按钮获取相应的版本信息
btn2.onclick = () => {
console.log(api.version);
}

通过preload.js预加载脚本,就能够在实现在渲染进程中展示版本信息

image-20240827214143149

需要注意加载的顺序:主进程 -> 预加载脚本 -> 渲染进程

进程通信(IPC)

通过一个案例来更好展示IPC之间的通信原理。

在根目录新建一个data.txt,当写入按钮点击时,将input标签中的内容写入text.txt。当点击读取按钮时,会将data.txt中的内容显示在下面的span标签中。初始页面构成如下图所示

image-20240827215326827

index.html如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!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'; img-src https://*; child-src 'none';" />
<title>Demo</title>
</head>
<body>
<h1>IPC通信</h1>
<input type="text">
<br><br>
<button id="btn1">写入</button>

<hr>
<button id="btn2">读取</button>
<br><br>
<span id="showData">当前未读取</span>
<script src="./index.js"></script>
</body>
</html>

渲染进程 → 主进程(单向)

为了实现写入按钮的功能,需要先获取input标签中的内容,将内容传递给预加载脚本preload.jspreload.js再将数据暴露给主进程,主进程获取到数据后,再调用Nodejs的fs模块将内容写入data.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", {
version: {
Electron: process.versions.electron,
Chrome: process.versions.chrome,
Node: process.versions.node,
},

// 使用ipcRenderer的send方法,将信息暴露给主进程
writeFile: (data) => {
ipcRenderer.send("write-file", data);
},
});

1
2
3
4
5
6
7
8
9
// index.js
const btn1 = document.getElementById("btn1");

btn1.onclick = () => {
// 获取主进程
const data = document.querySelector("input").value;
// 将数据传送到调用预加载脚本
api.writeFile(data);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// main.js
const { app, BrowserWindow, ipcMain } = require("electron");
const path = require("path");
const fs = require("fs");

// 将内容写入data.txt
function writeFile(data) {
fs.writeFileSync("./data.txt", data);
}

// 其他代码省略

app.on("ready", () => {
createWindow();

//MacOS策略兼容代码
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

ipcMain.on("write-file", (event, data) => {
writeFile(data);
});
});

整个执行的流程如图所示

image-20240827221747457

渲染进程 ↔ 主进程(双向)

接着完善读取按钮的实现逻辑,我们不仅要读取文件的内容,还需要读出的内容发送给渲染进程。因此属于双向的IPC通信,需要按钮发出命令,主进程读取了文件的具体内容后,将内容返回给渲染进程。使用的不再是sendon,而是invokehandle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// preload.js
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("myApi", {
version: {
Electron: process.versions.electron,
Chrome: process.versions.chrome,
Node: process.versions.node,
},

writeFile: (data) => {
ipcRenderer.send("write-file", data);
},
readFile: () => {
// 返回的是一个promise对象
return ipcRenderer.invoke("read-file");
},
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// index.js
const btn1 = document.getElementById("btn1");
const btn2 = document.getElementById("btn2");

btn1.onclick = () => {
const data = document.querySelector("input").value;
myApi.writeFile(data);
};

btn2.onclick = async () => {
const span = document.getElementById("showData");
// 等待具体内容
const data = await myApi.readFile();
// 将内容展示到span标签中
span.innerHTML = data;
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const { app, BrowserWindow, ipcMain } = require("electron");
const path = require("path");
const fs = require("fs");

app.on("ready", () => {
createWindow();

//MacOS策略兼容代码
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

ipcMain.on("write-file", (event, data) => {
writeFile(data);
});

ipcMain.handle("read-file", (event) => {
// 读取文件内容后返回
const data = fs.readFileSync("./data.txt", "utf-8").toString();
return data;
});
});

得到的效果如下,读取得到的内容和data.txt中的内容一致。

IPC通信

主进程 → 渲染进程(单向)

这次从主进程给渲染进程发消息。我们实现加载窗体过4s后发送一个message显示到渲染进程中。需要在主进程中使用send,预加载脚本中使用on。但是ipcMain中无send方法,需要使用win.webContents来实现,其余并无二致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// main.js
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
icon: "./assert/Comm Discord Dark.ico",
// 隐藏窗口边框
autoHideMenuBar: true,
webPreferences: {
// 要传入绝对路径
preload: path.resolve(__dirname, "./preload.js"),
},
});

win.loadFile("./render/index/index.html");

setTimeout(() => {
win.webContents.send("message", "Hello from main process");
}, 4000);
}

app.on("ready", () => {
createWindow();

// 其他省略
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// preload.js
const { contextBridge, ipcRenderer } = require("electron");
console.log("preload.js");

contextBridge.exposeInMainWorld("api", {
version: {
Electron: process.versions.electron,
Chrome: process.versions.chrome,
Node: process.versions.node,
},

writeFile: (data) => {
ipcRenderer.send("write-file", data);
},
readFile: () => {
return ipcRenderer.invoke("read-file");
},

getMessage: (callback) => {
return ipcRenderer.on("message", callback);
},
});
1
2
3
4
5
6
7
8
// index.js
window.onload = () => {
api.getMessage(logMessage)
}

function logMessage(event, data) {
alert(data);
}

实现效果如下

主进程向渲染进程

事实上,使用渲染进程和主进程双向通信的方法来替换主进程向渲染进程的单向通信也是可行的。

打包应用

使用electron-builder进行打包

安装

1
npm install electron-builder -D

package.json文件中设置相应的打包配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"name": "demo",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "nodemon --exec electron .",
"build": "electron-builder"
},
"build": {
"appId": "demo.electron",
"win": {
"icon": "./Comm Discord Dark.ico",
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
]
},
"nsis": {
"oneClick": false,
"perMachine": true,
"allowToChangeInstallationDirectory": true
}
},
"devDependencies": {
"electron": "^30.0.0",
"electron-builder": "^24.13.3"
},
"author": "gcnanmu",
"license": "ISC",
"description": "a electron demo app"
}

相关配置项说明:

  • name - 应用程序的名称
  • version - 应用程序的版本
  • appId - 应用程序的唯一标识符
  • icon - 应用图标(只影响打包程序的图标)
  • targe - NSIS 指定使用NSIS作为安装程序的格式
  • arch - 指定为64位架构
  • nsis
    • oneClick - 设置false为使安装程序显示安装向导,而不是一键安装
    • perMachine - 设置为true表示只为当前用户安装
    • allowToChangeInstallationDirectory - 设置为true表示在安装的过程中允许选择安装目录
  • author - 作者信息 会显示在程序右键的详细信息标签栏中

终端运行npm run build即开始打包,打包过程需要保证网络通畅(能访问github)

1
2
3
4
5
6
7
8
9
> demo@1.0.0 build
> electron-builder

• electron-builder version=24.13.3 os=10.0.19045
• loaded configuration file=package.json ("build" field)
• writing effective config file=dist\builder-effective-config.yaml
• packaging platform=win32 arch=x64 electron=32.0.1 appOutDir=dist\win-unpacked
• building target=nsis file=dist\demo Setup 1.0.0.exe archs=x64 oneClick=false perMachine=true
• building block map blockMapFile=dist\demo Setup 1.0.0.exe.blockmap

打包后得到dist文件夹

1
2
3
4
5
6
dist
├─ builder-debug.yml
├─ builder-effective-config.yaml
├─ demo Setup 1.0.0.exe
├─ demo Setup 1.0.0.exe.blockmap
└─ win-unpacked

其中demo Setup 1.0.0.exe即为安装程序,在win-unpacked中还存在一个非安装包形式的可执行程序。

electron-vite

创建脚手架

在了解了electron的基本原理后,按照原理分析,只要最终能够转换为html,css,js文件,那么理论上就可以使用electron。因此前端框架也可以使用electron进行打包。为了方便开发,有开发者将electronvite进行了结合,得到了更加利于electron开发的vite脚手架。

  1. alex8088/electron-vite: Next generation Electron build tooling based on Vite 新一代 Electron 开发构建工具,支持源代码保护 (github.com)

  2. electron-vite | 下一代 Electron 开发构建工具

创建一个electron-vite项目

1
npm create @quick-start/electron@latest

跟随指引选择相应的选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
D:\Web\electron>npm create @quick-start/electron@latest

> npx
> create-electron

√ Project name: ... electron-app
√ Select a framework: » vue
√ Add TypeScript? ... No / Yes
√ Add Electron updater plugin? ... No / Yes
√ Enable Electron download mirror proxy? ... No / Yes

Scaffolding project in D:\Web\electron\electron-app...

Done. Now run:

cd electron-app
npm install
npm run dev

D:\Web\electron>cd electron-app

D:\Web\electron\electron-app>npm install
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated @humanwhocodes/config-array@0.11.14: Use @eslint/config-array instead
npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead

> electron-app@1.0.0 postinstall
> electron-builder install-app-deps

• electron-builder version=24.13.3
• loaded configuration file=D:\Web\electron\electron-app\electron-builder.yml

added 473 packages in 31s

80 packages are looking for funding
run `npm fund` for details

这样就创建成功了。根目录的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
electron-app
├─ .editorconfig
├─ .eslintignore
├─ .eslintrc.cjs
├─ .gitignore
├─ .npmrc
├─ .prettierignore
├─ .prettierrc.yaml
├─ .vscode
├─ README.md
├─ build
├─ electron-builder.yml
├─ electron.vite.config.mjs
├─ node_modules
├─ package-lock.json
├─ package.json
├─ resources
└─ src

原理探究

日常开发来说,我们只需关注根目录下src文件夹的内容

1
2
3
4
5
6
7
8
9
10
11
12
src
├─ main
│ └─ index.js
├─ preload
│ └─ index.js
└─ renderer
├─ index.html
└─ src
├─ App.vue
├─ assets
├─ components
└─ main.js

从文件夹的命名就能看出端倪,这和前文中提到的electron的基本结构完全一致。

  • main - 对应主进程文件
  • preload - 对应预加载脚本文件
  • renderer - 对应模板文件

观察render下的src目录,可以发现为常规的以vue为框架的vite模板结构。即把原本渲染进程的html文件换为使用vue构建,这样可以无缝衔接原本的vite-vue开发习惯,而不需要其他的额外配置。

在终端输入npm run dev 运行项目,出现下图窗口则运行成功。

image-20240828131616607

点击send IPC按钮,可以看到控制台输出pong,看看是否和上文electron的实现逻辑相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- App.vue -->
<script setup>
import Versions from './components/Versions.vue'

const ipcHandle = () => window.electron.ipcRenderer.send('ping')
</script>

<template>
<img alt="logo" class="logo" src="./assets/electron.svg" />
<div class="creator">Powered by electron-vite</div>
<div class="text">
Build an Electron app with
<span class="vue">Vue</span>
</div>
<p class="tip">Please try pressing <code>F12</code> to open the devTool</p>
<div class="actions">
<div class="action">
<a href="https://electron-vite.org/" target="_blank" rel="noreferrer">Documentation</a>
</div>
<div class="action">
<a target="_blank" rel="noreferrer" @click="ipcHandle">Send IPC</a>
</div>
</div>
<Versions />
</template>

App.vue中可以看到,Send IPC绑定了事件ipcHandle,ipcHandle直接调用了window.electron.ipcRenderer.send('ping'),按原则说,渲染进程是不能直接使用window.electron的,这说明在预加载脚本中有导出electron。定位到./src/perload/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'

// Custom APIs for renderer
const api = {}

// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
window.electron = electronAPI
window.api = api
}

可以看到,预加载脚本通过contextBridge.exposeInMainWorld导出了electronAPI,访问@electron-toolkit/preload可以发现,其实该文件也只是使用了IpcRendererEvent,这在代码的第一行就已经体现了。

1
import { IpcRendererEvent } from 'electron';

到这就已经解释了为什么渲染进程能过访问window.electron,那么在主进程中是否有相应的on处理逻辑呢?答案是肯定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')

// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})

// IPC test
ipcMain.on('ping', () => console.log('pong'))

createWindow()

app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

在上述代码中可以看到 ipcMain.on('ping', () => console.log('pong'))。到此就已经证实了,即便使用脚手架,整个过程还是符合electron的基本原理的。

在页面中看到的版本号来自组件Versions.vue,对应的版本信息也是来自window.electron

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { reactive } from 'vue'

const versions = reactive({ ...window.electron.process.versions })
</script>

<template>
<ul class="versions">
<li class="electron-version">Electron v{{ versions.electron }}</li>
<li class="chrome-version">Chromium v{{ versions.chrome }}</li>
<li class="node-version">Node v{{ versions.node }}</li>
</ul>
</template>

了解了基本的开发需要用到的文件,可以继续探究package.json文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"name": "electron-app",
"version": "1.0.0",
"description": "An Electron application with Vue",
"main": "./out/main/index.js",
"author": "example.com",
"homepage": "https://electron-vite.org",
"scripts": {
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2",
"@rushstack/eslint-patch": "^1.10.3",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"electron": "^31.0.2",
"electron-builder": "^24.13.3",
"electron-vite": "^2.3.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.26.0",
"prettier": "^3.3.2",
"vite": "^5.3.1",
"vue": "^3.4.30"
}
}

可以看到,为了打包不出错,authordescription都赋予了一个默认值。而且为了方便打包,打包的build命令也准备齐全。

打包

上文提到脚手架提供了一系列的默认打包命令

  • build - 默认打包,会生成一个非安装包的可执行程序和安装程序
  • build:unpack - 只会生成一个非安装包的可执行程序
  • build:win,build:mac,build:linux - 直接指定安装包所属的操作系统,生成对应平台的安装程序

打包的相关设置在根目录的electron-builder.yml文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
appId: com.electron.app
productName: electron-app
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
asarUnpack:
- resources/**
win:
executableName: electron-app
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/

和上文提到的直接package.json中配置的选项大差不差,按需设置即可。根据需要运行npm run build或者其他的命令进行打包。