Electron基础篇

Electron官网地址

Electron简介

Electron是一个使用HTML、css和JavaScript构建跨平台桌面应用程序的框架

前端技术

Electron嵌入了Chromium和Node.js,使web开发人员能够创建桌面应用程序。

跨平台

Electron应用程序与macOS,Windows和Linux兼容,可在所有支持的架构的三个平台上运行。

开源

Electron是一个开源项目,由OpenJS基金会和一个活跃的贡献者社区维护。

开发环境

  • Node.js
  • nvm
    • 查看本机已安装版本列表: nvm list
    • 查看当前可用版本列表: nvm list available
    • 安装指定版本: nvm install <版本号>
    • 切换指定版本: nvm use <版本>
    • 卸载指定版本: nvm uninstall <版本号>

更多详情:point_right:

构建Electron工程

方法一:Quick Start

官网推荐

  1. 将项目克隆到本地

image-20240705175003692

  1. 安装依赖:npm i
  2. 启动程序:npm run start

image-20240705180335937

方法二:Electron Forge

npm方式——脚手架

  • npm init electron-app <项目名>
  • npx create-electron-app <项目名>
  • npm run start

方法三:手动创建

  1. 初始化项目:
    • npm init
    • yarn init
  2. 安装依赖:
    • npm install electron --save-dev
    • yarn add electron –dev
  3. 创建页面:index.html
  4. 程序入口:main.js
  5. 启动应用:npm run start

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//用于控制应用程序寿命和创建本机浏览器窗口的模块
const { app, BrowserWindow } = require('electron')
const path = require('node:path')

function createWindow () {
// 创建浏览器窗口。
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

// 并加载应用程序的index.html。
mainWindow.loadFile('index.html')

// 打开 DevTools。
// mainWindow.webContents.openDevTools()
}

// 当 Electron 完成时,将调用此方法
// 初始化并准备好创建浏览器窗口。
// 某些 API 只能在此事件发生后使用。
app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
// 在 macOS 上,当单击停靠图标,并且没有打开其他窗口。
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

// 当所有窗口都关闭时退出,但在 macOS 上除外。在那里,这很常见
// 用于应用程序及其菜单栏保持活动状态,直到用户退出
// 明确使用 Cmd + Q。
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

// 在此文件中,您可以包含应用的特定主进程的其余部分
// 法典。您也可以将它们放在单独的文件中,并在此处需要它们。

image-20240705182014525

解决警告问题

1
<meta http-equiv="Content-Security-width, initial-scale=1.0">

进程模型

Electron 继承了来自 Chromium 的多进程架构,这使得此框架在架构上非常相似于一个现代的网页浏览器。

为什么不是一个单一的进程

网页浏览器是个极其复杂的应用程序。 除了显示网页内容的主要能力之外,他们还有许多次要的职责,例如:管理众多窗口 ( 或 标签页 ) 和加载第三方扩展。

在早期,浏览器通常使用单个进程来处理所有这些功能。 虽然这种模式意味着您打开每个标签页的开销较少,但也同时意味着一个网站的崩溃或无响应会影响到整个浏览器。

多进程模型

为了解决这个问题,Chrome 团队决定让每个标签页在自己的进程中渲染, 从而限制了一个网页上的有误或恶意代码可能导致的对整个应用程序造成的伤害。 然后用单个浏览器进程控制这些标签页进程,以及整个应用程序的生命周期。 下方来自 Chrome 漫画 的图表可视化了此模型:

Chrome的多进程架构

Electron 应用程序的结构非常相似。 作为应用开发者,你将控制两种类型的进程:主进程渲染器进程。 这类似于上文所述的 Chrome 的浏览器和渲染器进程。

主进程 Main Process

  • 特点:程序入口、唯一
  • 职责:负责控制、协调、Node.js API
  • 事件:创建窗口、设置菜单、注册快捷键等系统级
  • 销毁:应用结束

渲染进程 Renderer Process

  • 特点:与BrowserWindow对应、彼此隔离
  • 渲染页面、无权访问Node.js API
  • 事件:窗口的显示、隐藏等
  • 销毁:窗口销毁

