提升海量用户极致体验的Hybrid架构设计

2020 年 2 月 21 日

提升海量用户极致体验的Hybrid架构设计

引言


上一篇原理篇,我们已经详细地阐述了 Hybrid App 的基础原理,了解了 Native 端 和 H5 端 是如何通信的,还有 bridge 的设计和接入。而本篇文章将开始把这些原因进一步实践,用代码真正地去实现一套完整且稳定的 Hybrid 方案。如果对原理还有疑问的小伙伴,请移步提升海量用户极致体验的 Hybrid 架构设计(原理篇),只有在理解了理论的基础上,进一步与实践相结合,才能真正地去深入一项技术。


摩天大楼


说了那么一大堆理论知识,可能有小伙伴会说:“ 你是不是吹流弊啊。”那就先来简单介绍下我们已经使用这套方案落地的项目之一。



这是一个完全内置在 App 里的 Hybrid 模块,由 Native 与 H5 深度协作完成,总共有 4 个页面,其中首页和制作页由 H5 制作,而相机页和保存页是复用 Native 页面。


项目上线一年累积使用次数已经超过 10 亿次。这套方案经受住了考验,并在过程中仍然在不断的优化和拓展。


使用这套实现方案是基于以下几点考虑:


  • 整个模块的风格多变,整体UI是与妆容所搭配的,而整个模块一直都在持续不断的迭代之中;

  • 项目逻辑流程的可变性大,需要H5强大的热更新能力,及时应对数据的变化,快速的试错和纠正;

  • 拍摄页与保存页是客户端已经有的模块,可以略微定制后直接复用;

  • 需要由客户端协助接入多套SDK,例如使用算法SDK进行复杂的图像处理。

  • 简单看完项目,我们接下来开始 bridge.js 的构建。由于本系列文章主要面向前端童鞋,因此我们主要展开 H5 的部分,即会注入到每个页面头部的 bridge.js 的实现,客户端中的 SDK 部分就不详细解构了,只会提到一些细节。


搭建地基 — bridge.js 架构


基于上篇文章阐述的结构,我们进一步去完善细节部分,先整理成下面这样的流程结构图,大家先看下图,有个大致的概念:



接下来我们会细看里面各个部分的代码实现。


(一) 业务方使用姿势


首先,我们先看下在这套方案中,业务方是如何使用的,下面以获取网络状态为例:



(二) H5 --> Native


接下来直接来看 nativeCall 的内部实现:



里面可以解构成下面 4 个步骤:


1.生成唯一 handler 标识,从 0 开始累加;


2.将参数按 handler 值的规则存入参数池(_paramsStore)中;


3.以 handler 注册自定义事件,绑定 callback,并将 callback 也存入 _callbackStore 中,addEvent(),储存的目的主要是为了事件解绑时使用;


4.以 iframe 的形式发送协议,并携带唯一标识 handler,send();



Native:


  • 客户端接收到请求后,会使用 handler 调用 getParam 从参数池中获取对应的参数。

  • 执行协议对应的功能;

  • 这样即走通了 H5 --> Native 的这个流程,在客户端完成了对应的功能后,既开始回传执行结果。


(三) Native --> H5


Native:


Native 完成功能后,直接调用 Bridge.postMessage(handler, data),将 执行结果 和 之前 nativeCall 传过来的 标识 回传给 H5;



H5:


  • H5 在接收到唯一标识后初始化对应的自定义事件,挂载数据后触发,这里涉及的就是 fireEvent 这个函数:

  • 这样,我们就已经完成了双端之间的双向交互机制了,梳理出了整个 bridge.js 的核心代码了,包含了:

  • 最重要的开放API: nativeCall 与 postMessage ;

  • 客户端获取参数函数: getParam ;

  • 事件回调系统中的 addEvent 和 fireEvent ;

  • 用于发送协议的 send。


安卓兼容性


