Electron学习笔记
- electron官方文档入口:https://www.electronjs.org/zh/docs/latest/
- 官方文档写得不错,笔记记一点重要的东西就好了
基本操作与认识
生命周期
- ready:app初始化完成
- dom-ready:一个窗口中的文本加载完成
- did-finish-load:导航完成时触发
- window-all-closed:所有窗口都被关闭时触发
- before-quit:在窗口关闭前触发
- will-quit:在窗口关闭且应用退出时触发
- quit:当所有窗口都被关闭时触发
创建并初始化项目
mkdir my-electron-app // 创建项目
cd my-electron-app // 转移到项目文件夹
yarn init // 入口点应当是main.js
yarn add electron --dev // 安装electron
窗口尺寸
ES语法
Electron 目前对 ECMAScript 语法 (如使用 import 来导入模块) 的支持还不完善
不同平台
可以检查 Node.js 的 process.platform 变量,帮助我们在不同操作系统上运行特定代码。 请注意,Electron 目前只支持三个平台:win32 (Windows), linux (Linux) 和 darwin (macOS)
进程通信
Electron 的主进程和渲染进程有着清楚的分工并且不可互换。 这代表着无论是从渲染进程直接访问 Node.js 接口,亦或者是从主进程访问 HTML 文档对象模型 (DOM),都是不可能的。
增加应用的复杂程度
- 增加渲染进程的网页应用代码复杂度
- 深化与操作系统和 Node.js 的集成
- Electron提供了丰富的工具集,可以让你和桌面环境整合起来。从建立托盘图标到添加全局的快捷方式,再到显示原生的菜单,都不在话下
- Electron 还赋予你在主进程中访问 Node.js 环境的所有能力
这组能力使得 Electron 应用能够从浏览器运行网站中脱胎换骨
打包
- 安装electron forge
yarn add --dev @electron-forge/cli
npx electron-forge import
- 打包程序
yarn run make
- 这个曾经失败过的打包过程,是这样解决的:打包的时候项目是不能运行的,需要先把项目关掉,打包才能正常进行下去。
Electron中的流程
流程模型
作为应用开发者,你将控制两种类型的进程:主进程 和 渲染器进程
主进程
- 主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口
- 能通过 Electron 的 app 模块来控制您应用程序的生命周期
- 主进程添加了自定义的 API 来与用户的作业系统进行交互, Electron 有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标
渲染器进程
- 每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程
- 以一个 HTML 文件作为渲染器进程的入口点
- 渲染器无权直接访问 require 或其他 Node.js API。 为了在渲染器中直接包含 NPM 模块,您必须使用与在 web 开发时相同的打包工具 (例如 webpack 或 parcel)
- 没有直接导入 Electron 內容脚本的方法
Preload脚本
- 预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因**能访问 Node.js API **而拥有了更多的权限。
- 预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程
- 通过在全局 window 中暴露任意 API 来增强渲染器,以便网页内容使用,但是因为语境隔离(Context Isolation)的原因,我们只能通过contextBridge模块与ipcRenderer模块来进行安全地交互
上下文隔离
- 上下文隔离功能将确保您的 预加载脚本 和 Electron的内部逻辑 运行在所加载的 webcontent网页 之外的另一个独立的上下文环境里
- 这对安全性很重要,因为它有助于阻止网站访问 Electron 的内部组件 和 您的预加载脚本可访问的高等级权限的API
安全事项
- 暴露进程间通信相关 API 的正确方法是为每一种通信消息提供一种实现方法
// ✅ 正确使用
contextBridge.exposeInMainWorld('myAPI', {
loadPreferences: () => ipcRenderer.invoke('load-prefs')
})
// ❌ 错误使用
// 它直接暴露了一个没有任何参数过滤的高等级权限 API
// 这将允许任何网站发送任意的 IPC 消息
contextBridge.exposeInMainWorld('myAPI', {
send: ipcRenderer.send
})
进程间通信
在 Electron 中,进程使用 ipcMain 和 ipcRenderer 模块,通过开发人员定义的“通道”传递消息来进行通信
渲染器进程到主进程(单向)
要将单向 IPC 消息从渲染器进程发送到主进程,您可以使用 ipcRenderer.send API 发送消息,然后使用 ipcMain.on API 接收
渲染器进程到主进程(双向)
双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 这可以通过将 ipcRenderer.invoke 与 ipcMain.handle 搭配使用来完成
主进程到渲染器进程
将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。 消息需要通过其 WebContents 实例发送到渲染器进程。 此 WebContents 实例包含一个 send 方法,其使用方式与 ipcRenderer.send 相同
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello from Electron renderer!</title>
</head>
<body>
<p id="info"></p>
Title: <input id="title"/>
<button id="setButton" type="button">Set</button>
<button type="button" id="btn">Open a File</button>
File path: <strong id="filePath"></strong>
Current value: <strong id="counter">0</strong>
</body>
<script src="./renderer.js"></script>
</html>
main.js
// Electron 目前对 ECMAScript 语法 (如使用 import 来导入模块) 的支持还不完善
// app 控制您的应用的事件生命周期
// BrowserWindow 它负责创建和管理应用的窗口
const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron')
const path = require('path')
function handleSetTitle(event, title){ // 标题设置
// 确认渲染器
const webContents = event.sender
// 修改渲染器title
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
}
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,
// 将脚本附在渲染进程上
// __dirname: 指向当前正在执行脚本的路径
// path.join: 将多个路径联结在一起,创建一个跨平台的路径字符串。
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')
// Open the DevTools.
// win.webContents.openDevTools()
}
app.whenReady().then(() => { // 应用就绪
// 使用 ipcMain.on API 在 set-title 通道上设置一个 IPC 监听器
ipcMain.on("set-title", handleSetTitle)
// ipcMain.handle用于双向通信 监听
ipcMain.handle('dialog:openFile', handleFileOpen)
// 主进程向渲染器进程发送信息
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
// 创建页面
createWindow()
// activate事件 监听活动状态?
app.on('activate', () => {
// 如果没有任何活动的窗口
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => { // 事件 所有窗口关闭之后
// 调用app.quit()方法退出应用 此方法不适用于macOS
if (process.platform !== 'darwin') app.quit()
})
renerer.js
// 使用DOM接口来替换 id 属性为 info 的 HTML 元素显示文本
const information = document.getElementById('info')
information.innerText = `本应用正在使用Chrome (v${versions.chrome()}),Node.js (v${versions.node()}),和Electron (v${versions.electron()})`
// 单向通信
const setButton = document.getElementById('setButton')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
// 进程通信 渲染器调用了通过preload脚本暴露出来的electronAPI的setTitle方法 并传进去了一个参数title
window.electronAPI.setTitle(title)
});
// 双向通信
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => { // 因为文件是需要等待用户操作的所以是异步调用吗
// 进程通信 调用暴露的方法openFile 等待返回值filePath
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
// 主进程向渲染器进程发送信息
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((_event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
})
preload.js
// 创建一个将应用中的 Chrome、Node、Electron 版本号暴露至渲染器的预加载脚本
// ipcRenderer用于进程间的通信
const { contextBridge, ipcRenderer } = require('electron')
// 在主进程中通过versions进行暴露
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
// 能暴露的不仅仅是函数,我们还可以暴露变量
})
contextBridge.exposeInMainWorld('electronAPI', {
// ipcRenderer.send单向发送信息 往set-title通道发送信息 传递参数title
setTitle: (title) => ipcRenderer.send('set-title', title),
// ipcRenderer.invoke双向发送信息
openFile: () => ipcRenderer.invoke('dialog:openFile'),
//
handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
API
nativeTheme
- 作用:读取并响应Chromium本地色彩主题中的变化
- 进程:主进程
shouldUseDarkColors
- 只读
- 此属性的值为一个 boolean 类型的值,代表着当前OS / Chromium是否正处于dark模式,或者应用程序是否正被建议使用dark模式的皮肤
themeSource
- 一个类型为string的属性,此属性可能的值为:system, light or dark. 它被用来覆盖、重写Chromium内部的相应的值
- 默认情况下 themeSource 是 system
- 将此属性设置为 dark 将产生以下效果:
- 当访问 nativeTheme.shouldUseDarkColors 时值为 true
- 任何在 Linux 和 Windows 上的 UI Electron 渲染,包括 context menus、devtools 等等,都会使用暗色界面。
- 在 MacOS 上打开的任何 UI 界面,包括 menus、window frames 等,渲染时都会使用暗色界面。
- prefers-color-scheme CSS 查询将匹配 dark 模式(这个是我们用来改变页面样式的)
- updated 事件将被触发