预加载脚本

预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。

预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。

main.js

1
2
3
4
5
6
7
8
9
10
11
const { BrowserWindow } = require('electron')
// ...
// 创建浏览器窗口。
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
// ...

因为预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用。

虽然预加载脚本与其所附着的渲染器在共享着一个全局 window 对象,但您并不能从中直接附加任何变动到 window 之上,因为 contextIsolation 是默认的。

preload.js

1
2
3
window.myAPI = {
desktop: true
}

上下文隔离

上下文隔离功能将确保您的 预加载脚本 和 Electron的内部逻辑 运行在所加载的 webcontent网页 之外的另一个独立的上下文环境里。 这对安全性很重要,因为它有助于阻止网站访问 Electron 的内部组件 和 您的预加载脚本可访问的高等级权限的API 。

这意味着,实际上,您的预加载脚本访问的 window 对象并不是网站所能访问的对象。 例如,如果您在预加载脚本中设置 window.hello = 'wave' 并且启用了上下文隔离,当网站尝试访问window.hello对象时将返回 undefined。

上下文隔离默认开启。关闭上下文隔离,但是存在安全风险

1
2
3
4
5
6
7
8
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: false, // 不使用上下文隔离
}
})

:star:Electron 提供一种专门的模块来无阻地帮助您完成这项工作。 contextBridge 模块可以用来安全地从独立运行、上下文隔离的预加载脚本中暴露 API 给正在运行的渲染进程。 API 还可以像以前一样,从 window.myAPI 网站上访问。

preload.js

1
2
3
4
5
6
// 在上下文隔离启用的情况下使用预加载
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
doAThing: () => {}
})

进程通信

进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。 由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。

IPC通道

在 Electron 中,进程使用 ipcMainipcRenderer 模块,通过开发人员定义的“通道”传递消息来进行通信。 这些通道是 任意 (可以随意命名它们)和 双向 (可以在两个模块中使用相同的通道名称)的。

单向通信

  • 渲染进程给主进程发送事件:ipcRenderer.send('channel', args)
  • 主进程监听渲染进程的事件:ipcMain.on('channel', (event,args)=> {})

案例:修改标题

preload.js

1
2
3
4
5
6
7
8
// 引入预加载脚本
const {contextBridge, ipcRenderer} = require('electron')
// 暴露一个方法给主进程
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => {
ipcRenderer.send('set-title', title)
}
})

index.html

1
2
3
4
5
6
7
8
9
10
<input type="text" id="input">
<button id="btn">点击修改标题</button>
<script>
const btn = document.getElementById('btn');
const input = document.getElementById('input');
btn.addEventListener('click', () => {
const title = input.value;
window.electronAPI.setTitle(title);
});
</script>

main.js

1
2
3
ipcMain.on('set-title', (event, title) => {
BrowserWindow.getFocusedWindow().setTitle(title)
})

页面调用预加载的事件,传递当前输入框的内容作为标题的参数。然后有preload.js将信息(标题)传给主进程,主进程修改当前页面的标题。

案例2:页面的缩放

BrownserWindow API

  • 最小化:win.minimize()
  • 最大化: win.maximize()
  • 还原:win.unmaximize()
  • 关闭:win.close()

无边框:

1
2
3
4
5
6
7
8
9
10
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
++ frame: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
contextIsolation: true
}
})

可拖动 CSS

  • -webkit-app-region: drag;
  • -webkit-app-region: no-drag;

双向通信

  • ipcRenderer.invoke(‘channel’,…args)//进程通信桥梁
  • ipcMain.handle(‘channel’,listener)//主进程监听渲染进程的事件

案例:打开文件

preload.js