如果看过上一篇提升海量用户极致体验的 Hybrid 架构设计(原理篇)的童鞋,这时可能会有个疑问:在 Android 4.4 以下时,使用的 loadUrl 进行 js 函数的调用,而此时是无法获取函数的返回值的,也就是说 4.4- 时,安卓并无法通过 getParam 这个函数来获取到协议的参数,这里需要做兼容性的处理,而我们这里可以使用一个曲线救国的骚操作,使用到的原理就是上一篇文章中有提到的另一种 H5 -> Native 的方案:WebView 中的 prompt 拦截


方案如下:


  • 当安卓接受到协议,并拿到 handler 值;

  • 使用无兼容性问题的 loadUrl 执行 js:Bridge.getParam(handler) ,直接将返回值直接通过 js 中的 prompt发出:

  • 通过重写 onJsPrompt 这个方法,拦截上一步发出的 prompt 的内容,并解析出相应的参数;


通过这样的方式,安卓全平台都可以完成参数的获取,并且方式统一,不需要分平台兼容,这就非常的 skrskr 啦。


现在看下来,是不是觉得炒鸡简单?。分分钟能写 100 个。没错!其实核心的原理就是这么的简单,但这只是一个最基础的地基而已,而基于地基之上,我们就可以开始一层一层建造我们的大楼了!


建造大楼 — 协议的定制


在完成最基础的架构后,我们就可以开始来进一步完成一些上层建筑了,制定一系列真正开放给业务方使用的协议 API,完善整套方案。


首先我们可以将这些协议分成 功能协议 和 业务协议。


功能协议


这类协议是指用于完善整套方案的基础功能的一些通用协议,以 command://作为通用头,封装在 SDK 之中,可以在全线 App、全线 WebView 中使用:


1.初始化机制


上篇文章有提到由于 bridge.js 注入的异步性,我们需要由客户端在注入完成后通知 H5。


这里我们可以约定一个通用的初始化事件,这里我们约定为 init,因此前端就可以进行入口的监听, 类似于我们常用的 DOMContentLoaded:



大家可以看到,这里用了个标记位用于避免事件被重复触发,这是由于客户端中是通过监听 WebView 的生命周期钩子来触发的,而 iframe 之类的操作会导致这些钩子的多次触发,因此需要双方各做一层防御性措施。


接下来,我们可以通过该事件,直接初始化传给 H5 一些环境参数和系统信息等,下面是我们使用到的:



同样的,我们可以约定更多的页面生命周期事件,例如因为 App 很经常性的隐藏到后台,因此在被激活时,我们可以设置个生命周期: resume,可以用于告知 H5 页面被激活。


Tips:


这里就能体现出我们通过事件机制来作为回调系统的优势了,我们可以以最习惯的方式进行事件的监听,而客户端可以直接使用 bridge.fireEvent(‘init’, data)触发事件,这样便可以优雅地实现 Native -> H5 的单方向交互。


2.打包机制


Hybrid 模块 的其中一种方式是将前端代码打包后内置于 App 本地,以便拥有最快的启动性能和离线访问能力。而这种方式最大的麻烦点,就是代码的更新,我们不可能每次有修改时就手动重新打包给客户端童鞋替换,而且这样也失去了我们的热更新机制。


因此这里就需要一套新的热更新机制,这套机制需要由客户端/前端/服务端 三端的童鞋提供对应的资源,共同协作完成整套流程。


资源:


  • H5: 每个代码包都有一个唯一且递增的版本号;

  • Native: 提供包下载且解压到对应目录的服务,前端可以由下面这个协议来调用该功能。

  • 服务端: 提供一个接口,可以获取线上最新代码包的版本号和下载地址。

  • 流程:

  • 前端更新代码打包后按版本号上传至指定的服务器上;

  • 每次打开页面时,H5请求接口获取线上最新代码包版本号,并与本地包进行版本号比对,当线上的版本号 大于 本地包版本号时,发起包下载协议:

  • 客户端接受到协议后,直接去线上地址下载最新的代码包,并解压替换到当前目录文件。

  • 拥有这样的机制后,H5在开发后,就可以直接打包将包上传到对应的服务器上,这样在 App 中打开页面后,即可以实时的热更新。


3.环境系统 和 多语言系统


