likes
comments
collection
share

「桌面端」Electron 你不知道的 BrowserView

作者站长头像
站长
· 阅读数 22

笔者最近很久没发文章了,主要是一直在加班赶桌面端改造工作,在Electron的大坑中越走越远,也看到了Electron的很多局限性。

社区上关于Electron的文章还是太少了,笔者会慢慢整理下在Electron上的感悟,包括布局、通信、调试技巧,让大家更了解下Electron。笔者这里再说一句,有选择的话,尽量还是别用Electron了 ...

概要

大终端开发,不仅啥都能做,还啥坑都能踩。

通过本文,你将获得:

  • 容器封装。
  • 预加载管理。
  • 加载中 Loading 实现。
  • 加载失败 Error 实现。

不会获得:

  • 便捷的通信机制[手动狗头]

效果

不干讲,先看看最终实现的效果:

「桌面端」Electron 你不知道的 BrowserView

如果用以前WebView的实现方式还是比较简单的,但官方是明确不支持这样做了Electron Web 嵌入

那选择上还有iframeBrowserView了,说实话,iframe实现上会简单很多,但没办法做到预加载,那整体效果上会变得差强人意。更重要的一点,由于历史遗留问题,项目中存在一套旧的通信机制,2套的机制不兼容(跨端篇再细讲),所以没办法用iframe

细节

言归正传,讲讲BrowserView是怎么实现的。

页签实现的逻辑跟前文大差不差,所以这里略过。

容器封装

封装容器做了哪些事?

我们希望无论是用BrowserWindow还是BrowserView渲染,都是一致的参数封装。

    /**
     * 唯一 ID
     */
    public readonly id: number;
    /**
     * 加载地址
     */
    private url?: string;
    
    ...

那除了一致的 UI、一致的 prelod 外,我们还希望有一致的逻辑处理

「桌面端」Electron 你不知道的 BrowserView

页面标题及图标

dom-ready时机可以比did-finish-load更早的读取html的内容。

title可以通过electron API 获取,但favicon系统只给了事件,通过事件变更来获取还不如直接通过JS来获取。

代码如下:

   private configDocumentInfo() {
       this.context.webContents.on('dom-ready', async () => {
           this.title = this.context.webContents.getTitle();
           this.icon = await this.executeJavaScript(
               `
(function() {
 const icon = document.querySelector('link[rel~="icon"]');
 return icon && icon.href || undefined;
})()
`,
           );
       });
   }

监听容器刷新事件

如何通知容器需要刷新,用 EventBus 来实现。

容器监听刷新事件,并判断是否是自身需要刷新:

    private listenGNBEvents() {
        GNBEventBus.shared.subscribe((data: any) => {
            if (data.eventName === 'container.containerWillRefresh') {
                let url = data.data.id;
                if (this.url === url) {
                    this.context.webContents.loadURL(this.url!);
                }
            }
        });
    }

刷新事件发送方实质是 Web 页面,通过通信层向Electron发送,这里通信层略过,在Electron处理刷新的地方调用:

async reload(): Promise<GNBContainerReloadResponse> {
      GNBManager.shared.container.emitContainerWillRefreshEvent(this.currentURL);  
}

监听容器事件

监听容器加载开始、加载完成、加载失败事件,做响应的处理,放在后面加载页、错误页实现来讲。

预加载管理

很多文章都提到用预加载来加快加载效率,但都没有具体的实现代码,本文把实现放出来给大家参考。

其实原理还是很简单,管理好预加载好的容器以及已存在的容器就好。

    /**
     * 预加载缓存的 Containers
     */
    private readonly preloads: GDWebContainer[];
    /**
     * 已存在的 containers
     */
    private readonly containers: Map<string, GDWebContainer>;

预加载原理就是消费掉一个容器,就及时补充一个。

核心代码:

   /**
     * 预加载 Container
     */
    private preloadContainer() {
        const count = MAX_PRELOAD_CONTAINER_COUNT - this.preloads.length;
        for (let i = 0; i < count; i++) {
            const view = new GDWebContainer(this.globalOptions);
            this.preloads.push(view);
        }
        console.log(
            `预加载 Container:${count}个,当前空闲 Container 数量:${this.preloads.length}`,
        );
    }