1
2
3
4
5
6
7
8
// 引入预加载脚本
const {contextBridge, ipcRenderer} = require('electron')
// 暴露一个方法给主进程
contextBridge.exposeInMainWorld('electronAPI', {
invoke: (channel, ...args) => {
return ipcRenderer.invoke(channel, ...args)
}
})

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 处理来自渲染进程的'dialog:openFile'事件。
*
* 此函数用于在主进程侧响应渲染进程发起的打开文件对话框请求。它通过dialog.showOpenDialog显示对话框,
* 并返回用户选择的文件路径。如果用户取消了对话框,则返回undefined。
*
* @returns {Promise<string | undefined>} 用户选择的文件路径,如果取消则为undefined。
*/
ipcMain.handle('dialog:openFile', async () => {
// 显示打开文件对话框,并等待用户选择文件
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
properties: ['openFile']
})

// 根据对话框是否被取消,返回用户选择的文件路径或undefined
return canceled ? undefined : filePaths[0]
})

index.html

1
2
3
4
btn2.addEventListener('click', async () => {
const filePaths =await window.electronAPI.invoke('dialog:openFile');
console.log(filePaths);
});

Dark Mode主题切换

nativeTheme API(本地主题API)

  • themeSource // 当前的主题是什么?’dark’,’light’,’system’
  • shouldUseDarkColors // 当前是否使用深色主题

CSS媒体查询

  • @media(prefers-color-scheme:dark){}
  • @media(prefers-color-scheme: light){}

preload.js

1
2
3
4
5
6
7
8
// 引入预加载脚本
const {contextBridge, ipcRenderer} = require('electron')
// 暴露一个方法给主进程
contextBridge.exposeInMainWorld('electronAPI', {
invoke: (channel) => {
return ipcRenderer.invoke(channel)
}
})

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const {app, BrowserWindow, ipcMain,nativeTheme} = require('electron')
// 改变主题
ipcMain.handle('native-theme', async () => {
if (nativeTheme.shouldUseDarkColors){
nativeTheme.themeSource = 'light'
}else{
nativeTheme.themeSource = 'dark'
}
return nativeTheme.themeSource
})
// 系统主题
ipcMain.handle('native-theme-system', async (event, theme) => {
nativeTheme.themeSource = 'system'
return 'system'
})

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<h1>主题切换</h1>
<div>
<button id="toggle">切换</button>
<button id="system">跟随主题</button>
</div>
<script>
const toggle = document.getElementById('toggle')
const system = document.getElementById('system')
// 监听事件, 切换主题
toggle.addEventListener('click',async() => {
const res = await window.electronAPI.invoke('native-theme')
console.log(res)
})
// 监听事件, 跟随系统主题
system.addEventListener('click', async () => {
const res = await window.electronAPI.invoke('native-theme-system');
console.log(res)
})
</script>
</body>

style.css

1
2
3
4
5
6
7
8
9
10
11
12
@media(prefers-color-scheme: dark){
body{
background: #333;
color: #fff;
}
}
@media(prefers-color-scheme: light){
body{
background: #fff;
color: #333;
}
}

:point_right: 官网文档

应用菜单创建

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const {app, BrowserWindow, Menu, nativeTheme} = require('electron');
// 创建菜单
const createMenuApplication = () => {
const template = [
// 菜单标题
{label: '首页',},
{
label: '分类',
// 子菜单
submenu: [
{label: '系统',},
{label: '生活',},
{type: 'separator'},
{label: '技术',},
],
},
{
label: '主题切换',
submenu: [
{
label: '暗黑',
checked: nativeTheme.shouldUseDarkColors,
click: () => nativeTheme.themeSource = 'dark'
},
{
label: '浅色',
checked: !nativeTheme.shouldUseDarkColors,
click: () => nativeTheme.themeSource = 'light'
}
]
}
]
// 创建应用级菜单
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu)
}

// 当 Electron 完成时,将调用此方法
// 初始化并准备好创建浏览器窗口。
// 某些 API 只能在此事件发生后使用。
app.whenReady().then(() => {
// 创建浏览器窗口
createWindow();
// 创建菜单
createMenuApplication();
}

效果:

image-20240706141658781

右键菜单

preload.js

1
2
3
4
5
6
7
8
9
10
const {contextBridge, ipcRenderer} = require('electron')
// 暴露给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel,args) => {
ipcRenderer.send(channel, args)
},
invoke: (channel, ...args) => {
return ipcRenderer.invoke(channel, args)
}
})

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<body>
<aside>侧边栏</aside>
<main>主体</main>

