本篇教程是使用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)
: 允许在桌面右下角显示小图标,提供应用程序的快速访问和交互。开发流程
核心架构图解
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
是加载一个静态文件,该文件可以使web项目打包后的入口文件。
应用运行
由于创建窗口需要在app.on('ready', () => {})
中,因此我们可以把创建窗口封装成一个函数并导出,在app.on('ready', () => {})
中执行,例如:封装mainWindow.js
const { BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const isProduction = process.env.NODE_ENV === 'production'
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 (isProduction) {
const entryPath = path.resolve(__dirname, '../../../build/index.html')
mainWindow.loadFile(entryPath)
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadURL('http://localhost:8000/')
}
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
}
module.exports = { createMainWindow }
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应用打包
上面我们启动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",
"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
:指定了您的应用程序的唯一标识符,这个值在打包和部署时会用到。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
可执行文件的名称打包后的内容
在desktop文件夹下的electron压缩包就是打包时使用的文件。
,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.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
- 窗口左上角坐标。 创建右下角托盘
对于一个桌面应用来说,右下角的系统托盘必不可少,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,
},
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
即可判断
应用层发送事件到主进程
我们封装好了方法之后就可以进行使用了,我们做一个简单的案例,应用的header,有最小化,最大化,关闭,和恢复按钮
,在点击时使用ipcRendererSend
方法将事件传给主进程并进行相应操作。由于我们需要一个状态判断显示最大化按钮还是恢复按钮
因此需要监听主进程在执行最大化和恢复之后传回的事件和当前状态。desktopHeader.tsx
import React, { memo, useState, useEffect } from 'react'
import './desktopHeader.less'
import SvgIcon from '@components/svgIcon'
import {
ipcRendererSend,
ipcRendererOn,
ipcRendererRemoveListener
} from '@common/desktopUtils'
import logoImage from '@assets/logo.png'
function DesktopHeader() {
const [windowIsMax, setWindowIsMax] = useState(false)
useEffect(() => {
const handleSetIsMax = (event: any, isMax: boolean) => {
setWindowIsMax(isMax)
}
ipcRendererOn('mainWindowIsMax', handleSetIsMax)
return () => {
ipcRendererRemoveListener('mainWindowIsMax', handleSetIsMax)
}
}, [])
const handleWindow = (eventName: string) => {
ipcRendererSend(`mainWindow-${eventName}`)
}
return (
Harbour
{windowIsMax ? (
) : (
)}
)
}
export default memo(DesktopHeader)
主进程接收事件并向应用层发送事件
在主进程中我们使用ipcMain.on来监听事件,并进行相应操作,并在相应操作之后在需要的时候发送事件到应用层。
下面我们封装mainWindow.js
里面包含创建窗口方法,获取窗口实例方法,获取窗口是否存在方法及事件监听方法
mainWindow.js
const { BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const isProduction = process.env.NODE_ENV === 'production'
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 (isProduction) {
const entryPath = path.resolve(__dirname, '../../../build/index.html')
mainWindow.loadFile(entryPath)
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadURL('http://localhost:8000/')
}
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
mainWindowListenEvents()
}
function mainWindowListenEvents() {
ipcMain.on('mainWindow-min', () => {
mainWindowIsExist() && mainWindow.minimize()
})
ipcMain.on('mainWindow-max', () => {
if (mainWindowIsExist()) {
mainWindow.maximize()
mainWindow.webContents.send('mainWindowIsMax', true)
}
})
ipcMain.on('mainWindow-restore', () => {
if (mainWindowIsExist()) {
mainWindow.unmaximize()
mainWindow.webContents.send('mainWindowIsMax', false)
}
})
ipcMain.on('mainWindow-close', () => {
mainWindowIsExist() && mainWindow.hide()
})
ipcMain.on('mainWindow-open-devtool', () => {
mainWindowIsExist() && mainWindow.webContents.openDevTools()
})
}
function mainWindowIsExist() {
return mainWindow && !mainWindow.isDestroyed()
}
function getMainWindow() {
return mainWindow
}
module.exports = {
getMainWindow,
createMainWindow,
mainWindowIsExist
}
窗口之间通信
要实现窗口之间的通信,我们实际上就是使用应用层和主进程之间的通信,由于主进程可以接收到任意窗口发过来的事件,因此我们想实现窗口之间的通信,只需要在主进程中进行转发就好,下面是图解。
// 主窗口
ipcRendererSend('sendToSecond', '123')
// 主进程
ipcMain.on('sendToSecond', (e, data) => {
secondWindow.webContents.send('sendToSecond', data)
})
// 第二窗口
ipcRendererOn('sendToSecond', handler)
使用clipboard操作剪切板
clipboard 是 Electron 提供的模块之一,用于在应用程序中进行剪贴板操作。它允许你读取和写入系统剪贴板中的文本、图像等数据。以下是一些常用的 clipboard 模块方法clipboard.writeText(text[, type])
将文本写入剪贴板。text
:要写入剪贴板的文本内容。type
(可选):可指定数据类型,默认为 clipboard。可以是 selection(用于选区)或 clipboard(用于剪贴板)。
const { clipboard } = require('electron')
clipboard.writeText('Hello, World!')
clipboard.readText([type])
从剪贴板中读取文本内容。type
(可选):可指定数据类型,默认为 clipboard。可以是 selection(用于选区)或 clipboard(用于剪贴板)。
const { clipboard } = require('electron')
const text = clipboard.readText()
console.log(text)
clipboard.writeHTML(markup[, type])
将 HTML 内容写入剪贴板。markup
:要写入剪贴板的 HTML 内容。type
(可选):可指定数据类型,默认为 clipboard。可以是 selection(用于选区)或 clipboard(用于剪贴板)。
const { clipboard } = require('electron')
const html = 'Hello, World!'
clipboard.writeHTML(html)
clipboard.readHTML([type])
从剪贴板中读取 HTML 内容。type
(可选):可指定数据类型,默认为 clipboard。可以是 selection(用于选区)或 clipboard(用于剪贴板)。
const { clipboard } = require('electron')
const html = clipboard.readHTML()
console.log(html)
clipboard.writeImage(image[, type])
将图像写入剪贴板。image
:要写入剪贴板的图像,可以是一个 nativeImage 对象或者一个文件路径。type
(可选):可指定数据类型,默认为 clipboard。可以是 selection(用于选区)或 clipboard(用于剪贴板)。
const { clipboard, nativeImage } = require('electron')
const image = nativeImage.createFromPath('/path/to/image.png')
clipboard.writeImage(image)
clipboard.readImage([type])
从剪贴板中读取图像。type
(可选):可指定数据类型,默认为 clipboard。可以是 selection(用于选区)或 clipboard(用于剪贴板)。
const { clipboard } = require('electron')
const image = clipboard.readImage()
clipboard.clear([type])
清空剪贴板内容。type
(可选):可指定数据类型,默认为 clipboard。可以是 selection(用于选区)或 clipboard(用于剪贴板)。
const { clipboard } = require('electron')
clipboard.clear()
使用dialog创建常用对话框
dialog 是 Electron 提供的模块之一,用于在桌面应用程序中创建对话框,以便与用户进行交互。它可以用于打开文件、保存文件、显示警告、错误等信息,以及进行用户输入的获取等操作。以下是一些常用的 dialog 模块方法
打开文件对话框
dialog.showOpenDialog([browserWindow, ]options)
打开一个文件选择对话框,允许用户选择一个或多个文件。
browserWindow
(可选):父窗口的引用。如果不传递此参数,对话框将会成为一个模态窗口。options
:配置对象,可以包含以下属性:
defaultPath
:字符串,指定对话框的默认路径。filters
:数组,定义文件类型过滤器。properties
:数组,包含 openFile、openDirectory、multiSelections 等属性,决定对话框的行为。const { dialog } = require('electron');
const options = {
title: '选择文件',
defaultPath: '/path/to/default/folder',
filters: [
{ name: 'Text Files', extensions: ['txt', 'text'] },
{ name: 'All Files', extensions: ['*'] }
],
properties: ['openFile', 'multiSelections']
};
dialog.showOpenDialog(null, options).then(result => {
console.log(result.filePaths)
}).catch(err => {
console.log(err)
})
保存文件对话框
dialog.showSaveDialog([browserWindow, ]options)
打开一个文件保存对话框,允许用户选择保存的路径和文件名。
browserWindow
(可选):父窗口的引用。options
:配置对象,可以包含以下属性:
defaultPath
:字符串,指定对话框的默认路径。filters
:数组,定义文件类型过滤器。const { dialog } = require('electron');
const options = {
title: '保存文件',
defaultPath: '/path/to/default/folder',
filters: [
{ name: 'Text Files', extensions: ['txt', 'text'] },
{ name: 'All Files', extensions: ['*'] }
]
};
dialog.showSaveDialog(null, options).then(result => {
console.log(result.filePath);
}).catch(err => {
console.log(err);
});
普通消息对话框
dialog.showMessageBox([browserWindow, ]options)
显示一个消息框,通常用于警告或者通知用户。
browserWindow
(可选):父窗口的引用。options
:配置对象,可以包含以下属性:
type
:可以是 'none'、'info'、'error'、'question'、'warning',决定消息框的类型。title
:字符串,消息框的标题。message
:字符串,要显示的消息文本。buttons
:数组,包含消息框的按钮,例如 ['Yes', 'No', 'Cancel']。defaultId
:数字,指定默认选择的按钮索引。const { dialog } = require('electron');
const options = {
type: 'info',
title: '信息',
message: '这是一个信息框。',
buttons: ['OK']
};
dialog.showMessageBox(null, options).then(result => {
console.log(result.response);
}).catch(err => {
console.log(err);
});
错误消息对话框
dialog.showErrorBox(title, content)
显示一个错误框,用于显示错误信息。
title
:字符串,对话框标题。content
:字符串,要显示的错误内容。const { dialog } = require('electron');
dialog.showErrorBox('发生错误', '这是一个错误框的示例。');
使用globalShortcut注册全局快捷键
globalShortcut 是 Electron 提供的模块之一,用于注册和响应全局键盘快捷键。这允许你在你的 Electron 应用程序中创建全局快捷键,以执行特定操作或触发事件。以下是一些常用的 globalShortcut 模块方法globalShortcut.register(accelerator, callback)
注册全局快捷键。
accelerator
:字符串,表示要注册的快捷键,如 "CmdOrCtrl+X"。callback
:当快捷键被触发时要执行的回调函数。const { globalShortcut } = require('electron');
globalShortcut.register('CmdOrCtrl+X', () => {
// 执行某些操作
});
globalShortcut.isRegistered(accelerator)
检查是否已经注册了指定的全局快捷键。accelerator
:要检查的快捷键。
const { globalShortcut } = require('electron');
const isRegistered = globalShortcut.isRegistered('CmdOrCtrl+X');
if (isRegistered) {
console.log('已注册');
} else {
console.log('未注册');
}
globalShortcut.unregister(accelerator)
注销已注册的全局快捷键。accelerator
:要注销的快捷键。
const { globalShortcut } = require('electron');
globalShortcut.unregister('CmdOrCtrl+X');
globalShortcut.unregisterAll()
注销所有已注册的全局快捷键。
const { globalShortcut } = require('electron');
globalShortcut.unregisterAll();
globalShortcut.getRegisteredKeys()
获取当前已注册的全局快捷键的列表。
const { globalShortcut } = require('electron');
const registeredKeys = globalShortcut.getRegisteredKeys();
console.log(registeredKeys);
使用Menu创建原生应用菜单和上下文菜单
Menu 是 Electron 中用于创建和管理应用程序菜单的模块。它允许你在应用程序的菜单栏、上下文菜单等位置定义菜单项,以便用户可以通过点击菜单项执行特定的操作。以下是一些常用的 Menu 模块方法和属性
创建菜单
创建顶级菜单
const { Menu } = require('electron');
const template = [
{
label: 'File',
submenu: [
{ role: 'openFile' },
{ role: 'saveFile' },
{ role: 'quit' }
]
},
{
label: 'Edit',
submenu: [
{ role: 'copy' },
{ role: 'paste' }
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu)
创建上下文菜单
const { Menu } = require('electron');
const template = [
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' }
];
const contextMenu = Menu.buildFromTemplate(template);
模板数组菜单和应用程序菜单
Menu.buildFromTemplate(template)
从模板数组创建一个菜单。template
:一个包含菜单项的数组。每个菜单项都是一个对象,包含 label、click 等属性。Menu.setApplicationMenu(menu)
设置应用程序菜单,通常用于顶级菜单栏。menu
:要设置为应用程序菜单的 Menu 对象。
菜单属性项
label
:字符串,菜单项显示的文本。accelerator
:字符串,可以是组合键,例如 'CmdOrCtrl+X'。click
:函数,当菜单项被点击时要执行的回调函数。role
:内置角色,例如 'copy'、'paste',会触发预定义的操作。submenu
:一个子菜单,包含一个子菜单的 Menu 对象。const template = [
{
label: 'File',
submenu: [
{ label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => { /* 打开文件 */ } },
{ role: 'save' },
{ type: 'separator' }, // 分隔线
{ role: 'quit' }
]
},
{
label: 'Edit',
submenu: [
{ role: 'copy' },
{ role: 'cut' },
{ role: 'paste' },
{
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
click: () => { /* 选择所有内容 */ }
}
]
}
];
菜单常用方法
menu.append(menuItem)
在菜单的末尾追加一个菜单项。menu.insert(position, menuItem)
在指定位置插入一个菜单项。menu.getMenuItemById(id)
根据菜单项的 id 获取菜单项。menu.popup([options])
在指定位置弹出菜单。
options
:一个对象,可以包含 x 和 y 属性,表示弹出菜单的位置。 使用nativeImage 处理图像
nativeImage 是 Electron 提供的模块之一,用于处理图像。它可以加载图像文件、从屏幕截取图像、创建空白图像等。nativeImage 支持跨平台,可以在主进程和渲染进程中使用。
nativeImage.createEmpty()
创建一个空白图像对象。const { nativeImage } = require('electron');
const emptyImage = nativeImage.createEmpty();
nativeImage.createFromPath(path[, options])
从文件路径创建图像对象。
const { nativeImage } = require('electron');
const imagePath = '/path/to/image.png';
const image = nativeImage.createFromPath(imagePath);
nativeImage.createFromBuffer(buffer[, options])
从缓冲区创建图像对象。
const { nativeImage } = require('electron');
const fs = require('fs');
const buffer = fs.readFileSync('/path/to/image.png');
const image = nativeImage.createFromBuffer(buffer);
nativeImage.createFromDataURL(dataURL)
从数据 URL 创建图像对象。const { nativeImage } = require('electron');
const dataURL = 'data:image/png;base64,iVBORw0KGg...';
const image = nativeImage.createFromDataURL(dataURL);
nativeImage.createFromNamedImage(imageName[, hslShift])
从系统的命名图像创建图像对象。
const { nativeImage } = require('electron');
const image = nativeImage.createFromNamedImage('NSStopProgressTemplate', { h: 0, s: 0, l: 0 });
nativeImage.createThumbnailFromPath(path, size[, callback])
从文件路径创建缩略图。
const { nativeImage } = require('electron');
const imagePath = '/path/to/image.png';
const size = { width: 100, height: 100 };
nativeImage.createThumbnailFromPath(imagePath, size, (thumbnail) => {
console.log(thumbnail);
});
nativeImage.isMacTemplateImage(image)
检查图像是否是 macOS 模板图像。
const { nativeImage } = require('electron');
const imagePath = '/path/to/image.png';
const image = nativeImage.createFromPath(imagePath);
console.log(nativeImage.isMacTemplateImage(image)); // true or false
nativeImage.toDataURL([options])
将图像转换为数据 URL。
const { nativeImage } = require('electron');
const imagePath = '/path/to/image.png';
const image = nativeImage.createFromPath(imagePath);
const dataURL = image.toDataURL({ scaleFactor: 2.0 });
使用screen获取屏幕信息
screen 是 Electron 提供的模块之一,用于获取有关屏幕和显示器的信息,以及执行与屏幕相关的操作。以下是一些常用的 screen 模块方法和属性:
screen.getPrimaryDisplay():
获取主显示器的信息,包括位置、大小等。screen.getAllDisplays():
返回所有连接的显示器的信息,以数组形式返回。screen.getDisplayNearestPoint(point):
返回距离指定点最近的显示器的信息。screen.getDisplayMatching(rect):
返回与指定矩形相交的显示器的信息。screen.getCursorScreenPoint():
返回鼠标指针当前所在的屏幕坐标。screen.getMenuBarHeight():
返回菜单栏的高度。screen.getPrimaryDisplay().workArea:
返回主显示器工作区信息,即屏幕去除菜单栏等系统区域的区域。screen.getPrimaryDisplay().bounds:
返回主显示器整体信息,包括整个显示器的坐标和尺寸。screen.getPrimaryDisplay().size:
返回主显示器的分辨率,即屏幕的宽度和高度。screen.on(event, callback):
监听屏幕相关事件,例如 display-added、display-removed 等,允许你在显示器变化时进行相应的操作。 应用层预留接口打开devtool
在我们开发过程中,需要经常用的控制台,而我们在开发时直接打开控制台又有些不友好。还有生产环境中我们有时也需要打开控制台定位一些问题,但是生产环境又不能那么轻易让用户能打开控制台,因此我们可以在开发环境和生产环境分别预留一个接口打开控制台,生产环境的方式要复杂一些。实现思路
我们可以在应用层监听键盘事件,当在开发环境中,按下ctrl + F12
时,我们就打开控制台。而在生产环境
中我们需要设计的复杂一些,可以在代码中放一个不显示的输入框(设置宽高边框均为0,并且固定定位就好),当按下特殊组合键时,聚焦输入框,并输入openDevtool
之后打开控制台,我这里设置的组合键为ctrl + win + alt + F12
代码如下
import React, { useEffect, useRef, useCallback, ChangeEvent } from 'react'
import DesktopHeader from '@components/desktopHeader'
import './app.less'
import {
isDesktop,
getProcessNodeEnv,
ipcRendererSend
} from '@common/desktopUtils'
import electronImg from '@assets/electronImg.png'
function App() {
const openDevtoolInput = useRef(null)
const openDevtool = useCallback(() => {
ipcRendererSend('mainWindow-open-devtool')
}, [])
const openDevtoolInputChange = (event: ChangeEvent) => {
const { value } = event.target
if (value === 'openDevtool') {
openDevtool()
}
}
useEffect(() => {
document.addEventListener('keydown', (e) => {
const { ctrlKey, metaKey, altKey, key } = e
// 开发环境使用ctrl + F12打开控制台
if (getProcessNodeEnv() === 'development' && ctrlKey && key === 'F12') {
openDevtool()
}
// 开发环境使用ctrl + win + alt + F12,然后键入'open devtool'打开控制台
if (getProcessNodeEnv() === 'production' && ctrlKey && metaKey && altKey && key === 'F12') {
if (openDevtoolInput.current) {
openDevtoolInput.current.focus()
}
}
})
}, [openDevtool])
return (
{getProcessNodeEnv() === 'production' && (
{ e.target.value = '' }}
/>
)}
{isDesktop() && }
)
}
export default App
在主进程监听该事件并打开控制台
ipcMain.on('mainWindow-open-devtool', () => {
mainWindowIsExist() && mainWindow.webContents.openDevTools()
})