「桌面端」Electron BrowserView 实现多标签页效果

7,750 阅读3分钟

笔者最近开始入局桌面客户端开发,真的是大终端开发,啥端都能做。

背景

客户端上我们采用的是 Electron,这优缺点就不讲了,懂得都懂。但也是有很多成熟产品为它站台,比如新版的 QQ、VS Code、语雀客户端等等。

最近接了一个可行性研究需求,需要实现如同 Figma 这样多 Tabs 切换的效果:

image.png

原以为实现起来很简单,毕竟这不是很成熟的功能了嘛,但细致研究起来,这东西也不简单。

调研

当然,先去找找有没有现成的轮子可以用。

在 github 翻了半天,也就 electron_tabs 是独立的,而且 star 比较多。

但是,electron_tabs 有个致命问题,它是基于 WebView 方案实现的,而 WebView 已经是官方不建议的方式了,详见:Web 嵌入

我们应该用官方推荐的BrowserView的方式实现标签页切换。加上使用BrowserView限定条件后,发现并没有这样的轮子存在。

当然没有轮子也是有道理的,毕竟使用BrowserView实现标签页,需要做:

  1. 主窗口:标签样式及控制。
  2. 主进程:标签管理。
  3. 渲染视图与标签管理的交互(打开标签页/跳转到某标签页)。
  4. 主窗口与标签管理的交互(新标签页/关闭标签页)。

那没有合适的轮子,那就要自己造轮子了。

效果

未命名的设计_525320245_20230605181021.gif

(凑合着看,视频还是无法传到掘金上,只能转糊糊的 gif 看个大致吧)

实现

主窗口:标签样式及控制

这个取巧一些,直接去 down electron_tabs 源码拿来改,这样就不用自己写样式了 ~

image.png

把这两个文件 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 调用。学习起来并不难,社区上的轮子确实不少,但这样很通用的需求点也没有一个成熟的三方库。

后续,笔者有空[手动狗头]会整理下,可以提供一个三方库或源码出去,供大家使用。


感谢阅读,如果对你有用请点个赞 ❤️

中秋节GIF动图引导在看提示.gif