笔者最近开始入局桌面客户端开发,真的是大终端开发,啥端都能做。
背景
客户端上我们采用的是 Electron,这优缺点就不讲了,懂得都懂。但也是有很多成熟产品为它站台,比如新版的 QQ、VS Code、语雀客户端等等。
最近接了一个可行性研究需求,需要实现如同 Figma 这样多 Tabs 切换的效果:
原以为实现起来很简单,毕竟这不是很成熟的功能了嘛,但细致研究起来,这东西也不简单。
调研
当然,先去找找有没有现成的轮子可以用。
在 github 翻了半天,也就 electron_tabs 是独立的,而且 star 比较多。
但是,electron_tabs 有个致命问题,它是基于 WebView 方案实现的,而 WebView 已经是官方不建议的方式了,详见:Web 嵌入。
我们应该用官方推荐的BrowserView
的方式实现标签页切换。加上使用BrowserView
限定条件后,发现并没有这样的轮子存在。
当然没有轮子也是有道理的,毕竟使用BrowserView
实现标签页,需要做:
- 主窗口:标签样式及控制。
- 主进程:标签管理。
- 渲染视图与标签管理的交互(打开标签页/跳转到某标签页)。
- 主窗口与标签管理的交互(新标签页/关闭标签页)。
那没有合适的轮子,那就要自己造轮子了。
效果
(凑合着看,视频还是无法传到掘金上,只能转糊糊的 gif 看个大致吧)
实现
主窗口:标签样式及控制
这个取巧一些,直接去 down electron_tabs 源码拿来改,这样就不用自己写样式了 ~
把这两个文件 copy 到项目里,然后删除里面关于webview
相关的部分,我们都不需要。
在入口控制上main.ts
,增加通信交互以及控制。
...
tabGroup.on('tab-active', (tab: Tab) => {
if (tab.src == defaultSrc) {
tabGroup.buttonContainer.style.display = 'none';
}
ipcRenderer.invoke('switchTab', tab.src);
});
tabGroup.on('tab-removed', (tab: Tab) => {
if (tab.src == defaultSrc) {
tabGroup.buttonContainer.style.display = 'flex';
}
window.open('closeTab', tab.src);
});
// 监听渲染视图打开或切换标签
ipcRenderer.on('switch-tab', (_, data: any) => {
let tab = tabGroup.tabs.find((item) => item.src === data.url);
if (!tab) {
tab = tabGroup.addTab({
title: data.title,
src: data.url,
active: true,
closable: true,
});
}
tab.activate();
});
...
这里控制最多➕号点击后隐藏,防止标签太多,影响性能。
主进程:标签管理
新建一个tab-manager
标签管理器来控制,它具备以下能力:
管理页面上加载的BrowserView
对象
提供一个预缓存的 Tabs 存储空的 Tabs 对象。
提供一个Map<string, GDWebContainer>
保存已加载 url 以及容器对象。
提供创建和切换标签方法:
...
public createTab(url: string): GDWebContainer {
const webContainer = this.preloads.pop();
if (!webContainer) {
throw new Error('Tab 创建失败');
}
this.mainWindow.addBrowserView(webContainer.context);
// 设置位置
webContainer.loadURL(url);
this.handleContainerEvent(webContainer);
this.tabs.set(url, webContainer);
this.preloadTab();
return webContainer;
}
...
public switchTab(url: string): GDWebContainer {
let webContainer = this.tabs.get(url);
if (!webContainer) {
webContainer = this.createTab(url);
}
this.active = webContainer;
this.attachWebContainerIfNeed(webContainer);
this.mainWindow.setTopBrowserView(webContainer.context);
return webContainer;
}
...
视图预热,提前加载BrowserView
空对象,让下一个标签更快的打开
const MAX_PRELOAD_TAB_COUNT = 1;
...
private preloadTab() {
const count = MAX_PRELOAD_TAB_COUNT - this.preloads.length;
for (let i = 0; i < count; i++) {
const view = new GDWebContainer(this.globalOptions);
this.preloads.push(view);
}
console.log(`预加载 Tab:${count}个,当前空闲 Tab 数量:${this.preloads.length}`);
}
监听渲染视图打开标签页事件,发送给主窗口修改
private handleContainerEvent(webContainer: GDWebContainer) {
webContainer.onOpenNewWindow((url) => {
this.switchTab(url);
this.mainWindow.webContents.send('switch-tab', { url: url, title: title });
});
}
切换标签时,更改当前显示的BrowserView
private handleRouterSwitchTab() {
ipcMain.handle('switchTab', (_: Event, url: string) => {
GDTabManager.shared.switchTab(url);
});
}
总结
桌面端开发笔者也是第一次介入,Electron 也是第一次接触,但语言框架都是相通的,特别 Electron 还是微软的,有着良(luo)好(suo)且固(si)定(ban)的 API 调用。学习起来并不难,社区上的轮子确实不少,但这样很通用的需求点也没有一个成熟的三方库。
后续,笔者有空[手动狗头]会整理下,可以提供一个三方库或源码出去,供大家使用。
感谢阅读,如果对你有用请点个赞 ❤️