上一篇介绍了《Electron快速入手,拥有自己的第一个桌面应用 》,刚开始学习Electron的小伙伴可以先看下上一篇文章,方便快速入手。
但是,Electron的原理及一些其他功能开发,是如何实现的呢?
简介
Electron 基于 Chromium 和 Node.js, 让你可以使用 HTML, CSS 和 JavaScript 构建桌面端应用。
这篇文章摘录了我自己在真实项目中的要点
和问题
。
可能有的小伙伴会问,为什么不说一下 Vue、React 或者其他的库,如何与 Electron 结合开发?
这是因为 Vue 或者 React 等其他库都是 UI 页面,可以单独独立开发,只需要在代码打包压缩后,使用 BrowserWindow 中的 loadURL 或者 loadFile 加载即可。
Electron核心
Electron实现跨平台桌面应用程序的原理
通过集成浏览器内核,使用前端的技术来实现不同平台下的渲染,并结合了 Chromium
、Node.js
和用于调用系统本地功能的 API
3大板块:
- Chromium:提供强大的
UI
渲染能力,用于显示网页内容。 - Node.js:主要用于本地文件系统和操作系统,提供
GUI
的操作能力,如path、fs、crypto 等模块。 - Native API:为 Electron 提供原生系统的
GUI
支持,使用 Electron Api 可以调用原生应用程序接口。
Electron主要核心点
Electron其实很简单,基本都是api,我自己整理了主要核心点有进程间通信
、app
、BrowserWindow
、Application
4个方面:
进程间通信
:处理主进程与渲染进程的双向通信。app
:应用及应用生命周期。贯穿桌面端应用整个生命周期,虽然用的不多,但是很重要。BrowserWindow
:浏览器窗口。大家可以理解成每一个 BWindow 都是一个独立的浏览器,用于加载渲染我们的前端代码。Application
:应用程序功能点。为应用提供更佳完善的功能点,如程序坞、菜单......
从这4个主要核心点入手,开发人员会更快进入开发状态。
进程间通信
Electron 中有2个类型的进程,一种是主进程,另外一种是渲染进程。
而 ipcMain 代表了从主进程到渲染进程的异步通信; ipcRenderer 则代表了从渲染器进程到主进程的异步通信。
ipcMain与ipcRenderer
Electron 会存在多个进程,那么多进程间如何实现通信呢?
注意:不建议使用同步通信,这会对应用整体性能产生影响。
注意:进程通信所传参数只能为原始类型数据和可被 JSON.stringify 的引用类型数据。故不建议使用 JSON.stringify。
多进程间的通信,无论有多少个渲染进程,都遵循“主进程与渲染进程通信”。
渲染进程之间不能直接通信。
同时 Electron 提供了异步
、同步
、Promise
通信方式。
异步
通过 event.reply(...)
将异步消息发送回发送者。
event.reply 会自动处理从非主 frame 发送的消息,建议使用 event.sender.send
或者 win.webContents.send
把消息发送到主 frame。
主进程Main
const {
ipcMain } = require('electron')
// 接收renderer的数据
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // 传递的数据 ping
// event.reply('asynchronous-reply', 'pong') // 发送数据到渲染进程
event.sender.send('asynchronous-reply', 'pong')
})
渲染进程Renderer
const {
ipcRenderer } = require('electron')
// 接收main的数据
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg) // prints "pong"
})
// 发送数据到主进程
ipcRenderer.send('asynchronous-message', 'ping')
同步
也可以设置同步接收(不建议使用),通过 event.returnValue
设置
主进程main
ipcMain.on('synchronous-message', (event, arg) => {
event.returnValue = 'sync'
})
渲染进程renderer
const syncMsg = ipcRenderer.sendSync('synchronous-message', 'ping');
console.log(syncMsg)
ipcMain.once(channel, listener)
只监听实现一次。
ipcMain.removeListener(channel, listener)
删除某一指定的监听。
ipcMain.removeAllListeners([channel])
移除多个指定 channel 的监听器,无参移除所有。
Promise
进程发送消息,并异步等待结果
ipcMain 提供 handle,ipcRenderer 提供了 invoke。
// 渲染进程
ipcRenderer.invoke('some-name', someArgument).then((result) => {
// ...
})
// 主进程
ipcMain.handle('some-name', async (event, someArgument) => {
const result = await doSomeWork(someArgument)
return result
})
app
Electron 中 app 模块,是用来控制应用程序的事件生命周期,同时也提供了管理应用程序的事件方法。
注意: 某些方法仅在特定的操作系统上可用,如 app.hide() 只在 macos 可用。
const {
app, BrowserWindow} = require('electron')
const path = require('path')
function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
app的生命周期
可以让我们在每个事件生命周期做出相应的处理,如监听初始化完成后创建窗口、监听应用被激活后聚焦窗口等。
1、ready
应用完成初始化时触发,这个是重点
。因为Electron中很多都是需要初始化完成后,才可以执行,如 menu 、窗口等。
为避免出现错误,建议所有的操作都在ready之后
const {
app } = require('electron')
app.on('ready', () => {
// create menu
// create browserWindow
})
可以通过 app.isReady()
来检查该事件是否已被触发。
若希望通过Promise实现,使用 app.whenReady()
。
2、第二个实例创建
开发中需要特别注意第二个实例(即第二个应用)创建时的处理。
因为 Electron 在一些情况下会触发第二个实例的创建,如程序坞对激活的默认操作等。
所以在 Electron 的应用即将要运行第二个实例时,处理方案一般是聚焦到主窗口。
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
let win = mainWindow; // 获取到主窗口mainWindow
if (win) {
if (win.isMinimized()) {
// 窗口最小化
win.restore(); // 窗口从最小化状态恢复
}
if (!win.isVisible()) {
// 窗口不可见
win.show(); // 窗口显示
}
win.focus(); // 窗口聚焦
}
});
}
第二个实例执行时,会在第二个实例中触发 requestSingleInstanceLock
事件。
requestSingleInstanceLock 是用来判断当前应用程序实例是否成功取得了锁。如果它取得锁失败,代表此时另一个应用实例已经取得了锁并且仍旧在运行,当前的应用程序实例就需要立即退出。
而此时 second-instance
这个事件将在你的应用程序的首个实例中触发。
3、当应用被激活时(macos)
app.on('activate', (event, webContents, details) => {
// 聚焦到最近使用的窗口
});
触发此事件的情况很多:
首次启动应用程序、尝试在应用程序已运行时或单击应用程序的坞站
或任务栏图标
时重新激活它。
4、当所有窗口关闭,退出应用
const {
app } = require('electron')
app.on('window-all-closed', () => {
app.quit()
})
5、渲染进程进程奔溃
app.on('render-process-gone', (event, webContents, details) => {
// 定位原因,可通过 log 记录;也可以做重启 retry 操作,切记限制count
// webContents.reloadIgnoringCache(); // 忽略缓存强制刷新页面
});
details
Object
reason
string - 渲染进程消失的原因。 可选值:clean-exit
- 以零为退出代码退出的进程abnormal-exit
- 以非零退出代码退出的进程killed
- 进程发送一个 SIGTERM,否则是被外部杀死的。crashed
- 进程崩溃oom
- 进程内存不足launch-failed
- 进程从未成功启动integrity-failure
- 窗口代码完整性检查失败
exitCode
Integer - 进程的退出代码,除非在reason
是launch-failed
的情况下,exitCode
将是一个平台特定的启动失败错误代码。
6、应用退出
app.on('will-quit', (event, webContents, details) => {
// timer 或者 关闭第三方的进程
});
app常用事件方法
通过操作 app 应用程序的事件方法,来控制应用程序,如重启、关闭等。
const {
app } = require('electron')
app.relaunch({
args: process.argv.slice(1).concat(['--relaunch']) })
app.exit(0)
1、重启应用
app.relaunch([options])
relaunch 被多次调用时,多个实例将会在当前实例退出后启动。
2、所有窗口关闭
app.exit(0)
所有窗口都将立即被关闭,而不询问用户
3、当前应用程序目录
app.getAppPath()
可以理解成获取应用启动(代码)目录
4、设置 "关于" 面板选项
app.setAboutPanelOptions({
applicationName: 'demo',
applicationVersion: '0.0.1',
version: '0.0.1'
});
5、注意,部分事件存在着系统的区别
// 只适用于 macOS
app.hide() // 隐藏所有的应用窗口,不是最小化.
BrowserWindow
const {
BrowserWindow } = require('electron')
const win = new BrowserWindow({
width: 800, height: 600 })
// 加载地址
win.loadURL('https://github.com')
// 加载文件
win.loadFile('index.html')
自定义窗口
可以通过自定窗口 options,实现自定义样式,如桌面窗口页面、桌面通知、模态框。。。
const options = {
width: 800,
height: 600
}
new BrowserWindow(options)
父子窗口
拖动父窗口,子窗口跟随父窗口而动。
const top = new BrowserWindow()
const child = new BrowserWindow({
parent: top })
注意:child
窗口将总是显示在 top
窗口的顶部。
win.setParentWindow(parent)
给窗口设置父窗口,null取消父窗口win.getParentWindow()
获取窗口的父窗口win.getChildWindows()
获取所有子窗口
显示与隐藏
窗口可以直接展示也可以延后展示。但每次执行 show
,都会将当前桌面
的焦点聚焦到执行show的窗口上。
const win = new BrowserWindow({
show: true,
})
在加载页面时,渲染进程第一次完成绘制时,如果窗口还没有被显示,渲染进程会发出 ready-to-show
事件。在此事件后显示窗口将没有视觉闪烁:
const win = new BrowserWindow({
show: false })
win.once('ready-to-show', () => {
win.show()
})
win.hide()
窗口隐藏,焦点消失
win.show()
窗口显示,获取焦点
win.showInactive()
窗口显示,但不聚焦于窗口(多用于当其他窗口需要显示时,但是不想中断当前窗口的操作)
win.isVisible()
判断窗口是否显示
Bounds
1、设置窗口的size、position
setBounds(bounds[, animate])
:同时设置 size、position,但是同时也会重置窗口。
setSize(width, height[, animate])
: 调整窗口的宽度和高度。
setPosition(x, y[, animate])
:将窗口移动到x 和 y。
注意:animate只在macOS上才会生效。
// 设置 bounds 边界属性
win.setBounds({
x: 440, y: 225, width: 800, height: 600 })
// 设置单一 bounds 边界属性
win.setBounds({
width: 100 })
win.setSize(800, 600)
win.setPosition(440, 225)
2、获取窗口size、position
getBounds()
获取窗口的边界信息。
getSize()
获取窗口的宽度和高度。
getPosition()
返回一个包含当前窗口位置的数组
center()
将窗口移动到屏幕中央(常用)。
3、常见问题
win.setSize
如果 width 或 height 低于任何设定的最小尺寸约束,窗口将对齐到约束的最小尺寸。
win.setPosition
有的电脑机型存在兼容问题,执行一次win.setPosition(x,y)不会生效,需要执行两次。
Application
应用包含了很多程序工具,如 Menu、Tray...
Menu
创建原生应用菜单和上下文菜单。
在mac中,菜单展示在应用内;而 windows 与 linux,菜单则会展示在各个窗口的顶部。
可以对某一窗口单独设置或者删除Menu,但是这只是针对 windows、linux 生效。
win.setMenu(menu)
设置为窗口的菜单栏 menu(只对 windows、linux 生效)。
win.removeMenu()
删除窗口的菜单栏(只对 windows、linux 生效)。
如何全局设置Menu
const {
Menu } = require('electron')
const template = [
{
label: 'Electron',
submenu: [
{
role: 'about', label: '关于' },
{
type: 'separator' },
{
role: 'services', label: '偏好设置' },
{
type: 'separator' },
{
role: 'hide', label: '隐藏' },
{
role: 'hideOthers', label: '隐藏其他' },
{
type: 'separator' },
{
role: 'quit', label: '退出' }
]
},
{
label: '编辑',
submenu: [
{
role: 'undo', label: '撤销' },
{
type: 'separator' },
{
role: 'menu_copy', label: '复制' },
{
role: 'menu_paste', label: '粘贴' }
]
},
{
label: '窗口',
submenu: [
{
role: 'minimize',
label: '最小化',
click: function (event, focusedWindow, focusedWebContents) {
}
},
{
role: 'close', label: '关闭' },
{
role: 'togglefullscreen', label: '全屏', accelerator: 'Cmd+,OrCtrl+,'}
]
}];
let menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
role:可以理解为官方命名好的指令,详见官方menuitemrole
label:我们对指令自定义的展示文字。
click:触发该指令的接受的函数
Tray
添加图标和上下文菜单到系统通知区
const {
ipcMain, app, Menu, Tray} = require('electron')
const iconPath = path.join(__dirname, './iconTemplate.png')
const tray = new Tray(iconPath)
const contextMenu = Menu.buildFromTemplate([{
label: 'tray 1',
click: () => {
}
}, {
label: 'tray 2',
click: () => {
}
}])
tray.setToolTip('Electron Demo in the tray.')
tray.setContextMenu(contextMenu)
setToolTip
设置鼠标指针在托盘图标上悬停时显示的文本
setContextMenu
设置图标的内容菜单(支持动态添加多个内容)
dialog
系统对话框
1.支持多选、默认路径
const {
dialog } = require('electron')
const options = {
title: '标题',
defaultPath: '默认地址',
properties: [
openFile, // 容许选择文件
openDirectory, // 容许选择目录
multiSelections, // 容许多选
showHiddenFiles, // 显示隐藏文件
createDirectory, // 创建新的文件,只在mac生效
promptToCreate,// 文件目录不存在,生成新文件夹,只在windows生效
]
}
dialog.showOpenDialog(win, options); // win是窗口
2.支持过滤文件
过滤文件后缀名 gif 的文件,显示所有文件用 * 代替
options.filters = [
{
name: 'Images', extensions: ['gif'] }
]
globalShortcut
键盘事件
需要先注册globalShortcut.register(accelerator, callback)
const {
globalShortcut } = require('electron')
globalShortcut.register('CommandOrControl+F', () => {
// 注册键盘事件是全局性质的,各个窗口都可以触发
})
globalShortcut.register(accelerator, callback)
注册全局快捷键globalShortcut.isRegistered(accelerator)
判断是否注册globalShortcut.unregister(accelerator)
取消注册
注意:应用程序退出时,注销键盘事件
app.on('will-quit', () => {
globalShortcut.unregisterAll()
})
Notification
系统通知
这个受限于当前系统是否支持
桌面通知,在 mac 或 windows 电脑的设置中,需特别注意是否容许通知。
const {
Notification } = require('electron');
const isAllowed = Notification.isSupported();
if (isAllowed) {
const options = {
title: '标题',
body: '正文文本,显示在标题下方',
silent: true, // 系统默认的通知声音
icon: '', // 通知图标
}
const notification = new Notification(argConig);
notification.on('click', () => {
});
notification.on('show', () => {
});
notification.on('close', () => {
});
notification.show();
}
notification.close()
关闭通知
session
管理浏览器会话、cookie、缓存、代理设置等。
1、全局
const {
session, BrowserWindow } = require('electron')
// 拦截下载
session.defaultSession.on('will-download', (event, item, webContents) => {
event.preventDefault() // 阻止默认行为下载或做指定目录下载
})
2、单独窗口
const win = new BrowserWindow({
width: 800, height: 600 })
win.loadURL('http://github.com')
const ses = win.webContents.session
ses.setProxy(config)
:设置代理
ses.cookies
:设置cookies或获取cookies
3、浏览器UserAgent
设置 UserAgent 可以通过 app.userAgentFallback 全局设置,也可以通过ses.setUserAgent
设置。
screen
检索有关屏幕大小、显示器、光标位置等的信息。
const primaryDisplay = screen.getPrimaryDisplay() // 获取光标所在屏幕的屏幕信息
const {
width, height } = primaryDisplay.workAreaSize // 获取光标下的屏幕尺寸
const allDisplay = screen.getAllDisplays() // 返回数组,所有的屏幕
screen.getPrimaryDisplay()
返回主窗口 Display
screen.getAllDisplays()
返回所有的窗口 Display[] 数组
screen.getDisplayNearestPoint
离光标最近的窗口
Node
项目中会用到 Node.js,下面是我整理的常用方法。
fs
本地文件读写
读取目录的内容
fs.readdirSync(path[, options])
fs.readdir(path[, options])
读取文件的内容
fs.readFileSync(path[, options])
fs.readFile(path[, options])
文件的信息
const stats = fs.statSync(path);
stats.isDirectory() // 是否为系统目录
stats.isFile() // 是否为文件
stats.size // 文件大小
。。。
路径是否存在
fs.existsSync(path)
写入文件
当 file
是文件名时,将数据写入文件,如果文件已存在则替换该文件。
fs.writeFile(file, data[, options], callback)
fs.writeFileSync(file, data[, options], callback)
移除文件
fs.rmSync(path[, options])
fs.rmdirSync(path[, options])
更改文件的权限
fs.chmod(path, mode, callback)
fs.chmodSync(path, mode, callback)
注意:mode是8进制,可通过parseInt(mode, 8)转化
修改文件夹名
fs.renameSync
拷贝文件到指定地址
fs.copySync
path
拼接绝对路径
path.resolve([...paths])
拼接路径
path.join([...paths])
路径文件夹名字
path.dirname(path)
获取文件名
path.basename(path)
都是属于 Nodejs,整理到最后懒得整理了~~,小伙伴们想研究的话,具体看Node.js
生成安装包
Electron打包生成跨平台软件,见我的另一篇文章
使用Electron生成安装包并打包为不同平台的应用格式。
存在的问题
Electron还是会存在部分坑~~
我整理了一些,已经添加到我个人的Electron专题集,小伙伴们可以借鉴一下。
我也会持续更新。。。
有些问题我可能遇到没写,或者我写的是错误的,欢迎大家积极指正沟通,共同学习,共同进步~~。
结语
感谢您的阅读!希望本文带给您有价值的信息。
如果对您有帮助,请「点赞」支持,并「关注」我的主页获取更多后续相关文章。同时,也欢迎「收藏」本文,方便以后查阅。
写作不易,我会继续努力,提供有意义的内容。感谢您的支持和关注!