本文所有源码均在:github.com/Sunny-117/e...
本文收录在《Electron桌面客户端应用程序开发入门到原理》掘金专栏
本文介绍
- 点对点通信
- 拼写检查
- 窗口池
- 原生文件的拖放
- 最近文件列表
- 屏幕截图
点对点通信
WebRTC,英语全称是 Web Real Time Communication,是一项实时通信技术,它允许网络应用在不借助中间媒介的情况下建立**浏览器之间点对点**的连接,实现视频流、音频流以及其他任意格式的数据传输。
WebRTC 项目始于 2011 年,Google 购买了一家名为 Global IP Solutions(GIPS)的公司,后者拥有一系列语音和视频通信技术。Google 将这些技术的一部分开源,并推动 WebRTC 成为一个开放的网络标准。自那以后,WebRTC 经过多年的发展和改进,得到了 W3C 和 IETF 的正式标准化。WebRTC 的出现,使用户在无须安装任何插件或者第三方软件的情况下,仅基于浏览器创建点对点的数据分享成为可能。
两个设备之间要建立WebRTC连接,就需要一个**信令服务器**。
信令服务器以一个"中间人"的身份帮助双方在尽可能少地暴露隐私的情况下建立连接。
比如对于音视频通话的应用来说,两端的媒体编码格式、媒体分辨率信息就是通过信令服务器互相告知对方的(这就是媒体协商),所以信令服务器最好是一个公网上的服务器,两个设备都能与这台服务器无障碍地通信。
WebRTC里面有一些核心的概念:
- STUN(Session Traversal Utilities for NAT):STUN 服务器的主要作用是帮助端点发现其公网上的IP地址和端口号。当一个 WebRTC 应用启动时,它会向 STUN 服务器发送一个请求,STUN 服务器会回应这个端点在互联网上的公网地址和端口。这被称为 NAT 穿透。
- TURN(Traversal Using Relays around NAT):TURN 服务器是 STUN 的一个扩展,它实际上是一个中继服务器。当 STUN 无法帮助端点建立直接连接时,TURN 服务器可以中继两个端点之间的数据包。
概括起来,sturn/turn服务器,都是在 WebRTC 通信中使用的服务器,它们帮助处理 NAT(网络地址转换)和防火墙带来的问题,使得位于这些网络限制后的设备能够相互通信。
具体的过程是两个设备通过sturn/turn服务器获取到自己映射在公网上的IP地址和端口号,再通过信令服务器告诉对方,这样双方就知道彼此的IP地址和端口号了,两个设备就可以基于此建立连接,这就是P2P打洞,也就是网络协商。注意,由于网络环境复杂,连接很可能没办法成功建立,此时sturn/turn服务器会协助传输数据,保证应用功能的正确性。
Coturn(github.com/coturn/cotu...
不过对于我们来讲,基于 C++ 的 Coturn 项目不太适合我们,这里我选择使用前端工程师更容易理解的 PeerJS(github.com/peers)项目构建W...
上下文菜单和拼写检查
上下文菜单
所谓上下文菜单,其实就是之前介绍过的右键菜单。
之前我们在实现右键菜单的时候,利用的是原生 JavaScript 来实现的。
js
const menu = document.getElementById("menu");
// 点击右键时对应的事件
window.oncontextmenu = function (e) {
e.preventDefault();
menu.style.left = e.clientX + "px";
menu.style.top = e.clientY + "px";
menu.style.display = "block";
};
// 用户点击右键菜单上面的某一项的时候
// 注意下面的查询 DOM 的方式只会获取到第一个匹配的元素
// 因此右键菜单上面的功能只会绑定到第一个菜单项上面
document.querySelector(".menu").onclick = function () {
console.log("这是右键菜单上面的某一个功能");
};
// 当用户点击窗口的其他地方的时候,右键菜单应该消失
window.onclick = function () {
menu.style.display = "none";
};
实际上,在 Electron 内部也内置了上下文菜单(右键菜单)的模块
具体示例如下:
js
const { Menu, MenuItem } = require("electron");
// 创建一个上下文菜单的实例
const contextMenu = new Menu();
// 回头我们就可以往这个 Menu 实例里面添加菜单项( MenuItem 的实例 )
contextMenu.append(
new MenuItem({
label: "复制",
role: "copy",
})
);
contextMenu.append(
new MenuItem({
label: "粘贴",
role: "paste",
})
);
contextMenu.append(
new MenuItem({
label: "剪切",
role: "cut",
})
);
module.exports = contextMenu;
创建一个 Menu 的实例,往这个 Menu 的实例里面通过 append 方法添加 MenuItem 的实例。
之后,在窗口实例上面监听 context-menu,然后调用 Menu 实例的 pop 方法来弹出上下文菜单。
js
// 设置右键菜单
// context-menu 事件会在用户点击右键时触发
win.webContents.on("context-menu", () => {
// contextMenu 是刚才导出的 Menu 实例
// 在 Menu 实例上面有一个 pop 方法,可以弹出菜单
// 这里接收了一个参数 win,回头上下文菜单就会在这个窗口上弹出
contextMenu.popup(win);
});
那么,在实际开发中,我们究竟使用哪一种方式来实现右键菜单?两者的对比如下:
- Electron 的
Menu
和MenuItem
方法 :- 集成度高:这种方法与 Electron 应用的其他原生功能(如系统级别的剪贴板操作)更好地集成。
- 跨平台一致性:Electron 的菜单在不同的操作系统上表现更加一致,更符合用户的操作系统习惯。
- 性能:作为原生组件,Electron 的菜单可能在性能上有一定优势。
- 限制:这种方法限制于 Electron 环境,不适用于纯网页应用。
- 原生 JavaScript 实现的上下文菜单 :
- 灵活性和可定制性:使用 HTML 和 CSS,你可以更自由地设计和样式化你的菜单。
- 适用性广:这种方法不仅适用于 Electron,还适用于任何网页应用,如果你是从网页改 Electron,那么这种方式可以直接用,不需要代码上的任何修改。
- 控制性:你可以更精细地控制菜单的行为和交互。
拼写检查
所谓拼写检查,顾名思义,就是检查用户的输入是否正确。
要使使用拼写检查,有这么几个步骤:
1. 开启拼写检查
首先需要在 webPreferences 里面进行配置:
js
webPreferences: {
spellcheck: true
},
对于 Electron9以及以上,默认开启,之前的版本,需要手动开启
2. 设置你要检查的语言
注意,这里在设置检查语言的时候,不同的系统是有区别的。
windows
该系统下需要手动进行设置:
js
// 将拼写检查器设置为检查美式英语和法语
// myWindow 就是窗口实例
myWindow.webContents.session.setSpellCheckerLanguages(['en-US', 'fr'])
// 包含所有可用语言的代码数组
const possibleLanguages = myWindow.webContents.session.availableSpellCheckerLanguages
macOS
macOS 系统下没办法手动设置,而是自动检查你当前系统的语言,根据你当前系统的语言进行检查。
3. 进行拼写检查操作
- myWindow.webContents.replaceMisspelling(suggestion):将错误的单词进行一个替换
- myWindow.webContents.session.addWordToSpellCheckerDictionary( ):允许用户将拼写错误的单词添加到字典里面
窗口池
1. 窗口池基本介绍
Electron创建窗口速度非常快,但渲染窗口速度很慢,从创建窗口完成到渲染窗口完成(加载本地一个简单的HTML页面)大概需要 2 秒的时间。
窗口池的原理就是提前准备n个隐藏的备用窗口,这里的 n 可以随意设置,一般情况下1~2 就足够了。让这 n 个隐藏的窗口加载一个空白页面。
-
当用户需要使用窗口时,程序就从窗口池中取出一个备用窗口,迫使内容区域路由到用户指定的页面,然后把窗口显示出来。由于我们已经初始化了窗口所需的资源,所以路由跳转的过程是非常快的,一般不会超过0.5秒。
-
当用户关闭窗口时,就直接把窗口实例释放掉,但程序会监听窗口的关闭事件,一旦释放了一个窗口,就马上创建一个新的隐藏窗口备用,也就是说确保窗口池中始终有n个窗口等待被使用。
窗口池的原理与线程池、数据库链接池的原理类似,创建线程或数据库链接是消耗资源非常高的操作,所以程序会创建一个"池子",提前准备好n个线程或链接,当应用程序索取时,就从"池子"里"捞出"一个空闲的线程或链接给消费者程序使用。一旦消费者程序使用完毕,要么归还线程或链接,要么直接释放,如果是直接释放的话,"池子"就要有自我创建的能力,确保"池子"里有充足的资源备用。我们这里就是采用直接释放窗口的逻辑。这主要是为了保证每次使用的窗口都具备全新的状态,而不必考虑清除上一次使用时遗留的状态。
2. 窗口池核心代码
窗口类
js
/**
* 窗口类
*/
class WindowItem {
/**
*
* @param {*} settings 创建窗口的时候的相关配置
*/
constructor(settings) {
this.width = settings.width;
this.height = settings.height;
this.x = settings.x;
this.y = settings.y;
this.id = v4(); // 窗口的唯一标识
this.window = this.createWindow();
if (settings.url) {
this.window.loadURL(settings.url);
} else {
// 如果没有url的话,就加载默认的页面
this.window.loadFile(path.join(__dirname, "../defaultWindow/index.html"));
}
}
createWindow() {
return new BrowserWindow({
width: this.width,
height: this.height,
x: this.x,
y: this.y,
show: false, // 一开始不显示窗口
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
}
}
module.exports = WindowItem;
窗口池类
js
/**
* 窗口池类:负责管理多个窗口
*/
class WindowManager {
constructor() {
this.pools = []; // 存储多个窗口实例
this.defaultSettings = {
width: 300,
height: 300,
x: 100,
y: 100,
url: null,
};
}
/**
* 初始化方法,用于初始化窗口池里面的窗口
* 按照默认配置初始化 3 个窗口
*/
init(n = 3) {
for (let i = 0; i < n; i++) {
this.createDefaultSettingWindow();
}
console.log(this.pools);
}
/**
* 按照默认配置来创建窗口
*/
createDefaultSettingWindow() {
this.pools.push(new WindowItem(this.defaultSettings));
}
/**
* 获取窗口池中窗口的数量
*/
getWindowCount() {
return this.pools.length;
}
/**
* 从窗口池里面拿一个窗口出来
*/
getWindow() {
return this.pools.shift();
}
}
module.exports = WindowManager;
原生文件拖放
所谓原生文件拖放(Native File Drag and Drop)是指**操作系统级别**支持的一种用户界面交互模式,允许用户通过鼠标或其他指针设备,将文件从一个位置拖动并释放(或"放置")到另一个位置。
ev.sender.startDrag 方法是 Electron 中用于启动原生拖拽操作的API。这个方法属于 WebContents 类,通常在 Electron 的主进程中使用,用于响应渲染进程的请求来开始一个拖拽操作。它允许应用程序指定拖拽过程中的文件和可选的自定义图标。
startDrag基本用法
startDrag 方法接受一个对象参数,该对象包含拖拽操作的相关信息。最基本的属性包括:
- file:要拖动的文件的路径。这是一个必须提供的属性。
- icon:拖拽操作中显示的图标的路径。这是一个可选属性,用于自定义拖拽时的图标。
下面是一个简单的示例:
js
ipcMain.on('ondragstart', (event, filePath) => {
event.sender.startDrag({
file: filePath,
icon: path.join(__dirname, 'path/to/icon.png')
});
});
没有拖拽完成的回调
关于拖拽操作的完成,startDrag 方法本身并不提供直接的回调或事件来通知拖拽完成。
这是**因为拖拽操作是与操作系统交互的**,操作的结果(如用户将文件拖��到哪里)并不反馈给 Electron 应用程序。
虽然没有直接的回调,但你可以通过其他机制来监听或处理拖拽完成后的情况:
- 监听目标位置的事件:如果拖拽的目标是应用程序内部的某个部分,你可以在那个部分监听 drop 事件来响应文件被拖拽到那里。
- 利用操作系统级别的通知:如果拖拽操作涉及到应用程序外部,比如拖拽文件到桌面或其他应用,通常没有办法直接从 Electron 获取操作完成的通知。在这种情况下,应用程序需要根据具体的使用场景来设计反馈机制。例如使用 Electron 的 Notification API 来发送桌面通知。
屏幕截图
我们来看一下如何实现屏幕截图这个需求。
screen
screen 模块来自于 electron.
对应文档地址:www.electronjs.org/zh/docs/lat...
这里我们主要用到的是一个 getPrimaryDisplay 方法,该方法用于返回主窗口的 Display 对象,这个对象包含了关于主显示器的各种信息,如尺寸、分辨率、工作区大小等。
Display
对象的属性包括但不限于:
bounds
: 显示器的绝对坐标和大小,包含x
、y
、width
、height
。workArea
: 显示器的工作区域,不包括系统任务栏和Dock,也是应用程序可用来显示内容的区域,同样包含x
、y
、width
、height
。scaleFactor
: 显示器的缩放因子,适用于高DPI显示器。rotation
: 显示器的旋转角度(如90、180、270度)。
workAreaSize
是从 getPrimaryDisplay()
返回的 Display
对象中访问的一个属性,它提供了主显示器工作区的尺寸。这个属性是 workArea
的简化版,仅包含 width
和 height
两个字段,分别表示工作区的宽度和高度(以像素为单位)。
这个属性非常有用,因为它告诉你应用程序可以使用的实际屏幕空间大小,排除了任务栏、Dock、窗口装饰等占用的空间。这使得开发者可以根据工作区大小来设计应用的布局和尺寸,确保应用窗口不会被这些系统元素覆盖或挤出屏幕边界。
使用场景举例
假设你想要创建一个新的窗口,恰好填充整个主显示器的工作区,不被系统任务栏或Dock遮挡,可以这样实现:
js
const { screen } = require('electron');
const mainWindow = new BrowserWindow({
width: screen.getPrimaryDisplay().workAreaSize.width,
height: screen.getPrimaryDisplay().workAreaSize.height
});
desktopCapturer
desktopCapturer 模块也是来自于 electorn.
对应的文档地址:www.electronjs.org/zh/docs/lat...
desktopCapturer.getSources(options) 是 Electron 中的一个方法,用于枚举和获取可用的媒体源,以便进行屏幕捕获。
该方法接收一个 options 参数,该参数是一个对象,用于指定获取源的类型和其他参数。主要的属性包括:
types
:一个字符串数组,定义要获取的源的类型。可用的类型包括screen
和window
,分别代表整个屏幕和单个窗口。thumbnailSize
:一个对象,指定返回的缩略图的大小。它应包含width
和height
属性。
该方法的返回值是一个 promise,这个 promise 解析为一个包含源信息的数组。
每个源(即每个屏幕或窗口)是一个对象,包含如下属性:
id
:源的唯一标识符,用于后续捕获此源的屏幕或窗口。name
:源的名称,如窗口标题或屏幕编号。thumbnail
:源的缩略图,是一个nativeImage
对象,可以用于预览。
使用场景举例
js
// 引入 Electron 的 desktopCapturer 模块。
// desktopCapturer 用于访问屏幕捕获功能,允许应用捕捉屏幕和单独窗口的视频流。
const { desktopCapturer } = require('electron');
// 定义一个异步函数 getSources,用于获取屏幕和窗口的源信息。
async function getSources() {
try {
// 使用 desktopCapturer.getSources 方法异步获取所有屏幕和窗口的源。
// 这个方法接受一个 options 对象作为参数,用于指定要获取的源的类型和缩略图大小。
const sources = await desktopCapturer.getSources({
// types 属性是一个数组,指定了需要获取哪些类型的源。
// 'window' 表示窗口源,'screen' 表示屏幕源。
types: ['window', 'screen'],
// thumbnailSize 属性指定返回的缩略图的大小。
// 在这个例子中,缩略图的宽度被设置为 128 像素,高度被设置为 72 像素。
thumbnailSize: { width: 128, height: 72 }
});
// 遍历获取到的源信息数组。
for (const source of sources) {
// 打印每个源的名称到控制台。
// source.name 通常是窗口的标题或屏幕的标识。
console.log('Source:', source.name);
// source.id 是捕获源的唯一标识符,可以用于实际的屏幕捕获操作。
// source.thumbnail 是源的缩略图,是一个 nativeImage 对象,可以用于界面上显示预览。
// 这里的示例代码仅打印了源的名称,实际应用中可以使用 source.id 和 source.thumbnail 来实现屏幕捕获和预览功能。
}
} catch (err) {
// 如果在获取源信息的过程中发生错误,则捕获这个错误并打印到控制台。
console.error('Error getting sources:', err);
}
}
// 调用 getSources 函数,开始获取屏幕和窗口的源信息。
getSources();
这段代码展示了如何使用 Electron 的 desktopCapturer
模块来获取和处理屏幕及窗口捕获源的基本流程。通过调整 types
和 thumbnailSize
参数,开发者可以根据具体需求获取不同类型的源和合适大小的缩略图。
nativeImage
同样是 electron 所提供的模块。
文档地址:www.electronjs.org/zh/docs/lat...
该模块提供了一组 API 来创建和管理图像。这个模块的主要作用是允许开发者在 Electron 应用中以编程方式处理图像数据,包括从文件、数据URL、剪贴板或其他 nativeImage 实例中创建图像,以及获取和修改图像的属性。
使用场景
nativeImage
模块在 Electron 应用中有广泛的应用场景:
- 应用图标 :使用
nativeImage
来设置应用窗口的图标(BrowserWindow
或app
图标)。 - 托盘图标 :在系统托盘区域显示的图标也可以通过
nativeImage
来创建和管理。 - 剪贴板操作 :可以使用
nativeImage
来读取或写入剪贴板中的图像内容。 - 动态图像处理 :对于需要动态生成或处理图像的应用,如图像编辑器或工具,
nativeImage
提供了基本的图像处理能力。
示例代码
创建一个 nativeImage
实例从本地文件:
js
const { nativeImage } = require('electron');
let image = nativeImage.createFromPath('/path/to/image.png');
从数据URL创建一个 nativeImage
实例:
js
let dataUrl = 'data:image/png;base64,...';
let image = nativeImage.createFromDataURL(dataUrl);
设置应用窗口图标:
js
const { BrowserWindow, nativeImage } = require('electron');
let win = new BrowserWindow({
icon: nativeImage.createFromPath('/path/to/icon.png')
});
nativeImage
模块通过提供这些功能,极大地增强了 Electron 应用在图像处理方面的能力,使得开发者能够更灵活地在其应用中使用和操作图像。
在我们的代码中,会用到 createFromDataURL 方法,该方法用于从数据 URL(Data URL)创建一个 nativeImage
对象。
假设你有一个图像的数据 URL,想要将其作为应用的窗口图标。这时,你可以使用 nativeImage.createFromDataURL(dataUrl)
方法来创建一个 nativeImage
对象,然后将其设置为窗口的图标。
js
const { BrowserWindow, nativeImage } = require('electron');
// 假设这是一个图像的数据URL
let dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...';
// 使用 nativeImage.createFromDataURL 方法从数据URL创建一个 nativeImage 对象
let image = nativeImage.createFromDataURL(dataUrl);
// 创建一个新的 BrowserWindow 实例
let win = new BrowserWindow({
// 使用创建的 nativeImage 对象作为窗口图标
icon: image
});
// 加载应用的 HTML 文件或任何其他内容
win.loadURL('file://' + __dirname + '/index.html');
requestSingleInstanceLock 方法
requestSingleInstanceLock 是属于 app 模块上面的一个方法。
对应的方法地址:www.electronjs.org/zh/docs/lat...
用于确保应用程序只有一个实例在运行。当你的应用尝试启动一个新实例时,这个方法可以帮助你检测到这种情况,并允许你将焦点转移到原有的实例上,而不是打开一个新的窗口或实例。这对于开发那些不应该或不需要同时运行多个实例的应用尤其有用,例如,编辑器、工具应用或某些类型的媒体播放器。
当 app.requestSingleInstanceLock()
被调用时,Electron 会检查当前应用的实例是否已经获得了"单实例锁"。如果没有,当前实例会尝试获取这个锁:
- 如果成功获取到锁,意味着当前实例是第一个启动的实例,应用会正常启动,返回值为 true。
- 如果没有获取到锁,意味着已经有一个实例在运行了,返回值为 false,当前尝试启动的实例会触发
second-instance
事件并退出,开发者可以监听这个事件来处理额外的逻辑,比如将焦点转移到已经运行的实例的窗口上。
代码示例
js
const { app } = require('electron');
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
// 如果获取锁失败,说明已经有一个实例在运行,当前实例应该退出。
app.quit();
} else {
// 如果成功获取到锁,当前实例是第一个实例,应该继续运行应用。
// 创建窗口的逻辑...
// 然后我们为应用绑定 second-instance 事件
app.on('second-instance', (event, commandLine, workingDirectory) => {
// 当尝试启动第二个实例时,该事件会被触发。
// 你可以在这里处理将焦点转移到你的主窗口上等逻辑。
// commandLine: 启动第二个实例的命令行参数
// workingDirectory: 第二个实例的工作目录
});
}
本文所有源码均在:github.com/Sunny-117/e...
「❤️ 感谢大家」
如果你觉得这篇内容对你挺有有帮助的话: 点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。觉得不错的话,也可以阅读 Sunny 近期梳理的文章(感谢掘友的鼓励与支持 🌹🌹🌹):
我的博客:
Github: https://github.com/sunny-117/
前端八股文题库: sunny-117.github.io/blog/
前端面试手写题库: github.com/Sunny-117/j...
手写前端库源码教程: sunny-117.github.io/mini-anythi...
热门文章
- ✨ 爆肝 10w 字,带你精通 React18 架构设计和源码实现【上】
- ✨ 爆肝 10w 字,带你精通 React18 架构设计和源码实现【下】
- 前端包管理进阶:通用函数库与组件库打包实战
- 🍻 前端服务监控原理与手写开源监控框架 SDK
- 🚀 2w 字带你精通前端脚手架开源工具开发
- 🔥 爆肝 5w 字,带你深入前端构建工具 Rollup 高阶使用、API、插件机制和开发
- 🚀 Rust 构建简易实时聊天系统
专栏