前端必学桌面开发:Electron+React开发桌面应用(1W字以上超详细)
本篇教程是使用Electron + React18
进行开发,这里主要讲electron的使用。首先我们需要一个react的项目环境,react的项目搭建及开发教程可以参考我的react专栏里的文章:react相关技术这里都有
前言
Electron是一个开源的跨平台桌面应用程序开发框架,允许开发者使用前端 Web 技术(HTML、CSS 和 JavaScript)来构建桌面应用程序
背景和起源
Electron 最初由 GitHub 公司开发,最早用于构建 GitHub Desktop。随着其成功,Electron 逐渐成为一个受欢迎的开发框架,许多知名应用程序如 Visual Studio Code、Slack、WhatsApp 等也使用 Electron 构建。
基本原理
Electron 使用 Chromium 渲染引擎来显示 Web 内容,同时结合 Node.js 来提供对操作系统的访问和控制。这使得开发者能够使用 Web 技术来构建桌面应用程序,同时还能够利用底层操作系统的功能。
主要特点
-
跨平台
: Electron 应用程序可以在多个操作系统(如 Windows、macOS、Linux)上运行,因为它们在不同平台上共享相同的核心代码。 -
前端技术
: 开发者可以使用熟悉的前端技术,如 HTML、CSS 和 JavaScript,来构建用户界面。 -
Node.js 集成
: 通过 Node.js,开发者可以在应用程序中使用 JavaScript 来处理文件系统、网络通信、操作系统 API 等等。 -
自定义性
: 开发者可以通过使用原生的 Web 技术和样式,创建非常定制化的用户界面。 -
社区支持
: 有一个活跃的社区,提供了大量的插件和库,以帮助开发者构建更强大、更高效的应用程序。
核心组件
-
主进程(Main Process)
: 这是应用程序的主要控制中心,运行 Node.js 环境,负责管理和控制所有的渲染进程和窗口。 -
渲染进程(Renderer Process)
: 每个 Electron 窗口对应一个独立的渲染进程,它们运行在 Chromium 渲染引擎中,负责显示用户界面。 -
主窗口(Main Window)
: 主窗口是应用程序的主界面,通常是一个 Chromium 窗口,用来显示 Web 内容。 -
系统托盘图标(Tray)
: 允许在桌面右下角显示小图标,提供应用程序的快速访问和交互。
开发流程
- 使用 HTML、CSS 和 JavaScript 创建用户界面。
- 在主进程中使用 Node.js 进行应用程序的逻辑控制。
- 通过与底层操作系统 API 进行交互,实现文件操作、网络通信等功能。
- 使用 Electron 打包工具将应用程序打包成特定平台的可执行文件。
核心架构图解
Electron安装
安装electron
首先,我们需要在一个常规的React项目中,安装electron,为了使我们功能代码部分和electron窗口部分更清晰,我们可以在项目的根目录新建一个desktop
文件夹,专门用来存放electron部分代码和资源。目录结构大概如图所示:
我们cd desktop
到desktop文件夹下,执行npm init -y
初始化包管理器,然后安装electron相关包:electron
:electron核心包cross-env
:cross-env 是一个用于设置跨平台环境变量的工具。它可以在 Windows、Linux 和 macOS 等操作系统上提供一致的环境变量设置方式,使得在不同平台上运行脚本时能够保持一致的行为。electron-builder
:electron-builder 是一个用于打包、构建和部署 Electron 应用程序的强大工具
npm i electron cross-env electron-builder
Electron App生命周期,创建窗口,应用运行
App生命周期
在electron应用的运行过程中存在着自己的生命周期,在不同的生命周期中我们可以做对应的事情,下面介绍一些常用的生命周期,electron的生命周期通过electron中的app实例监听
,我们在desktop目录下新建一个index.js文件,作为electron的入口文件,并在其中监听应用的各个生命周期
ready
触发时机:当 Electron 初始化完成并且应用程序准备好创建浏览器窗口时。
作用:通常用于初始化应用程序的主要界面和一些基础设施。
示例:在 ready 事件中创建主窗口和初始化托盘。certificate-error
触发时机:当在加载网页时发生证书错误时。
作用:可以在这个事件中拦截证书错误并决定是否继续加载页面。
示例:在证书错误时阻止默认行为并返回 true 以继续加载页面。before-quit
触发时机:当用户尝试退出应用程序时,通常是通过关闭所有窗口或者点击关闭按钮。
作用:在应用程序退出之前执行一些清理操作。
示例:可以在这个事件中执行一些清理或保存操作。window-all-closed
触发时机:所有应用程序窗口都被关闭时。
作用:在此事件中通常用于在应用程序完全退出之前保留某些功能。
示例:在 macOS 下通常会保留菜单栏。activate
触发时机:在点击 Dock 图标(macOS)或者任务栏图标(Windows)时。
作用:通常用于在所有窗口都已关闭的情况下,重新创建主窗口。
示例:在 macOS 下,当点击 Dock 图标时,可以重新创建主窗口。quit
触发时机:应用程序即将退出时。
作用:在应用程序退出之前执行最后的清理操作。
示例:在这个事件中可以销毁托盘或其他资源。will-quit
触发时机:在应用程序即将退出时,但在 quit 事件之前。
作用:在应用程序退出之前执行一些清理或保存操作。
示例:在这个事件中可以执行一些清理或保存操作。will-finish-launching
触发时机:在应用程序即将完成启动时。
作用:可以在此事件中执行一些在应用程序完全启动之前需要完成的操作。
示例:在这个事件中可以初始化一些启动时需要的资源。
const { app } = require('electron')
const { createMainWindow } = require('./windows/mainWindow')
app.on('ready', () => {
createMainWindow()
})
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
event.preventDefault()
callback(true)
})
app.on('before-quit', () => {
console.log('app before-quit')
})
app.on('window-all-closed', function () {
console.log('window-all-closed')
})
app.on('activate', function () {
console.log('activate')
})
app.on('quit', function () {
console.log('quit')
getTray() && getTray().destroy()
})
app.on('will-quit', function () {
console.log('will-quit')
})
app.on('will-finish-launching', function () {
console.log('will-finish-launching')
})
创建窗口
我们在desktop文件夹中创建一个windows文件夹,里面存放每个窗口的相关代码(我们项目中通常不止一个窗口)
,我们在windows文件夹中创建一个mainWindow.js
文件,用于创建一个简单的窗口
// 在主进程中.
const { BrowserWindow } = require('electron')
const path = require('path')
const win = new BrowserWindow({ width: 800, height: 600 })
// Load a remote URL
win.loadURL('http://localhost:8000/')
// Or load a local HTML file
win.loadFile(path.resolve(__dirname, '../../../build/index.html'))
其中loadURL
用于加载一个服务器地址,运行后将会在窗口中显示该地址的内容,我们这里的http://localhost:8000/
是代码运行的本地环境地址loadFile
是加载一个静态文件,该文件就是渲染层代码打包后的入口文件
。
应用运行
由于创建窗口需要在app.on('ready', () => {})
中,因此我们可以把创建窗口封装成一个函数并导出,在app.on('ready', () => {})
中执行,例如:
封装mainWindow.js
const { BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const isDevelopment = process.env.NODE_ENV === 'development'
let mainWindow = null
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1160,
height: 752,
minHeight: 632,
minWidth: 960,
show: false,
frame: false,
title: 'Harbour',
webPreferences: {
nodeIntegration: true,
preload: path.resolve(__dirname, '../utils/contextBridge.js')
},
icon: path.resolve(__dirname, '../assets/logo.png')
})
if (isDevelopment) {
mainWindow.loadURL('http://localhost:8000/')
} else {
const entryPath = path.resolve(__dirname, '../../build/index.html')
mainWindow.loadFile(entryPath)
}
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
}
module.exports = { createMainWindow }
代码解析:我们这里使用process.env.NODE_ENV的值判断当前的运行环境,这里的运行环境需要说明一下,当我们使用下面配置的npm run dev-electron运行时,该值为"development",当我们将渲染层代码打包后,使用npm run prod-electron运行时,该值为"production",然而当我们使用electron-builder打包出来的安装包运行时,该值不存在为undefined。因此只有当该值是"development"时我们才加载一个我们渲染层启动的服务地址,其他两种情况下我们都需要加载我们渲染层打包后的入口文件即:build目录下的index.html
我们在窗口触发"ready-to-show"时显示窗口是为了使加载时的白屏时间不被用户看到
index.js导入并在app.on('ready', () => {})中执行
const { app } = require('electron')
const { createMainWindow } = require('./windows/mainWindow')
app.on('ready', () => {
createMainWindow()
})
此时我们就可以运行electron,我们在package.json中配置运行命令
{
"scripts": {
"dev-electron": "cross-env NODE_ENV=development electron main/index.js",
"prod-electron": "cross-env NODE_ENV=production electron main/index.js",
}
}
执行命令,启动开发环境
npm run dev-electron
运行成功,出现如下窗口(窗口内部内容可自行定义)使用npm run prod-electron命令可以启动生产环境,该生产环境指的是渲染层的功能代码使用webpack打包后的代码,使其渲染到窗口中。真正的生产环境应该是下面介绍的使用electron-builder打包后的应用程序,此时process.env.NODE_ENV为undefined
Electron应用打包
上面我们启动electron的应用都是使用的node_modules中的electron包,我们想要得到一个真正可以安装的安装包,还需要使用第三方打包工具进行打包,上面有提到过,我们将使用electron-builder
打包成可安装的安装包。上面我们已经安装了electron-builder
,下面我们需要在package.json中配置build属性来自定义安装配置
。(限于自身设备问题,这里只介绍在Windows系统的打包配置,electron可以打包成各种安装包,使其可以在mac,Linux系统上运行,其他系统的配置可自行查阅资料。
)下面我们介绍一下配置内容和各个配置含义。package.json完整配置
{
"name": "desktop",
"productName": "Harbour",
"version": "1.0.0",
"description": "",
"main": "main/index.js",
"scripts": {
"dev-electron": "cross-env NODE_ENV=development electron main/index.js",
"prod-electron": "cross-env NODE_ENV=production electron main/index.js",
"build-electron-win64": "electron-builder -w --x64"
},
"build": {
"productName": "Harbour",
"appId": "harbour.electron.app",
"files": [
"build/**/*",
"main/**/*"
],
"directories": {
"output": "dist"
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"installerIcon": "./main/assets/logo.ico",
"uninstallerIcon": "./main/assets/logo.ico",
"installerHeaderIcon": "./main/assets/logo.png",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "Harbour"
},
"win": {
"icon": "./main/assets/logo.ico",
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
"target": "nsis"
},
"electronDist": "./electron"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cross-env": "^7.0.3",
"electron": "^26.1.0",
"electron-builder": "^24.6.3"
}
}
配置解释
-
productName
:指定了您的应用程序的产品名称,通常用于构建过程中生成的安装程序文件名等地方。 -
appId
:指定了您的应用程序的唯一标识符,这个值在打包和部署时会用到。 -
files
:指定打包时所需打包的文件 -
directories.output
:指定了输出目录的路径,即构建后的文件将会保存在 dist 目录中。 -
nsis
:指定了 NSIS(Nullsoft Scriptable Install System)打包的相关配置。-
oneClick
:指定是否启用一键安装模式。 -
allowElevation
:是否允许提升权限进行安装。 -
allowToChangeInstallationDirectory
:是否允许用户更改安装目录。 -
installerIcon
:安装程序的图标文件路径。 -
uninstallerIcon
:卸载程序的图标文件路径。 -
installerHeaderIcon
:安装程序的头部图标文件路径。 -
createDesktopShortcut
:是否在桌面上创建快捷方式。 -
createStartMenuShortcut
:是否在开始菜单中创建快捷方式。 -
shortcutName
:创建的快捷方式的名称。
-
-
win
:指定了 Windows 平台的配置。-
icon
:指定应用程序的图标文件路径。 -
artifactName
:定义生成的构建文件的命名规则模板。 -
target
:指定构建的目标平台,这里是 NSIS。
-
-
electronDist
:指定了预先下载的 Electron 包的路径。
特别注意
这里有几个需要特别注意的点:
- 首先我们用的
logo.ico
文件尺寸大小至少是256*256
的 - 由于打包时需要使用
electron的相关包文件,为了提高打包速度,我们一般会提前下载与我们node_modules相同版本的.zip包,然后打包时使用electronDist指定打包用的文件目录,可以缩减打包时间
- 自定义
artifactName
,该名称就是打包后我们可安装的.exe
可执行文件的名称 -
electron-builder打包原理
实际上是将package.json
同目录的所有文件进行整体打包输出,如下图所示,在package.json
同级目录下有一些文件夹我们是不需要进行打包的,其中dist下是我们上次打包输出的内容,electron是我们预下载的打包所需的.zip包,node_modules下面是我们开发时所用的依赖包,这些都不需要打包进去。
因此我们需要指定我们打包时所需要打包的文件夹,此时就需要用到package.json里面build配置中的files属性,如上配置,我们只需要将build目录下的文件和main下面的文件打包即可。
这里的build目录下是渲染层的代码,main下面都是我们主进程的代码
打包后的内容
dist目录下就是打包生成的内容,其中第一个红框的Harbour.exe是可直接执行的文件,无需安装
,第二个红框中的.exe可执行文件就是可安装的文件,在文件夹中,双击即可进入安装流程。
Electron常用API详解
使用BrowserWindow创建窗口
创建窗口常用配置Option
在我们创建窗口时可以配置很多自定义配置,下面是一些常用配置及解析:
-
width 和 height
:用于设置窗口的初始宽度和高度。 -
x 和 y
:控制窗口的初始位置,以屏幕坐标为基准。 -
fullscreen
:布尔值,指定窗口是否以全屏模式启动。 -
resizable
:布尔值,控制用户是否可以调整窗口大小。 -
minWidth 和 minHeight
:指定窗口的最小宽度和最小高度。 -
maxWidth 和 maxHeight
:指定窗口的最大宽度和最大高度。 -
frame
:布尔值,指定是否显示窗口的外部框架(包括标题栏和控制按钮)。 -
title
:用于设置窗口的标题。 -
icon
:指定窗口的图标文件路径。 -
backgroundColor
:用于设置窗口的背景颜色。 -
webPreferences
:用于配置窗口的 Web 集成选项,例如启用 Node.js、预加载脚本等。 -
nodeIntegration
:指定是否在渲染进程中启用 Node.js 集成,允许在渲染进程中使用 Node.js API。 -
contextIsolation
:启用上下文隔离,将渲染进程的环境与主进程隔离开来,以提高安全性。 -
preload
:指定一个预加载的 JavaScript 文件的路径,该文件在渲染进程运行之前加载。 -
devTools
:指定是否允许在窗口中打开开发者工具。 -
webSecurity
:指定是否启用同源策略,限制页面对其他源的请求。 -
alwaysOnTop
:布尔值,控制窗口是否始终保持在顶部。 -
fullscreenable
:布尔值,指定窗口是否可以进入全屏模式。 -
show
:布尔值,指定创建窗口后是否立即显示。 -
transparent
:布尔值,指定窗口是否支持透明度。 -
parent 和 modal
:用于实现模态窗口的行为。 -
closable
:布尔值,指定用户是否可以关闭窗口。 -
focusable
:布尔值,指定窗口是否可以获得焦点。 -
minimizable 和 maximizable
:控制窗口是否可以最小化和最大化。 -
skipTaskbar
:布尔值,控制窗口是否在任务栏中显示。
const { BrowserWindow } = require('electron');
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
x: 100,
y: 100,
fullscreen: false,
resizable: true,
minWidth: 400,
minHeight: 300,
frame: true,
title: 'My Electron App',
icon: '/path/to/icon.png',
backgroundColor: '#ffffff',
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
preload: 'path/to/preload.js',
devTools: true,
webSecurity: true
},
alwaysOnTop: false,
fullscreenable: true,
show: true,
transparent: false,
closable: true
});
mainWindow.loadFile('index.html');
窗口常用的实例事件
窗口有很多实例事件,使用window.on来监听,可以在这些事件触发时做一切操作
例如下面是一些常用的实例事件:close
触发时机:窗口即将关闭时触发,但实际关闭前。
作用:允许执行一些在窗口关闭前的清理操作,或者阻止窗口关闭。closed
触发时机:窗口已经关闭时触发。
作用:通常用于释放资源或执行一些在窗口关闭后的最终操作。resize
触发时机:窗口大小发生变化时触发。
作用:允许在窗口大小变化时执行一些操作。move
触发时机:窗口位置发生变化时触发。
作用:允许在窗口位置变化时执行一些操作。focus
触发时机:窗口获得焦点时触发。
作用:允许在窗口获得焦点时执行一些操作。blur
触发时机:窗口失去焦点时触发。
作用:允许在窗口失去焦点时执行一些操作。minimize
触发时机:窗口被最小化时触发。
作用:允许在窗口最小化时执行一些操作。maximize
触发时机:窗口被最大化时触发。
作用:允许在窗口最大化时执行一些操作。unmaximize
触发时机:窗口从最大化状态恢复时触发。
作用:允许在窗口从最大化状态恢复时执行一些操作。ready-to-show
触发时机:当窗口完成初始化并且准备好显示时触发。
作用:允许在窗口已准备好显示之后执行一些操作。这通常在窗口加载内容后并准备好显示时触发,用于控制窗口的显示时机。show
触发时机:当窗口被显示时触发。
作用:允许在窗口显示时执行一些操作。hide
触发时机:当窗口被隐藏时触发。
作用:允许在窗口隐藏时执行一些操作。enter-full-screen
触发时机:当窗口进入全屏模式时触发。
作用:允许在窗口进入全屏模式时执行一些操作。leave-full-screen
触发时机:当窗口离开全屏模式时触发。
作用:允许在窗口离开全屏模式时执行一些操作。
// main.js
const { app, BrowserWindow } = require('electron');
let mainWindow;
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
});
// 加载你的 HTML 文件
mainWindow.loadFile('index.html');
// 事件: 关闭
mainWindow.on('close', (event) => {
// 允许或阻止窗口关闭
// event.preventDefault();
// 执行清理操作
});
// 事件: 关闭后
mainWindow.on('closed', () => {
// 释放资源或执行最终操作
mainWindow = null;
});
// 事件: 调整大小
mainWindow.on('resize', () => {
// 在窗口调整大小时执行操作
});
// 事件: 移动
mainWindow.on('move', () => {
// 在窗口移动时执行操作
});
// 事件: 获得焦点
mainWindow.on('focus', () => {
// 在窗口获得焦点时执行操作
});
// 事件: 失去焦点
mainWindow.on('blur', () => {
// 在窗口失去焦点时执行操作
});
// 事件: 最小化
mainWindow.on('minimize', () => {
// 在窗口最小化时执行操作
});
// 事件: 最大化
mainWindow.on('maximize', () => {
// 在窗口最大化时执行操作
});
// 事件: 还原
mainWindow.on('unmaximize', () => {
// 在窗口从最大化状态还原时执行操作
});
// 事件: 准备好显示
mainWindow.on('ready-to-show', () => {
// 在窗口准备好显示后执行操作
mainWindow.show();
});
// 事件: 显示
mainWindow.on('show', () => {
// 在窗口显示时执行操作
});
// 事件: 隐藏
mainWindow.on('hide', () => {
// 在窗口隐藏时执行操作
});
// 事件: 进入全屏模式
mainWindow.on('enter-full-screen', () => {
// 在窗口进入全屏模式时执行操作
});
// 事件: 离开全屏模式
mainWindow.on('leave-full-screen', () => {
// 在窗口离开全屏模式时执行操作
});
}
窗口常用的实例属性
窗口自身存在很多的实例属性,可以使我们获取到窗口的一些当前状态。下面是一些常用的实例属性。
-
win.id
- 窗口的唯一ID。 -
win.webContents
- 包含窗口网页内容的BrowserWindowProxy对象。 -
win.devToolsWebContents
- 用于开发者工具窗口的webContents。 -
win.minimizable
- 是否允许最小化窗口,默认为true。 -
win.maximizable
- 是否允许最大化窗口,默认为true。 -
win.fullScreenable
- 是否允许全屏窗口,默认为true。 -
win.resizable
- 是否允许改变窗口大小,默认为true。 -
win.closable
- 是否允许关闭窗口,默认为true。 -
win.movable
- 是否允许移动窗口,默认为true。 -
win.alwaysOnTop
- 是否永远置顶,默认为false。 -
win.modal
- 是否为模态窗口,默认为false。 -
win.title
- 窗口标题。 -
win.defaultWidth/Height
- 窗口默认宽高。 -
win.width/height
- 窗口当前宽高。 -
win.x/y
- 窗口左上角坐标。
窗口常用的实例方法
-
win.loadURL(url)
- 加载指定URL到窗口中,通常用于加载本地文件或远程网页。 -
win.webContents.send(channel, ...args)
- 在窗口之间发送异步消息。channel 是一个字符串,用于标识消息的类型,...args 是要传递的参数。 -
win.show()
- 显示窗口,通常与 hide() 方法配合使用。 -
win.hide()
- 隐藏窗口。 -
win.close()
- 关闭窗口 -
win.minimize()
- 最小化窗口 -
win.maximize()
- 最大化窗口 -
win.restore()
- 恢复窗口大小和位置。 -
win.setSize(width, height[, animate])
- 设置窗口的宽度和高度。 -
win.setPosition(x, y[, animate])
- 设置窗口的位置。 -
win.getTitle()
- 获取窗口的标题。 -
win.setTitle(title)
- 设置窗口的标题。 -
win.setMenu(menu)
- 设置窗口的菜单。 -
win.setResizable(resizable)
- 设置窗口是否可以改变大小。 -
win.setAlwaysOnTop(flag[, level[, relativeLevel]])
- 将窗口置顶。 -
win.setMenu(null)
- 隐藏窗口的菜单栏。 -
win.setProgressBar(progress)
- 设置窗口的任务栏进度条。 -
win.focus()
- 将窗口置于前台并获得焦点。 -
win.isVisible()
- 返回窗口是否可见。 -
win.isFullScreen()
- 返回窗口是否全屏。 -
win.isMaximized()
- 返回窗口是否最大化。 -
win.webContents.executeJavaScript(code[, userGesture])
- 在窗口的渲染进程中执行一段 JavaScript 代码。 -
win.openDevTools([options])
- 打开开发者工具。
创建右下角托盘
对于一个桌面应用来说,右下角的系统托盘必不可少,electron应用的系统托盘使用tray
这个api实现,下面是封装的专门处理系统托盘的文件systemTray.js
const { app, Tray, Menu } = require('electron')
const path = require('path')
const { getMainWindow, mainWindowIsExist } = require('./windows/mainWindow')
let tray = null
const iconPath = path.resolve(__dirname, './assets/logo.png')
function initTray() {
tray = new Tray(iconPath)
const contextMenu = Menu.buildFromTemplate([
{
label: '打开应用', click: () => {
mainWindowIsExist() && getMainWindow().show()
}
},
{ label: '退出应用', click: () => { app.quit() } },
])
tray.setToolTip('Harbour') // 设置鼠标悬停时显示的提示信息
tray.setContextMenu(contextMenu)
tray.on('click', () => {
mainWindowIsExist() && getMainWindow().show()
})
}
function getTray() {
return tray
}
module.exports = { initTray, getTray }
代码解析
-
iconPath
获取托盘图标路径,这里注意一定要使用path.resolve
生产绝对路径
否则打包成安装包后会无法找到该文件导致报错 -
Menu.buildFromTemplate
是electron的一个方法,用来创建一个菜单,菜单的label
是显示的内容,click
是点击后触发的事件 -
tray.setToolTip('Harbour')
是用来设置鼠标悬停时显示的提示信息 -
tray.setContextMenu(contextMenu)
将使用Menu.buildFromTemplate创建出的菜单设置为托盘菜单 -
tray.on('click', () => {})
当点击托盘的时候触发的事件,我们这里是将mainWindow
show出来
初始化系统托盘
系统托盘的初始化需要在app.on('ready')
之后,因此我们将初始化系统托盘的方法封装好导出,在app.on('ready')
中执行
const { app } = require('electron')
const { createMainWindow } = require('./windows/mainWindow')
const { initTray, getTray } = require('./systemTray')
app.on('ready', () => {
createMainWindow()
initTray()
})
应用层和主进程通信(ipcMain,ipcRender)
应用层和主进程之间的通信流程是:
应用层使用ipcRender.send方法将事件及数据传递到主进程
主进程使用ipcMain.on或者ipcMain.once方法监听事件并获取数据
主进程使用ipcMain.removeListener移除事件监听或者ipcMain.removeAllListeners移除所有事件监听
主进程使用窗口实例的webContents.send方法将事件和数据传递到应用层
应用层使用ipcRender.on或者ipcRender.once监听事件并获取数据
应用层使用ipcRenderer.removeListener移除事件监听或者ipcRenderer.removeAllListeners移除所有事件监听
图解如下
将ipcRender,process注入到应用层
我们知道ipcMain和ipcRender都是electron的Api,要想在应用层使用ipcRender就需要先将其注入到应用层,在electron中使用contextBridge.exposeInMainWorld方法将electron的Api注入到应用层,注入之后我们就可以在应用层的window*问注入的属性。我们这里将ipcRender和process两个属性注入到应用层,分别用来实现通信和判断当前运行环境。
封装contextBridge.js文件
const { contextBridge, ipcRenderer } = require('electron')
/**
* contextBridge.exposeInMainWorld的作用就是将主进程的某些API注入到渲染进程,
* 供渲染进程使用(主进程并非所有的API或对象都能注入给渲染进程,需要参考文档)
* ipcRenderer 渲染进程通过window.ipcRenderer调用
*/
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, ...args) => {
if (args?.length > 0) {
ipcRenderer.send(channel, ...args)
} else {
ipcRenderer.send(channel)
}
},
on: (channel, func) => {
ipcRenderer.on(channel, func)
},
once: (channel, func) => {
ipcRenderer.once(channel, func)
},
removeListener: (channel, func) => {
ipcRenderer.removeListener(channel, func)
},
sendSync: (channel, ...args) => {
if (args?.length > 0) {
return ipcRenderer.sendSync(channel, ...args)
} else {
return ipcRenderer.sendSync(channel)
}
},
invoke: (channel, ...args) => {
try {
return ipcRenderer.invoke(channel, ...args)
} catch (error) {
console.error(`Error invoking API: ${channel}`, error)
}
},
})
contextBridge.exposeInMainWorld('process', {
NODE_ENV: process.env.NODE_ENV
})
这里我们将ipcRender的send,on,once,removeListener,sendSync,invoke方法及process.env.NODE_ENV注入到应用层,后续可在应用层进行使用
注意,该方法需要在应用层渲染时执行,因此我们刚好可以用到创建窗口中的option.webPreferences.preload来加载该文件,后续有案例代码。
应用层封装注入的Api
我们将ipcRender和process
注入到应用层后,为了后期的维护我们可以将所有的方法再次进行封装,放在一个统一的文件中,封装desktopUtils.ts
declare global {
interface Window {
ipcRenderer: {
send: (...args: any[]) => void,
on: (channel: string, listener: (...args: any[]) => void) => void,
once: (channel: string, listener: (...args: any[]) => void) => void,
removeListener: (channel: string, listener: (...args: any[]) => void) => void,
sendSync: (...args: any[]) => any,
invoke: (...args: any[]) => Promise<any>,
},
process: {
NODE_ENV: 'development' | 'production'
}
}
}
type ArgsType = string | number | boolean | { [key: string]: any } | any[]
export const isDesktop = () => {
return !!window.ipcRenderer
}
export const getProcessNodeEnv = () => {
return window?.process.NODE_ENV
}
export const ipcRendererSend = (eventName: string, ...args: ArgsType[]) => {
window.ipcRenderer?.send(eventName, ...args)
}
export const ipcRendererSendSync = (eventName: string, ...args: ArgsType[]) => {
return window.ipcRenderer?.sendSync(eventName, ...args)
}
export const ipcRendererInvoke = (eventName: string, ...args: ArgsType[]) => {
try {
return window.ipcRenderer?.invoke(eventName, ...args)
} catch (error) {
console.error(`Error invoking IPC: ${eventName}`, error)
return null
}
}
export const ipcRendererOn = (eventName: string, listener: (...args: ArgsType[]) => void) => {
window.ipcRenderer?.on(eventName, listener)
}
export const ipcRendererOnce = (eventName: string, listener: (...args: ArgsType[]) => void) => {
window.ipcRenderer?.once(eventName, listener)
}
export const ipcRendererRemoveListener = (eventName: string, listener: (...args: ArgsType[]) => void) => {
window.ipcRenderer?.removeListener(eventName, listener)
}
这里的isDesktop是用来判断当前是否是桌面端的,因为很多时候我们使用electron开发的桌面端应用需要兼容web端,由于应用层代码几乎相同,我们只需要在一些情况下特别处理桌面端的逻辑即可。由于web端的window上一定没有ipcRender这个属性,因此可以根据window.ipcRenderer来判断
getProcessNodeEnv
是用来获取当前桌面端的运行环境的,这里可以返回当前是开发环境还是生产环境,如果是web端的话,直接用process.env.NODE_ENV
即可判断
应用层发
上一篇: 使用 vscode 的提示
下一篇: [算法笔记] B1015 德才兼备论