Electron
一、构建第一个electron应用
1.初始化
npm init -y //初始化项目
npm install --save-dev electron //下载electron到开发依赖
2.创建入口index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
and Electron <span id="electron-version"></span>.
</body>
</html>
3.创建入口文件
//main.js
const { app,BrowserWindow } = require('electron')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})
4.创建窗口应用
Package.json配置
//Package.json
{
"name": "electron-app",
"version": "1.0.0",
"description": "一个处于实验阶段的app",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"keywords": [],
"author": "alanmf",
"license": "MIT",
"devDependencies": {
"electron": "^21.3.0"
}
}
npm start //执行 electron .
为了更方便页面更新我们使用nodemon
nodemon是一个自动重启node应用的工具,当监听的文件或监听目录下的文件发生修改时,自动重启应用
npm i nodemon -D
更改package.json
//Package.json
{
"name": "electron-app",
"version": "1.0.0",
"description": "一个处于实验阶段的app",
"main": "main.js",
"scripts": {
"start": "nodemon --exec electron ."
},
"keywords": [],
"author": "alanmf",
"license": "MIT",
"devDependencies": {
"electron": "^21.3.0"
}
}
二、electron核心
1.electron核心概念
主进程;启动项目时运行的main.js脚本就是我们说的主进程,在主进程运行的脚步可以创建web页面的形式展示GUI,主进程只有一个。
渲染进程:每个Electron的页面都在运行着自己的进程,这样的进程称之为渲染进程(基于Chromium的多进程结构)。
主进程使用BrowserWindow创建实例,主进程销毁后,对应的渲染进程会被终止。主进程与渲染进程通过 IPC 的方式(事件驱动)进行通信。
2.electron窗口控制台
//打开开发者工具
win.webContents.openDevTools();
//暂时关闭安全警告(不推荐)
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
//配置CSP(阻挡一部分安全警告)
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
3.主进程事件生命周期
关闭所有窗口时退出应用 Windows & Linux
在Windows和Linux上,关闭所有窗口通常会完全退出一个应用程序。
为了实现这一点,你需要监听 app
模块的 'window-all-closed'事件。如果用户不是在 macOS(
darwin) 上运行程序,则调用
app.quit()`。
app.on('window-all-closed', () => {
//macOS用户在关闭窗口时不会关闭应用
if (process.platform !== 'darwin') app.quit()
})
当 Linux 和 Windows 应用在没有窗口打开时退出了,macOS 应用通常即使在没有打开任何窗口的情况下也继续运行,并且在没有窗口可用的情况下激活应用时会打开新的窗口。
为了实现这一特性,监听 app
模块的 activate
事件。如果没有任何浏览器窗口是打开的,则调用 createWindow()
方法。
因为窗口无法在 ready
事件前创建,你应当在你的应用初始化后仅监听 activate
事件。 通过在您现有的 whenReady()
回调中附上您的事件监听器来完成这个操作。
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
这样,我们的窗口的控件就完整了(实现macOS用户-没有窗口打开时创建新窗口-关闭窗口不关闭应用,点击程序坞应用打开新窗口)
4.通过预加载脚本从渲染器访问Node.js
现在,最后要做的是输出Electron的版本号和它的依赖项到你的web页面上。
在主进程通过Node的全局 process
对象访问这个信息是微不足道的。 然而,你不能直接在主进程中编辑DOM,因为它无法访问渲染器 文档
上下文。 它们存在于完全不同的进程!
这是将 预加载 脚本连接到渲染器时派上用场的地方。 预加载脚本在渲染器进程加载之前加载,并有权访问两个 渲染器全局 (例如 window
和 document
) 和 Node.js 环境。
创建一个名为 preload.js
的新脚本:
//preload.js
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})
上面的代码访问 Node.js process.versions
对象,并运行一个基本的 replaceText
辅助函数将版本号插入到 HTML 文档中。
要将此脚本附加到渲染器流程,请在你现有的 BrowserWindow
构造器中将路径中的预加载脚本传入 webPreferences.preload
选项。
// include the Node.js 'path' module at the top of your file
const path = require('path')
// modify your existing createWindow() function
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
// ...
这里使用了两个Node.js概念:
__dirname
字符串指向当前正在执行脚本的路径 (在本例中,它指向你的项目的根文件夹)。path.join
API 将多个路径联结在一起,创建一个跨平台的路径字符串。
我们使用一个相对当前正在执行JavaScript文件的路径,这样相对路径将在开发模式和打包模式中都将有效。
5.contextBridge
contextBridge
模块有以下方法:
contextBridge.exposeInMainWorld(apiKey, api)
apiKey
string - 将 API 注入到窗口
的键。 API 将可通过window[apiKey]
访问。api
any - 你的 API可以是什么样的以及它是如何工作的相关信息如下。
应用开发接口(API)
提供给 exposeInMainWorld
的 api
必须是一个 Function
, string
, number
, Array
, boolean
;或一个键为字符串,值为一个 Function
, string
, number
, Array
, boolean
的对象;或其他符合相同条件的嵌套对象。
Function
类型的值被代理到其他上下文中,所有其他类型的值都会被 复制 并 冻结。 在 API 中发送的任何数据 /原始数据将不可改变,在桥接器其中一侧的更新不会导致另一侧的更新。
代码应用:
//preload.js+
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myApi',{
platform:process.platform
})
那我们就可以在页面上去访问window上的方法了
//app.js
console.log(window.myApi.platform) //darwin
三、主进程与渲染进程通信
进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。 由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。
在 Electron 中,进程使用 ipcMain
和 ipcRenderer
模块,通过开发人员定义的“通道”传递消息来进行通信。 这些通道是 任意 (您可以随意命名它们)和 双向 (您可以在两个模块中使用相同的通道名称)的。
1.渲染器进程到主进程(单向 )
要将单向 IPC 消息从渲染器进程发送到主进程,您可以使用 ipcRenderer.send
API 发送消息,然后使用 ipcMain.on
API 接收。
我们下边就按照官网的例子来说吧!
(1) 使用 ipcMain.on
监听事件
在主进程(main.js)中,使用 ipcMain.on
API 在 set-title
通道上设置一个 IPC 监听器:
//main.js+
const { ipcMain } = require('electron')
ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})
(2) 通过预加载脚本暴露 ipcRenderer.send
要将消息发送到上面创建的 IPC 监听器,可以使用 ipcRenderer.send
API。 默认情况下,渲染器进程没有权限访问 Node.js 和 Electron 模块。 作为应用开发者,需要使用 contextBridge
API 来选择要从预加载脚本中暴露哪些 API。
在您的预加载脚本中添加以下代码,向渲染器进程暴露一个全局的 window.electronAPI
变量。
//preload.js+
const { contextBridge,ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
此时,将能够在渲染器进程中使用 window.electronAPI.setTitle()
函数。
(3) 构建渲染器进程 UI
在 BrowserWindow 加载的我们的 HTML 文件中,添加一个由文本输入框和按钮组成的基本用户界面:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
Title: <input id="title"/>
<button id="btn" type="button">Set</button>
<script src="./renderer/app.js"></script>
</body>
</html>
将在导入的 renderer.js
文件中添加几行代码,以利用从预加载脚本中暴露的 window.electronAPI
功能:
//app.js
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
});
到目前的代码为止,我们的一个渲染进程到主进程的进程通信就已经实现了,我们可以试一试我们的UI界面功能
总结:
!!!我们上面有一些地方不理解的方法和API,我们现在讲一下
上面的 回调函数有两个参数:一个 IpcMainEvent 结构和一个 title
字符串。 每当消息通过 set-title
通道传入时,此函数找到附加到消息发送方的 BrowserWindow 实例,并在该实例上使用 win.setTitle
API。
我们只是为了更简单的理解进程通信,具体 API 见官方文档吧!
2.渲染器进程到主进程(双向)
双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 这可以通过将 ipcRenderer.invoke
与 ipcMain.handle
搭配使用来完成。
(1)使用 ipcMain.handle
监听事件
在主进程(main.js)中,我们将创建一个 handleFileOpen()
函数,它调用 dialog.showOpenDialog
并返回用户选择的文件路径值。 每当渲染器进程通过 dialog:openFile
通道发送 ipcRender.invoke
消息时,此函数被用作一个回调。 然后,返回值将作为一个 Promise 返回到最初的 invoke
调用。
//main.js
const { app,BrowserWindow,ipcMain,dialog } = require('electron')
const path = require('path')
//回调函数
async function handleFileOpen() {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (canceled) {
return
} else {
return filePaths[0]
}
}
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
win.webContents.openDevTools()
}
app.whenReady().then(() => {
//监听,执行回调
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
(2)通过预加载脚本暴露 ipcRenderer.invoke
在预加载脚本中,我们暴露了一个单行的 openFile
函数,它调用并返回 ipcRenderer.invoke('dialog:openFile')
的值。 我们将在下一步中使用此 API 从渲染器的用户界面调用原生对话框。
//preload.js
const { contextBridge,ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
(3)构建渲染器进程 UI
最后,让我们构建加载到 BrowserWindow 中的 HTML 文件。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Dialog</title>
</head>
<body>
<button type="button" id="btn">Open a File</button>
File path: <strong id="filePath"></strong>
<script src='./renderer/app.js'></script>
</body>
</html>
用户界面包含一个 #btn
按钮元素,将用于触发我们的预加载 API,以及一个 #filePath
元素,将用于显示所选文件的路径。需要在渲染器进程脚本中编写几行代码
//app.js
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
到目前的代码为止,我们的一个渲染进程到主进程的双向进程通信就已经实现了,我们可以试一试我们的UI界面功能
总结:
ipcRenderer.invoke
API (老方法了,但依然好用)是在 Electron 7 中添加的,作为处理渲染器进程中双向 IPC 的一种开发人员友好的方式。 但这种 IPC 模式存在几种替代方法。
使用 ipcRenderer.send
我们用于单向通信的 ipcRenderer.send
API 也可用于双向通信。 这是在 Electron 7 之前通过 IPC 进行异步双向通信的推荐方式。
//preload.js
// 您也可以使用 `contextBridge` API
// 将这段代码暴露给渲染器进程
const { ipcRenderer } = require('electron')
ipcRenderer.on('asynchronous-reply', (_event, arg) => {
console.log(arg) // 在 DevTools 控制台中打印“pong”
})
ipcRenderer.send('asynchronous-message', 'ping')
//main.js
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // 在 Node 控制台中打印“ping”
// 作用如同 `send`,但返回一个消息
// 到发送原始消息的渲染器
event.reply('asynchronous-reply', 'pong')
})
这种方法有几个缺点:
- 您需要设置第二个
ipcRenderer.on
监听器来处理渲染器进程中的响应。 使用invoke
,您将获得作为 Promise 返回到原始 API 调用的响应值。 - 没有显而易见的方法可以将
asynchronous-reply
消息与原始的asynchronous-message
消息配对。 如果您通过这些通道非常频繁地来回传递消息,则需要添加其他应用代码来单独跟踪每个调用和响应。
使用 ipcRenderer.sendSync
API也是可以替代的,但不推荐
ipcRenderer.sendSync
API 向主进程发送消息,并 同步 等待响应。
//main.js
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // 在 Node 控制台中打印“ping”
event.returnValue = 'pong'
})
//perload.js
// 您也可以使用 `contextBridge` API
// 将这段代码暴露给渲染器进程
const { ipcRenderer } = require('electron')
const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result) // 在 DevTools 控制台中打印“pong”
这份代码的结构与 invoke
模型非常相似,但出于性能原因,我们建议避免使用此 API。 它的同步特性意味着它将阻塞渲染器进程,直到收到回复为止。(容易阻塞渲染器进程)
3.主进程到渲染器进程
将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。 消息需要通过其 WebContents
实例发送到渲染器进程。 此 WebContents 实例包含一个 send
方法,其使用方式与 ipcRenderer.send
相同。
(1)使用 webContents
模块发送消息
我们需要首先使用 Electron 的 Menu
模块在主进程中构建一个自定义菜单,该模块使用 webContents.send
API 将 IPC 消息从主进程发送到目标渲染器。
//main.js
const { app,BrowserWindow,ipcMain,Menu } = require('electron')
const path = require('path')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{
click: () => win.webContents.send('update-counter', 1),
label: 'Increment',
},
{
click: () => win.webContents.send('update-counter', -1),
label: 'Decrement',
}
]
}
])
Menu.setApplicationMenu(menu)
win.loadFile('index.html')
win.webContents.openDevTools()
}
app.whenReady().then(() => {
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
(2)通过预加载脚本暴露 ipcRenderer.on
与前面的渲染器到主进程的示例一样,我们使用预加载脚本中的 contextBridge
和 ipcRenderer
模块向渲染器进程暴露 IPC 功能:
//preload.js
const { contextBridge,ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
加载预加载脚本后,渲染器进程应有权访问 window.electronAPI.handleCounter()
监听器函数。
(3)构建渲染器进程 UI
我们将在加载的 HTML 文件中创建一个接口,其中包含一个 #counter
元素,我们将使用该元素来显示值:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Menu Counter</title>
</head>
<body>
Current value: <strong id="counter">0</strong>
<script src='./renderer/app.js'></script>
</body>
</html>
为了更新 HTML 文档中的值,我们将添加几行 DOM 操作的代码,以便在每次触发 update-counter
事件时更新 #counter
元素的值。
//app.js
const counter = document.getElementById('counter')
window.electronAPI.handleCounter((event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
event.sender.send('counter-value', newValue)
})
在上面的代码中,我们将回调传递给从预加载脚本中暴露的 window.electronAPI.onUpdateCounter
函数。 第二个 value
参数对应于我们传入 webContents.send
函数的 1
或 -1
,该函数是从原生菜单调用的。
到目前的代码为止,我们的一个主进程到渲染器进程的进程通信就已经实现了,我们可以试一试我们的UI界面功能
总结:
对于从主进程到渲染器进程的 IPC,没有与 ipcRenderer.invoke
等效的 API。 不过,可以从 ipcRenderer.on
回调中将回复发送回主进程。
我们可以对前面例子的代码进行略微修改来演示这一点。 在渲染器进程中,使用 event
参数,通过 counter-value
通道将回复发送回主进程。
//app.js
const counter = document.getElementById('counter')
window.electronAPI.handleCounter((event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
event.sender.send('counter-value', newValue)
})
在主进程中,监听 counter-value
事件并适当地处理它们。
//main.js
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
4.渲染器进程到渲染器进程
没有直接的方法可以使用 ipcMain
和 ipcRenderer
模块在 Electron 中的渲染器进程之间发送消息。 为此,有两种选择:
- 将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
- 从主进程将一个 MessagePort 传递到两个渲染器。 这将允许在初始设置后渲染器之间直接进行通信。
参考链接:
[Electron官网]: “https://www.electronjs.org/zh/docs/latest/tutorial/quick-start “