通常,我们会将项目分成多个不同的环境,相互隔离。而由于 Hybrid 模块是置于 App 中的,因此环境需要与 App 进行匹配,这里就可以直接使用上面第一点提到的,通过 init 中携带的数据 data.env 来匹配:


env: 0 - 正式环境; 1 - 测试环境; 2 - 开发环境;

同理, 多语言也可以直接使用 e.data.language 直接进行匹配;

Tips:

环境机制我们通常主要用于匹配后端的环境,正式环境和测试环境对应不同的接口。而这里还有一点特别的,就是需要注意代码包的更新,上述的包更新条件要包含三个方面: 版本号、环境和 App版本,在不同环境不同 App 版本下,也应该更新到相应的最新代码包。


4. 事件中转站


由于页面是 H5 开发,而 Native 可能需要控制 H5 页面,例如最常用的场景:


当页面中有弹窗或者 SPA 切换页面时,安卓的返回实体键应该能完成对应的回退,而不是因为 WebView 没有 history 就直接关闭。


类似于这类需求,这里就可以定制一个事件中心(eventListeners ),用于监听客户端的实体返回键:



5. 数据传递机制


在业务中,很多场景需要做到 Native 与 H5 保持数据的同步,此时就可以使用类似上面的原理,制定一套数据传递协议:



Tips:


Hybrid 模块通常需要从对应的入口进入,因此这里有一种可以优化的方式:


由 App 在启动时先去获取线上数据,在进入 WebView 后直接通过 init 或者触发 getData 直接发送给 H5,这样能减少请求数量,优化用户体验。


6. 代理请求


H5 中最常用的就是请求,通常我们可以直接使用 ajax,但是这里有几个问题比较棘手:


  • 最常见的请求跨域;

  • 数据算法加密;

  • 用户登录校验;

  • 而客户端的请求便不会出现这些问题,因此我们可以由客户端代理我们发出的请求,可以定制4个协议: getProxy,postProxy, getProxyLogined,postProxyLogined,其中带有 Logined 的协议代表着在请求时会自动携带已登录用户的 token 和 uid 等参数,使用在一些需要登录信息的接口上。这样做的好处是:

  • H5 方就无需处理繁多的各项复杂信息,不需要进行跨端传输;

  • 能够对 H5 与 Native 的请求出口进行统一,方便加工处理。


7.更多


除了这些重要的功能外,我们还可以非常自由地定制很多协议,让 H5 拥有更多更强大的功能,下面是我们所定制的一些功能:


  • getNetwork:获取网络状态;

  • openApp:唤起其它 App;

  • setShareInfo与callShare:分享内容到第三方平台;

  • link:使用新的 WebView 打开页面;

  • closeWebview:关闭 WebView;

  • setStorage 与 getStorage:设置与获取缓存数据;

  • loading:调用客户端通用 Loading;

  • setWebviewTitle:设置 WebView 标题;

  • saveImage:保存图片到本地;

  • 这里可以定义更多的通用性协议,这里有个原则可以遵守,即这部分协议应该是基础性功能,应该是纯净的,适用于所有的业务方。根据上篇文章提到的理念,这部分是当成通用 SDK 进行维护与升级的,因此不应该耦合业务层的任何逻辑。

  • 而有时我们会遇到需要定制一些业务上的逻辑,例如上面提到的项目中,我们要将用户图片通过算法处理成卡通画。这样的需求就是非常的业务化,不适用于其它项目,因此我们应该定制成业务协议。


业务协议


这类协议区别于功能协议,它们会杂合一定程度的业务逻辑,而这些逻辑只是针对于特定的项目。其实对于 H5 的使用上,差别并不大,只是使用对应特殊的协议头用于区分,例如:



这类协议通常不包含在 SDK 中,因此需要由客户端的童鞋针对项目的 WebView 进行定制,使用 bridge.js 提供的基础功能实现对应的复杂功能。而在其它的项目入口中,就无法使用这些协议。


总结


看到总结两个字,有没有长舒了一口气。通过这两篇文章,我们终于将 Hybrid 方案的前端部分完全的解构清楚了,是不是有种神清气爽的感觉,完全可以马上开启你们的 Hybrid 之旅了。鼓掌鼓掌!


