一篇姗姗姗姗姗姗姗姗来迟的文章。
前言
前一阵子笔者在忙于桌面端 Electron 相关的开发,工欲善其事,必先利其器。
对于基于浏览器的桌面端框架(Electron / Tauri / ...)来说,如何构建稳定快捷的跨端通信机制,是其中的开发重点。
那前面我们在跨端通信终结者|看我是如何保证多端消息一致性的 🔥一文中说到,使用codegen工具链解决跨端通信一致性的问题。桌面端只需要当作一个实现终端,加入到这套机制里即可。
分析
由于笔者公司是 Electron 框架开发的,所以这里只对 Electron 的实现举例。
Electron 进程间通信官方讲了很多种进程间通信的方式,但我们只需要最推荐的方式,且只需要异步的方法。
为什么不建议使用同步的方法?同步方法会堵塞主进程,虽然比如变量传值等用同步方法十分方便,但无法保证这个过程不会劣化。
那对于codegen来讲,需要去生成3部分的代码:
预加载脚本 preload.js
使用ipcRenderer
向渲染进程有限暴露能力。
这个参照官方文档的定义即可。
我们生成的如下,分成多个模块:
web 调用端
调用端是一致的,无论是在 App 还是桌面端,使用方式一致,API 一致,自动适配各端。
但内部处理不同,生成的桌面端独有的调用方式,关键调用代码类似如下:
window.$gnb.$settings$checkUpdate()
在桌面端上会去调用上文的preload.js
预加载与桌面端主进程通信。
Electron 支撑端
同样的,我们在桌面端也是生成一套支撑库来用于协同通信一致性。
在实现上,只需要实现这些服务即可:
实现细节
本文着重的细节介绍在 Electron 支撑端生成的实现上,这还是有一点技巧的。
构造
这和在 App 上实现不大一样,App 上是收到通信后再根据name 进行转发,而在 Electron 上,通信需要先注册后使用,所以需要在constructor
就把通信通道建立起来。
当然,你也可以只使用一个通道,然后内部根据name进行转发,这样与 App 一致,但多通道比单通道性能会更好。
生成的Service
即如上图,ipcMain.handle()
与预加载preload.js一一对应。
observer
是实现的接口实例,需要外部实现,当然不实现也不会报错。
resultWithPromise()
是为了结果一致性。
这里还要说的一点就是我们在桌面端把event
也要传出去,这一点跟 App 的处理不一样,App 一般是当前容器发出的调用请求,但对桌面端来讲,天然是多容器同时存在的,我们在某些情况下就必须要区分是哪个容器发起的调用请求。
扩展一下,App 上可以区分出是哪个容器发的请求吗?
当然可以,需要对我们的
js-bridge
进行改造,在初始化容器时向 Web 中注入一个唯一容器 id,类似window.__webview_id=xxx
,在调用链路上携带这个 id 即可判断。当然,现在没有这个使用场景,没必要做这一层,后续需要做这一层也不会对当前实现产生影响。
event
是一个IpcMainEvent
,可以从中拿到当前是容器、窗口的句柄,比如获取窗口BrowserWindow.fromWebContents(event.sender);
。
事件通知
正向调用有了,那桌面端如何主动发起请求通知容器?还是与 App 的实现一样,构造一个事件通知机制。笔者发现上文也没有把事件通知如何实现的讲解出来... 这里一并补上。
定义
首先,我们还是在 YAML 文件中定义一层事件出来。
事件监听广播机制,在调用方会提供 on/off 方法,提供监听/取消监听的能力。
在宿主方发送全局广播。
name(必填) 事件名称
note(必填) 事件说明
callback(可选) API 返回出参
参数不建议超过5个,超过的话可以定义成模型。
-
name
参数名称 -
type
参数类型,按照 schema 提示定义,拒绝不符合规范的 type -
note
参数说明
比如:
yaml
events:
- name: loginDidSuccess
note: 登录成功
- name: userInfoDidChange
note: 用户信息发生变化
桌面端发出
消息通知应该是1对多的场景,天然在桌面端上,比如用户登出,登录成功,付费等等,需要改变多个容器或者窗口的显示状态。
那我们如何发出这一事件呢?按照官网的做法还是用 ipc 通信,可以,但不够好,不够好的原因是 ipc 通信是一个一对一通信,也不够通用,到 App 上还是要做一套。
通用的做法是什么呢?
我们可以直接使用执行 JS 方法的方式,进行容器通信:
可以看到,我们是执行dispatchEvent(new CustomEvent(...))
的方式发送一个事件给各个容器即可。
那还需要一个管理总线,让每个容器注册上来进行发送,这很简单:
对于codegen
来说,那抹平这一层,让调用范式化即可,比如:
前端接收
前端接收是通用的,App 可以执行到,桌面端也可以执行到,具体就是实现了一个event-manager
来进行转发作业。
实现细节不贴了,但有监听就要有取消监听,使用方自己决定。
codegen
生成的代码示例如下:
安全防护
安全总是我们要考虑的一环,除了在桌面端容器层面完全符合官方文档外,我们在跨端通信这一层做了白名单防护,防止加载到恶意页面后还能通过桥接操作原生信息。
这也很简单,在每个调用处增加了强制校验即可:
从event
获取到当前加载的 URL 是否符合校验规则。
特性能力
GNB 是通用的端侧通信约定层,但在桌面端上,现在有挺多不通用的通信约定,只在桌面端才会存在使用,不能很好跟通用约定所结合。
当然可以通过增加platform
属性定义一些只在桌面端生效的 GNB API,如下红框:
但有一些 API 的场景十分特别,只在桌面端框架层使用,而不是通用页面。比如控制BrowserView
间的视图关系、回到主窗口页面等,这些不适合来定义成通用端侧能力,而应该考虑其他的实现方式。
那我们在具体实现上,增加一个特殊的特性通道,专门用于构造这一部分特有的能力:
通过动态 type 解析的方式,完成框架渲染层与主进程的通信。
在逻辑开发时,同时提供给渲染层调用的代码方法,完成自身闭环。
具体使用例子也可以看:桌面端|Electron BrowserView 多容器管理(内附 Demo)
里面还有如何一对一通信的,细节不表了。
总结
从整体跨端通信的角度,我们建设上已经是完善了,除了 Flutter for web 的如何统一通信没讲(也不打算讲了),其他平台都有介绍。
后续
但这就是终点吗?并不是,我们构建统一跨端通信的目的是整合跨端项目,跨端通信,说具体点,是一套通信协议,是可以直接用,但直接使用底层能力并不是一个好的解决方式,我们希望的是从业务角度出发,解决前端页面在各个平台的能力兼容性,而不是多写一层 if else 来增加前端同学的理解成本。
后续文章规划:
《基于自动生成的跨端统一事件通信方案》
《基于自动生成的跨端统一存储方案》
感谢阅读,如果对你有用请点个赞 ❤️