背景
在上文中,简单实现了一个使用BrowserView
实现多标签的方案,在评论区很多小伙伴想看看源码,最近刚好有时间可以梳理下。
多标签页只是 UI 上的表象,内在逻辑是BrowserView
多容器管理,所以本文重点会从容器管理这边讲解下整个的实现逻辑。
介绍
目录结构
先来看看项目依赖项:
Demo 尽量减少技术栈来让大家更好更快理解,而不是还要理解各种框架使用。因此,除了Electron
自身的框架必要依赖外,只使用了vite
做渲染层脚手架。
公司项目渲染层是用的Vue3
,但对我们 Demo 而言,不需要依赖任何三方框架,用Web
原生实现就好。
/render UI 渲染,这个上文中写的比较多了
/src Electron 主框架
/src/container 容器管理
/src/helpers 通信交互辅助方法
/src/pages 页面管理
/src/service 通信实现
/src/window 简单的主窗口实现(本文就不涉及窗口管理,减少大家的认知成本)
运行方式
源码地址中忘了写 README,还是太仓促了,所以有同学评论说 Demo 跑不起来,这里补充下源码的运行方式。
整个项目用的是Electron
推荐的yarn
作为包管理工具,包安装命令如下:
shell
yarn
由命令可知,需要开2个终端,一个运行渲染层,一个启动Electron
。
运行渲染层:
shell
yarn render:dev
启动Electron
:
shell
yarn start
运行结果
启动后,正常显示如下:
登录后,点击任意需要window.open
的按钮:
出现新标签页:
解析
下面我们来深入分析下源码,每一层都做了什么事。
渲染层
目录介绍
/tabs 存放标签页的实现,实现方式上文有讲,魔改electron-tabs
实现的,本文提不再复述了。
/utils 这里放的是事件接收、事件通知的方法封装。因为是从项目中抽出来的,所以沿用了GNB
跨端通信的实现。感兴趣的见此文:跨端通信终结者|看我是如何保证多端消息一致性的。(PS:桌面端如何实现的,如今还只起了个头,文章一直欠着...[手动狗头])
main.ts 逻辑调用层,监听Electron
通知过来的方法以及主动调用的逻辑。
代码不长,应该很好理解。
这里着重讲一下 Tab 和容器的关联关系:
以下图这个创建 Tab 监听为例:
我们其实用的是BrowserView
容器的 ID 来作为 Tab 的唯一 ID,这样来保证渲染层和容器的一致性。
新增 Tab
上文有同学评论说,不知道如何在渲染层创建一个 Tab 页。
这里解答一下,很简单:createTabOnWindow('https://www.gaoding.com')
调用createTabOnWindow
方法即可。
那如何显示出一个默认的 + Tab 按钮,来新增 Tab 呢?
这个 UIelectron-tabs
已经有写好的 API 了:
但我们是BrowserView
容器魔改版本,笔者项目需求也不需要 + 按钮,所以源码需要稍微调整下(github 已更新):
-. 不再使用defaultTab
来设置默认点击产生的Tab
(原因下文会讲)
-. 打开new-tab-button="true"
,让样式显示。
-. 点击 + 按钮,创建相应的 Tab 页。
Typescript
tabGroup.on('click-add-button', () => {
createTabOnWindow('https://www.gaoding.com/create-design')
})
效果如下:
通信层
渲染层调用框架
preload.js
先来看一下preload.js
很简单,只暴露一个window.$gnb
句柄,一个window.$gnb.desktop
方法给前端使用。
前端调用
前端 gnb.desktop.ts 通过传类型和参数来调用框架方法:
框架处理
方法响应处理写在DesktopService
这个类中。
通过方法映射functionMap
找到具体方法,进行实际调用即可。
这种设计,是GNB
桌面端特性一部分,具体在跨端通信终结者|桌面端已加入全家桶🔝(还是草稿) 这篇文章中具体介绍设计理念~
框架通知渲染层
反向的,框架发生变化也需要通知到 UI 渲染层。而这里其实有一个重要的设计理念:
不仅需要框架与渲染层一对一通信的能力
还需要框架与渲染层一对多通信的能力
一对一通信比较好理解,比如需要框架侧监听 title 变化,并且修改tab
的标题:
那我们为什么需要一对多通信?本次源码并没有涉及,但这在多容器的场景下很常见:多容器页面间的状态管理,比如支付成功后,需要通知到所有打开的容器页面刷新会员信息,这里不再铺开陈述了。
框架发送
这里其实也是做了一个巧妙的解耦:
事件发送和容器接收间不产生依赖关系,而是通过GNBEventBus
事件总线做中转。
容器接收代码:
BrowserWindow
BrowserView
实际发送 JS:
可以看到,就是发送自定义事件来处理。至于是单独容器接收还是多容器接收,由传入的容器 ID 控制。
GDEventBus
触发示例:
前端监听
渲染层也是通过事件管理器GNBEventManager
来监听自定义事件的触发。
Typescript
GNBEventManager.shared.register() // 需要在最开始注册
gnb.desktop.ts 封装了监听方法:
容器层
容器是用于将应用与其所有必要文件捆绑到一个运行时环境中的技术
上文一直在讲容器的概念,一般大家听的比较多的是后端概念,比如Dock
容器,那在桌面端中容器是指什么呢?
在笔者理解,指的是渲染层最小运行时环境,换成Electron
术语,BrowserWindow
和BrowserView
都是容器。但窗口能实现的,其实都可以嵌套BrowserView
来实现,所以BrowserView
更符合容器定义。
容器封装
我们不直接使用BrowserView
,而是封装一层,可以更好的做扩展及容器约束,配置初始化,建立通信机制。详见:container.ts。
这里因为是为了多标签页源码而产生的 Demo,所以其实阉割掉很多能力。按我们规划上,Web 容器蓝图如下:
这是不分端的容器设计图,无论是 App 还是桌面端都是按图中的方向进行建设。
容器管理
我们有单个容器的建设后,还需要对容器进行统一管理,来保证它的生命周期以及外部规范使用。详见:index.ts。
容器预加载
减少加载速度,提高用户体验,我们每使用一个容器,都会提前预加载一个新的空白容器放到内存中。
容器池
Demo 删减掉了这一部分,简单说一下,容器池主要是为了防止容器越来越多,影响到整体性能,而采用容器循环利用的方案。
业务层
容器管理、窗口管理(本文未涉及)也好,都是基建的一部分。但对业务来讲,如何去拼装容器到窗口中,形成优雅的可视化页面,这个也需要进行统一管理。
页面管理
GDTabPageContainer
就是 Demo 中管理整体布局的页面容器类。详见:index.ts。
它包含几部分功能:
-. 框架加载初始化
-. 管理容器布局
-. 容器持有
可以看到我们创建 Tab、切换 Tab、关闭 Tab 的方法也都是在这里面
总结
源码解析就告一段落,笔者认为管理好窗口和容器,能在性能和体验达到平衡,才是Electron
的重中之重,不然不如就直接套壳Web
页不就完了。按某人的话说,又不是不能用 [手动狗头] ~
感谢阅读,如果对你有用请点个赞 ❤️