但这也远非终点,或者说这永无终点。~大楼建成后,离真正的摩天大楼还是差着一步 — 内部装修,其实接下来我们还需要做很多的优化措施,来解决一些仍然存在的问题,这部分其实我们也一直还在努力的阶段。


受篇幅所限,有时间会将这部分再写一篇优化篇,主要来与大家探讨下我们所能想到的一些优化方案,非常期待大佬们也能给我们提供更多的建议和解决办法。感恩~~😇


作者介绍:郭晓东,美图前端工程师,一只有梦想、爱技术的前端程序猿。


本文转载自美图技术公众号。


原文链接:https://mp.weixin.qq.com/s/LLGOb0jv2dDPEiMBnEwu5w


2020 年 2 月 21 日 22:51335

评论

发布
暂无评论
发现更多内容

架构师训练营第六周作业

Geek_4c1353

我服了,难倒无数程序员的源码面试,就这样被轻轻松松讲透彻

小Q

Java 学习 源码 架构 面试

分布式文件存储QoS硬核黑科技,真香

焱融科技

高性能 存储 HPC 分布式文件存储 QoS

二十四、深入Python多进程multiprocessing模块

刘润森

Python

SpringCloud Alibaba开篇:SpringCloud这么火,为何还要学习SpringCloud Alibaba?

冰河

分布式 微服务 高性能 SpringCloud Alibaba

数字货币交易所开发,去中心化交易所平台搭建

WX13823153201

数字货币交易所开发

java安全编码指南之:文件IO操作

程序那些事

java安全编码 java安全 java安全编码指南 java代码规范

云原生时代 容器持久化存储的最佳方式是什么?

京东智联云开发者

数据库 云存储

算法训练营毕业总结——以此自勉

Airship

算法 算法和数据结构

一笔订单,但是误付了两笔钱!这种重复付款异常到底该如何解决?

楼下小黑哥

支付宝 微信支付 支付系统 支付

来自朋友最近阿里、腾讯、美团等P7岗位面试题

艾小仙

Java 阿里巴巴 程序员 腾讯 面试

anyRTC与京东智联云市场达成战略合作,携手音视频平台

anyRTC开发者

ios 音视频 WebRTC RTC 安卓

基于阿里云容器的CI/CD落地实践

LorraineLiu

阿里云 k8s Helm jenkins CI/CD

架构师训练营第 1 期第 6 周作业

业哥

USDT支付入金系统开发搭建,跨境USDT支付系统开发

135深圳3055源中瑞8032

搜狗搜索或成为企鹅号流量入口:腾讯欲实现自己的流量闭环

石头IT视角

「深度解析」告诉你如何选择容器存储

焱融科技

Kubernetes 云原生 焱融科技 容器存储 分布式文件存储

网易首席架构师2年心血只为趣谈网络协议,内容强不强你说了算

周老师

Java 编程 程序员 架构 面试

分布式关系数据库

韩向民

币币交易所开发,区块链交易系统源码

135深圳3055源中瑞8032

数字货币钱包开发,去中心化钱包源码搭建

135深圳3055源中瑞8032

uni-app支持PC宽屏适配

崔红保

uni-app 前端框架

缓存架构不够好,系统容易瘫痪

架构师修行之路

缓存 微服务 架构设计

甲方日常 38

句子

工作 随笔杂谈 日常

openEuler开源下一代全场景虚拟化平台StratoVirt

openEuler

开源 虚拟化 openEuler stratovirt

架构师训练营 week5 作业

陈皓07

保证缓存与数据库的数据一致性不是很容易

架构师修行之路

缓存 一致性

你用过宏##粘贴函数,然后用函数指针查找执行吗?今天就给你说道说道

良知犹存

c c++

合约一键智能跟单软件,跟单平台开发

135深圳3055源中瑞8032

云开发·多次订阅一次性订阅消息后定时发送

Yukun

微信小程序 小程序云开发 消息推送 订阅消息

WebSocket-技术专题-服务器端消息推送

李浩宇/Alex

提升海量用户极致体验的Hybrid架构设计-InfoQ