<script>
const aside = document.querySelector('aside');
const main = document.querySelector('main');

aside.addEventListener('contextmenu', (event) => {
event.preventDefault();
console.log('侧边栏右键菜单');
window.electronAPI.send('contextmenu', [{
label: '分类',
// 子菜单
submenu: [
{label: '系统',},
{label: '生活',},
{type: 'separator'},
{label: '技术',},
],
},])
})

main.addEventListener('contextmenu', (event) => {
event.preventDefault();
console.log('主体右键菜单');
window.electronAPI.send('contextmenu', [{
label: '感情',
// 子菜单
submenu: [
{label: '系统',},
{label: '生活',},
{type: 'separator'},
{label: '技术',},
],
},])
})
</script>
</body>

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const {app, BrowserWindow, Menu, nativeTheme,ipcMain} = require('electron');
/**
* 监听ipcMain的'contextmenu'事件,用于处理右键菜单的逻辑。
* 当右键菜单被触发时,此函数将构建一个自定义的菜单并显示在相应的窗口中。
*
* @param {Event} event Electron的事件对象,用于传递相关数据。
* @param {Object[]} arg 右键菜单的初始项数组,来自前端的请求。
*/
ipcMain.on('contextmenu',(event,arg)=>{
// 构建菜单模板,融合传入的参数和固定的菜单项。
const template = [
...arg,
// 添加一个固定项:'首页'菜单项。
// 菜单标题
{label: '首页',},

]
// 根据菜单模板创建实际的菜单对象。
// 创建应用级菜单
const menu = Menu.buildFromTemplate(template);
// 从事件对象中获取发送请求的Web内容对应的BrowserWindow对象。
const win = BrowserWindow.fromWebContents(event.sender)
// 在对应的窗口中弹出构建好的菜单。
menu.popup({window:win})
})

image-20240706153353062

Tray 托盘

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const {app, BrowserWindow, Menu, nativeTheme, ipcMain, Tray, nativeImage} = require('electron');


let mainWindow = null;
// 创建浏览器窗口。
const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});

// 并加载应用程序的index.html。
mainWindow.loadFile(path.join(__dirname, 'index.html'));
// 窗口关闭事件
mainWindow.on('close', (event) => {
// 阻止默认事件——彻底关闭该进程
event.preventDefault();
mainWindow.hide();
});
// 窗口关闭事件
mainWindow.on('closed', () => {
mainWindow = null;
})
// Open the DevTools.
// mainWindow.webContents.openDevTools();
};

/**
* 创建系统托盘图标并设置其相关属性。
* 托盘图标具有一个上下文菜单,包含打开主窗口和退出应用程序的选项。
*
* @remarks
* 此函数不接受任何参数。
* 创建的托盘图标和上下文菜单与主应用程序窗口和应用程序本身的行为直接相关。
*/
function createTray() {
// 构建上下文菜单的模板,包含两个菜单项:打开主窗口和退出。
const contextMenu = Menu.buildFromTemplate([
{label: '打开主窗口', click: () => {
// 点击“打开主窗口”菜单项时,显示主应用程序窗口。
mainWindow.show()
}},
{type: 'separator'}, // 分隔菜单项。
{label: '退出', click: () => {
// 点击“退出”菜单项时,销毁主应用程序窗口并退出应用程序。
mainWindow.destroy();
app.quit()
}}
])
// 从路径创建托盘图标的图像。
const icon = nativeImage.createFromPath('src/assets/tray.jpg');
// 创建托盘图标实例。
const tray = new Tray(icon)
// 设置托盘图标的工具提示文本。
tray.setToolTip('这是我的应用程序.')
// 设置托盘图标的标题。
tray.setTitle('这是标题')
// 设置托盘图标的上下文菜单。
tray.setContextMenu(contextMenu)
}

