空有宝刀屠龙技,奈何世间无真龙。
释义:本系列的文章在于打破思维惯性,建立超脱技术语言的编程体系。但对于小团队来讲用不上 ,对于大团队来说你站的位置又没话语权,所以只为各位看官扩扩思路。
起因
笔者之前写的大部分文章都算是为前端服务 或者为终端服务的跨端解决方案。本文算是一个新的系列,讲讲我眼中的大前端解决方案是什么?
前端开发现在大都是泛指 Web 技术开发。
终端开发泛指 App / 桌面客户端 / loT 等等。
跨(多)端开发一次开发,在多终端可用。
有同学肯定会疑惑,毕竟大前端的理解不同的前端岗位有不同理解。
node.js + Web前后全栈算大前端吗?算。
H5 / 小程序移动端竖屏开发算大前端吗?算。
RN / Taro / UNI / Flutter / Electron跨端语言开发算大前端吗?也算。
但笔者觉得,这些从整体前端架构体系上来说,只能算技能框架体系,不能说是大前端的解决方案。
比如以笔者公司举例,PC 端 Web 编辑器起家,以上这些技能公司都用或者都有用过,但他们基本上都是独立的工程:
- 需要有个小程序,用 Taro 建设一个小程序项目。
- 需要桌面端,用 Electron 建设一个桌面端项目。
- 需要嵌入 App,就单独开发一套竖屏页面及逻辑。
- ...
大部分公司,甚至各个大厂也是这么干的,最多就是模块化做的更好,逻辑组件封装的更优雅。
原因很清晰,在不同的端/平台上,选择这些框架体系是常规通用做法了。
问题也明显,扩展的端或平台越多,需要维护的前端人力就越多,成本就越高,在降本增效的现实下,最后能做的就是砍掉维护不动的端或平台。
这很常见,但在技术层面其实不合理:
- UI 层面无论用什么框架,响应式布局都可以解决大部分横竖屏适配问题,特殊页面可以用不同的组件来封装页面。
- 逻辑层面对于业务来说,逻辑上就应该是一致的,端和平台差异很小,也都可以通过抽象协议来抹平。
当然,本文不是想说构建一套全家桶方案,可以解决所有平台或端的开发难题(凡是吹捧一次开发,全端可用的,笔者概不恭维,一到各种细节上就十分拉胯)。
本系列恰恰相反,从各种细节入手,从前端/终端角度来阐述,笔者眼中开发所需的解决方案。
前言
回归正题,本文具体介绍的是事件通信如何在大前端里应用。
先回想一下 React / Vue 这种 SPA 单页应用为什么能流行起来?
有一个重要的原因就是它把之前页面间传参转变为可以统一进行的状态管理,消除了页面隔阂,无论是跳转多少个页面,都可以在保存用户操作的基础上刷新数据,这在传统多页开发上是很难做到的。
状态管理本质上就是发布订阅来响应变化。
而现在还没有一个统一的方式可以解决跨端、跨容器、跨标签页场景的事件通信问题:
- 同浏览器多个 Web 标签页,没办法同步数据,需要用户手动刷新。
- 桌面端多个 Web 页面,现在是用很多业务相关的桥接来提供事件通知能力。
- App 也一样,App 与 Web 页面也是通过桥接来构建业务事件通知。
当然,跳开大前端、终端思维限制,更大维度的事件通信是前后一体,用长链接的方式提供多设备多端多容器状态统一,但长链接需要前后端沟通,且公司能负担这一部分价值成本。
当然对我们方案来说,后续接入长链接,对业务使用来说还是无感知的,甚至服务端也是可以统一消息定义,来保证约束一致。
当然,除了第一点是历史遗留问题。
2、3点从实现上现在也是可用状态,但使用方式不统一,桥接的侵入导致需要多出来 if 判断,这并不利于代码的维护。
那势必我们需要一种覆盖全端的事件通信机制,让事件发送方和接收方都不再受所在端、所在容器、所在项目(业务)限制,同时又能统一对其管控,毕竟无管理的事件通信往往是一场灾难,这和前端禁止任意使用watch
监听变化一个道理。
应用场景
这有很多的应用场景,简单举例2个遇到过的痛点:
- 解决一直存在的 Web 编辑器与用户资产之间的需刷新响应问题:这不仅是桌面端上,更是浏览器上多标签页一直以来的痛点:编辑器存入我的空间,我的空间那边不会主动刷新。我的空间增加文件夹,编辑器需要刷新才知道有新文件夹。
- 解决 Web 多标签页 / 端侧多容器情况下,用户信息、会员信息变更不同步的问题。
目标
目标
覆盖大前端(终端)链路的事件通信机制:
- 能互通事件发送方和事件接收方不再需要是同一个工程,甚至可以是不同语言的工程项目中。
- 有约束完备的规范约束,来保证调用链路正确,毕竟都不在一个工程或者一个语言体系中,各个开发组同学相互之间需要一种优雅的协定方式。
- 可观测不仅事件定义是可明确观测的,事件接收方也可以观测到通信来源及堆栈。
分析
设计关键点:
- 事件通信跟状态管理还有所不同,事件通信是去中心化的,没有管理的概念,所以更需要人为约束。
- 事件通信是实时的,发布订阅机制,不做任何缓存(发送时当前有监听就有收到,延迟监听就收不到)。
- 提供合乎各端习惯的 API,以前端举例:onXX(监听)、offXX(取消监听)、onceXX(一次监听)、emitXX(触发)。
- 事件通信必然是1对多的,1个发送者,N 个观察者。不存在也不可能存在一对一通信。
- 基于上一点,如果业务需要1对1的事件监听,应该是根据事件参数中的某一个或多个字段,这一点可以由平台参数设置,生成带必填参数的事件双方,减少业务侧 if 判断,降低业务逻辑圈复杂度。
- SDK 内部实现各端侧发送事件带有当前调用端及简略调用堆栈信息,接收方可以获取到是哪端哪句代码调用的,用于排查问题。
方案
关于 GNB 详见:跨端通信终结者|看我是如何保证多端消息一致性的
以从 Web 侧事件调用为例:
看起来需要建设的东西很多,但很多能力是可复用的。
列举下整套事件方案要建设的内容:
- 通信链路(App 链路、桌面端通信链路、Web 通信链路、Flutter、Rust 等)
- 事件定义平台
- 代码生成(codegen)
- 跨端事件通信协议(GNB)
- 包生成管理
- CI/CD
结果及产物
产物包括 Web、App 在内的各个端或者语言的 SDK,用于标准化的收发消息。
收发消息也是通过代码生成出标准的 API 触发/监听方法,用于规范使用:
因为事件通信不像 UI 那么具象化显示,以桌面端举例下内部消息堆栈:
具体实现
代码生成细节和 GNB 不在本文讲了,以前的文章都有涉及。
通信链路
这里主要讲一下前端通信链路:
iOS、Android、桌面端的通信原理其实很简单,构建一个 EventManager 用于转发消息给自己或者各个 Web 容器或者 Flutter 容器即可。
跨标签页通信
这篇文章介绍了8种跨标签页 API 调用方式跨标签页通信的8种方式。
通过我们事件的定位来说,适合我们的有:
其中 StorageEvent 需要使用 session storage
但使用 session storage 还是会有内存缓存,如果变更的数据一致的,可能不会触发 StorageEvent,这需要额外处理,比如增加时间戳之类的实现,也会在内存中留下很多冗余数据。
所以倾向使用 BroadcastChannel,它这个 channel 定位完美匹配我们的场景。
API 示例:
javascript
// 连接到广播频道
var bc = new BroadcastChannel("test_channel");
// 发送简单消息的示例
bc.postMessage("This is a test message.");
// 简单示例,用于将事件打印到控制台
bc.onmessage = function (ev) { console.log(ev); };
API 测试结果
单标签页:
自己无法监听自己发送的事件。
iframe 可以监听来自父页面的事件和把事件发送给父页面。
多标签页:
页面可以互相发送。
iframe 也可以互相发送。
iframe 和页面同时监听,另一个页面任意发送,可以同时收到。
标签页拉成2个不同的窗口:
同样有效
safari 测试:
以上皆有效
不同浏览器:
无效
单标签页内部通信
采用构建 CustomEvent 用于事件发送及监听,这样对于整个 window 是生效的,也跟 Vue、React 等框架无关。
基类截图:
构建一个 event-foundation 模块,其中包括:
- gnb.ts 处理跨端的消息通信
- event-bus.ts 处理页面内部通信
- web-channel.ts 处理跨标签页通信
事件定义平台
事件 Key 事件唯一标识,因为事件是一个行为,所以最好是一个动词来描述,比如 user_info_update。
所属业务 项目 / 业务 / 模块来分包,增加业务隔离性,这个最好跟业务来定,比如账户、DAM、编辑器等,无需带端侧标识。
参数集合
- 参数可以录入一个或者多个,会生成 interface 规范外部调用。(尽量不使用 Any,定义规范的类型,事件通信也不应该用于传输复杂数据)
- 是否为标识位:一旦设置标识位,则 API 会强制要求发送/接收方必填该字段。例如:
AccountEvent.emitUserInfoUpdate('${id}', params)
/AccountEvent.onUserInfoUpdate('${id}', (params) => { ... })
包生成管理
原理:前端(包含桌面端)会根据定义生成多模块的 SDK 代码,根据模块的定义 JSON 是否变化,来判断是否发布新的仓库到我们的私有 npm 上。
App 有些差异,是使用application-services 版本号来引入最新的 SDK 包。
CI / CD
这个就比较简单,利用现有的打包服务器资源,定时或手动执行生成脚本即可。
总结
有了以上的事件通信解决方案后,开发者就不用头疼事件在多个端上如何适配,打通各个语言在唯一平台上的通信孤岛。
从这几年的对事件通信的抽象链路上看:
- GDAPI 解决 iOS 模块间通信
- GNB 解决跨端通信
- 本文从而解决多语言多端以及内部事件通信问题。
相辅相成,看起来抽象越来越高,但原则是不变的,提供易用可用规范的 API 解决开发维护问题。
后续
除事件通信外,存储共享也是需要打通的,下一篇介绍下大前端解决方案 · 存储。
感谢阅读,如果对你有用请点个赞 ❤️