加载失败页面实现

为什么要分开讲?因为他们的实现原理不同。

「桌面端」Electron 你不知道的 BrowserView

失败页面实质上是变更了加载的 URL,指向了本地的 HTML。

先在容器内进行失败监听:

    private listenContainerEvents(): void {
        this.context.webContents.on('did-fail-load', (_event, errorCode, errorDescription) => {
            console.error(
                `GDWebContainer 加载失败,错误代码: ${errorCode},错误描述: ${errorDescription}`,
            );
            if (this.errorView) {
                GDErrorPage.show(this.context);
            }
        });
    ...

GDErrorPage的实现其实很简单:

    /**
     * 显示
     */
    public static show(view: BrowserView): void {
        view.webContents.loadFile(...);
    }

那如何重新加载呢?

这就是上文的刷新事件在起作用了,收到containerWillRefresh事件后,拿当前容器的url参数进行重新加载,而不是去刷新当前地址。

加载 Loading 页面实现

「桌面端」Electron 你不知道的 BrowserView

当前效果这样,也可以在HTML渲染实现上换成骨架图之类的。

这个为什么跟加载失败页不同?我们在加载过程中去替换 URL,那就离了大谱了[手动狗头],所以这里采用的方式是盖一个BrowserView上去。

一样,需要在容器层监听事件:

            let loadingPage: GDLoadingPage = new GDLoadingPage();
            this.context.webContents.on('did-start-loading', () => {
                setTimeout(() => {
                    loadingPage.show(this.context);
                }, 50);
            });
            this.context.webContents.on('did-finish-load', () => {
                loadingPage.hide();
            });

            this.context.webContents.on('did-fail-load', () => {
                loadingPage.hide();
            });

这里可以看到有个延迟 50ms 加载,延迟是为了下一渲染帧再进行显示,防止出现BrowserView还没加到BrowserWindow导致 loading 页面不出现的问题。

GDLoadingPage的实现也不复杂:

export class GDLoadingPage {
    private view: BrowserView | null = new BrowserView();
    private window: BrowserWindow | null;

    /**
     * 显示
     */
    public show(parent: BrowserView | BrowserWindow): void {
        if (!this.view) {
            return;
        }
        if (parent instanceof BrowserWindow) {
            this.window = parent;
        } else {
            this.window = BrowserWindow.fromWebContents(parent.webContents);
        }
        this.view.setAutoResize({
            horizontal: true,
            width: true,
            height: true,
        });
        this.window?.addBrowserView(this.view);
        this.view.setBounds(parent.getBounds());
        this.view.webContents.loadURL(
            (import.meta as any).env.VITE_DEV_LOADING_VIEW || 'gaodingpro://web.root/loading/',
        );
    }

    /**
     * 隐藏
     */
    public hide(): void {
        this.window?.removeBrowserView(this.view!);
        this.view = null;
    }
}

里面要注意的点是隐藏的时候我们要view = null;防止继续占用内存。

URL 作为跳转唯一索引

桌面端是多页面同时展示的形态,那在副窗口页签上,对用户来说,点击相同的链接,应该打开(返回)同一个页面,而不是一直新开页面。

所以在整体设计上,采用 URL 标志作为唯一索引,每个固定的链接都不会重复打开多份(从用户交互以及性能考虑)。

如果真需要创建多份相同页面的场景,使用不同的 URL 或者拼装时间戳。

当然,十分不建议在桌面端创建多个相同页面,毕竟Electron性能开销还是很大的。

总结

从 App 开发者角度看,Electron实在不灵活,类比下,BrowserView = WebView 而不是 View,所以这个性能损耗上可想而知。更何况布局局限性也很大,下篇或者下下篇来介绍下用BrowserView布局要怎么处理一系列的问题。


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

「桌面端」Electron 你不知道的 BrowserView
转载自:https://juejin.cn/post/7260030417461346361
评论
请登录