效果:

image-20240706154833627

Notification 通知

:point_right: 官方文档

主进程中创建通知

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const {app, BrowserWindow, Menu,
nativeTheme, ipcMain, Tray, nativeImage,++ Notification} = require('electron');


/**
* 创建一个系统通知。
*
* 该函数通过Notification类创建一个系统通知对象,并配置其标题、正文和图标。
* 当通知被点击时,会触发点击事件的回调函数。
*/
function createNotification() {
// 创建一个通知实例,配置通知的标题、正文和图标
const notification = new Notification({
title: '通知标题',
body: '通知内容',
icon: 'src/assets/tray.jpg',
})

// 注册通知点击事件的处理函数
notification.on('click', () => {
console.log('通知被点击')
})

// 显示通知
notification.show()
}
app.whenReady().then(() => {
// 创建浏览器窗口
createWindow();
// 创建菜单
// createMenuApplication();
// 创建托盘
createTray()
// 创建通知
++ createNotification()
}

在渲染进程中创建

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
document.getElementById('btn').addEventListener('click', () => {
const options = {
title: '发现新版本',
body: '发现新版本,是否更新?',
icon: './assets/tray.jpg',
}
const notify = new window.Notification('发现新版本', options)
notify.onclick = () => {
window.open('https://bilibili.com')
}
})
</script>

快捷键

:point_right:官网文档

渲染进程创建局部快捷键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script> 
window.onkeydown = (event) => {
event.preventDefault();
const {key, ctrlKey, shiftKey, altKey, metaKey} = event;
if (!(ctrlKey||shiftKey)){
return;
}
if (key === 's'){
console.info('保存')
}
if (key === 'z'){
console.info('撤销')
}
}
</script>

主进程创建全局快捷键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const {app, BrowserWindow, Menu,
nativeTheme, ipcMain, Tray,
nativeImage,Notification,
++ globalShortcut
} = require('electron');

app.whenReady().then(() => {
// 创建浏览器窗口
createWindow();
// 创建菜单
createMenuApplication();
// 创建托盘
createTray()
// 创建通知
createNotification();
// 注册快捷键
++ registerShortcuts();})
/**
* 注册全局快捷键。
* 本函数旨在通过监听特定的键盘快捷键,实现一些特定的功能,如打开开发者工具或关闭窗口。
* 使用Electron的globalShortcut模块来实现这一功能。
*
* @remarks
* - 'CommandOrControl+Shift+I' 快捷键用于打开开发者工具。
* - 'CommandOrControl+W' 快捷键用于关闭当前窗口。
*/
function registerShortcuts() {
// 注册快捷键CommandOrControl+Shift+I,用于打开开发者工具
globalShortcut.register('CommandOrControl+Shift+I', () => {
BrowserWindow.getFocusedWindow().webContents.openDevTools();
});

// 注册快捷键CommandOrControl+W,用于关闭当前窗口
globalShortcut.register('CommandOrControl+W', () => {
BrowserWindow.getFocusedWindow().close();
});
}

同系统的快捷键的显示方式

image-20240706172353156

关于 “注销全局快捷键” 的方式如下所示:

1
2
3
app.on("will-quit", () => {
globalShortcut.unregisterAll();
});

dialog 对话框

官方文档

Electron dialog API

  • showMessageBox
  • showErrorBox
  • showOpenDialog
  • showSaveDialog

Node.js fs API

  • readFile
  • writeFile

消息提示

  • showMessageBox
  • showErrorBox

preload.js

1
2
3
4
5
6
7
8
9
10
const {contextBridge, ipcRenderer} = require('electron')
// 暴露给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel,args) => {
ipcRenderer.send(channel, args)
},
invoke: (channel, args) => {
return ipcRenderer.invoke(channel, args)
}
})

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
const message = document.getElementById('message');
message.addEventListener('click', async () => {
const option = {
type: 'error',
title: '标题',
message: '提示信息',
detail: '内容',
checkboxLabel: '记住我',
buttons: ['OK', 'Cancel'],
}
const result = await window.electronAPI.invoke('dialog:message-box', option)
console.log(result)
})
</script>

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 处理来自渲染进程的对话框消息请求。
*
* 此函数用于响应渲染进程(Web页面)发送的请求,以显示各种类型的对话框(如消息框、保存文件对话框等)。
* 它通过ipcMain模块的事件监听器来接收请求,并使用dialog模块的功能来显示对话框。
*
* @param {Event} event IPC事件对象,用于传递对话框选项和接收对话框的返回值。
* @param {Object} options 对话框的配置选项,如消息文本、按钮标签、对话框类型等。
* @returns {Number} 对话框的返回值,通常是一个表示用户点击的按钮的索引。
*/
ipcMain.handle('dialog:message-box', async (event, options) => {
// 获取发送请求的Web内容对应的BrowserWindow对象
const win = BrowserWindow.fromWebContents(event.sender)
// 显示同步消息框,并返回用户的选择
return dialog.showMessageBox(win, options)
})

image-20240707195233822

选择文件

  • showOpenDialog

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
// 打开文件,将文件内容写入textarea
const fileBtn = document.getElementById('file-btn');
fileBtn.addEventListener('click', async () => {
const options = {
title: '打开文件',
// defaultPath: 'D:\\',
buttonLabel: '就你了',
filters: [
{name: 'Images', extensions: ['jpg', 'png', 'gif']},
{name: 'Movies', extensions: ['mkv', 'avi', 'mp4']},
{name: 'Custom File Type', extensions: ['as']},
{name: 'All Files', extensions: ['*']},
],
// properties: ['openFile', 'openDirectory', 'multiSelections'],
properties: ['openFile'],
}
const result = await window.electronAPI.invoke('dialog:open-file', options)
if (result.canceled) {
return;
}
console.log(result)
// 将读取的文件的info写入textarea
textarea.value = await window.electronAPI.invoke('dialog:open-file-content', result.filePaths[0])
})
</script>

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 处理来自渲染进程的打开文件对话框请求。
*
* 此函数被设计为在主进程侧响应渲染进程通过IPC(Inter-Process Communication)发送的请求,
* 具体来说,当渲染进程需要打开一个文件对话框以允许用户选择文件时,它会触发这个事件。
* 使用Electron的dialog模块来显示操作系统原生的文件选择对话框,并返回用户选择的结果。
*
* @param {Electron.IpcMainEvent} event - IPC事件对象,用于接收和发送消息。
* @param {Object} options - 显示对话框时的选项,例如文件类型过滤器等。
* @returns {Promise<Array<string>>} - 返回一个Promise,解析为用户选择的文件路径数组。
*/
ipcMain.handle('dialog:open-file', async (event, options) => {
// 从事件对象中获取发送请求的渲染进程对应的浏览器窗口对象。
const win = BrowserWindow.fromWebContents(event.sender)
// 在找到的浏览器窗口上显示打开文件对话框,并返回对话框的结果。
return dialog.showOpenDialog(win, options)
})


/**
* 处理来自渲染进程的'dialog:open-file-content'事件。
*
* 此函数被设计为在主进程内使用,通过IPC(Inter-Process Communication)机制与渲染进程通信。
* 当渲染进程发送'dialog:open-file-content'事件时,此函数会被调用,用于读取指定文件的内容。
*
* @param {Electron.IpcMainEvent} event - IPC事件对象,用于与发送方通信。
* @param {string} filePath - 要读取的文件的路径。
* @returns {Promise<string>} - 返回一个Promise,解析为指定文件的内容,以字符串形式。
*/
ipcMain.handle('dialog:open-file-content', async (event, filePath) => {
// 使用Node.js的fs模块异步读取文件内容。
return fs.promises.readFile(filePath, 'utf-8')
})
  • showSaveDialog

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script>
const fileBtnSave = document.getElementById('file-btn-save');
// 将文本域的内容写入文件
fileBtnSave.addEventListener('click', async () => {
const options = {
title: '保存文件',
// defaultPath: 'D:\\',
buttonLabel: '保它',
filters: [
{name: 'Images', extensions: ['jpg', 'png', 'gif']},
{name: 'Movies', extensions: ['mkv', 'avi', 'mp4']},
{name: 'Custom File Type', extensions: ['as']},
{name: 'All Files', extensions: ['*']},
]
}
const result = await window.electronAPI.invoke('dialog:save-file', options)
if (result.canceled) {
return;
}
try{
await window.electronAPI.invoke('dialog:save-file-content', {filePath:result.filePath, content: textarea.value})
await window.electronAPI.invoke('dialog:message-box', {
type: 'info',
title: '保存成功',
message: '保存成功',
detail: '保存成功',
buttons: ['OK'],
})
}catch (e){
console.log(e)
}
})
</script>

main.js

1
2
3
4
5
6
7
8
ipcMain.handle('dialog:save-file', async (event, options) => {
const win = BrowserWindow.fromWebContents(event.sender)
return dialog.showSaveDialog(win, options)
})

ipcMain.handle('dialog:save-file-content', async (event, {filePath, content}) => {
return fs.promises.writeFile(filePath, content)
})

online 在线状态

官方文档

在线/离线 状态探测

index.html

1
2
3
4
5
6
7
8
9
10
<script>
const onlineStatus = document.getElementById('online-status');
const refresh = async () => {
onlineStatus.innerText = navigator.onLine ? '在线' : '离线';
await window.electronAPI.send('online-status:change', navigator.onLine)
}
refresh();
window.addEventListener('online',refresh)
window.addEventListener('offline',refresh)
</script>

动态更改托盘图标

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const onlineIcon = nativeImage.createFromPath('src/assets/online.jpeg');
const offlineIcon = nativeImage.createFromPath('src/assets/offline.jpeg');
let tray = null;
function createTray() {
tray = new Tray(onlineIcon)
}
// 监测当前的在线状态
ipcMain.on('online-status:change', (event, status) => {

// 在线状态,则为在线图标,否则为离线图标
// if (status) {
// tray.setImage(onlineIcon)
// } else {
// tray.setImage(offlineIcon)
// }
// 图标闪烁的效果
let flag = true;
setInterval(() => {
flag ? tray.setImage(onlineIcon) : tray.setImage(nativeImage.createEmpty());
flag = !flag;
},500)
})

多窗口

加载窗口

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let loadingWindow = null
const createLoadingWindow =() => {
loadingWindow = new BrowserWindow({
width: 300,
height: 300,
frame: false,
webPreferences: {
webSecurity: false, // 添加这一行
preload: path.join(__dirname, 'preload.js')
}
})
// 并加载应用程序的index.html。
loadingWindow.loadFile( 'src/loading.html');
}

ipcMain.on('loading-finish', (event) => {
loadingWindow.close()
mainWindow.show()
})

父子窗口

通过使用 parent 选项,你可以创建子窗口:

1
2
3
4
5
6
const { BrowserWindow } = require('electron')

const top = new BrowserWindow()
const child = new BrowserWindow({ parent: top })
child.show()
top.show()

child 窗口将总是显示在 top 窗口的顶部.

模态窗口

模态窗口是禁用父窗口的子窗口。 要创建模态窗口,必须同时设置parentmodal属性:

1
2
3
4
5
6
7
8
const { BrowserWindow } = require('electron')

const top = new BrowserWindow()
const child = new BrowserWindow({ parent: top, modal: true, show: false })
child.loadURL('https://github.com')
child.once('ready-to-show', () => {
child.show()
})

阻止关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
win.on('close', async(event)=>{

// 阻止默认 行为

// 询问是否确认此操作

const options = {

type: 'question',

title: '操作提示'

message:'确认关闭当前窗口吗?'

button:['OK', 'Cancel'],

}

cosnt result = await window.ipcRenderer.invoke('dialog:meeage-box',option);

if(result.response === 0){

win.destroy();}

})