转转Hybrid体系建设历程
前言
H5 在转转 App 中有着较高的比重,承载了 B2C,C2B,C2C 多个业务的巨大流量,转转在前端技术这块,考虑到业务架构和人员结构的情况,没有采用 ReactNative,Weex,小程序等技术,一直在 Hybrid 领域进行深耕。
从 App 诞生之初直到现在,转转的 Hybrid 体系经历了一些迭代和演进。之前公众号历史文章曾经发布过《转转Hybrid体系建设》,这篇文章是站在前端视角为我们展现了其中一部分内容。今天我尝试客户端的视角来回顾下转转 Hybrid 体系的建设历程,在这个建设历程中,有着一些我们的困惑、挫败、思考以及喜悦,希望与屏幕前的你,一起分享。
几个时期
转转的 Hybrid 体系建设,经历了几个重要的时期,我把总结为下面三个阶段:
-
野蛮生长 -
混乱之治 -
性能优化和开发调试体验提升
下面,我来详细阐述下这三个阶段。文章比较长,信息密度不大,一起来吧 :)
1. 野蛮生长时期
在转转 App 发展的前几年,大概是 2015 年 ~ 2020 年上半年这段时间,Hybrid 缺少系统的规划,我们刚出茅庐,Hybrid 在我们眼中是什么呢?
一个WebView,加载一个 H5,将前端内容加载展示出来,H5 JavaScript 与 App 进行通信交互,仅此而已。
我们当时还不能很好的意识到,一个成熟的或者理想中 Hybrid 体系应该要建设成什么样。
这时候 App 承载的业务形态在不断的尝试中,前端与 App 的通信接口,也在野蛮不断扩充添加着。A 业务今天来了个需求,说我们需要加一个通信接口,OK,前端和 App 同学约定好,测试了下没发现问题上线🚀。B 业务明天来了一个需求,说我们也需要一个,OK,再来,接口不断添加着。
从开发过程,到测试过程,最后到上线运行,问题也随之慢慢的暴露出来。
1.1 开发过程
我们没有接口相关的文档或者非常的缺乏,前端同学在开发使用的时候,无法很好的知晓 App 目前有哪些能力可以使用,以及接口的参数和返回值是什么样的,更多的是靠查看代码和询问客户端同学,非常的不方便,沟通成本还是挺高的。
另外就是由于没有合理的规划和接口分类,造成了不少接口的功能是类似的,但实现上有一些差异,会给前端同学在使用上造成困惑,究竟用哪一个才对。
1.2 测试过程
在通信接口开发时,客户端同学自测怎么做的呢?Android 和 iOS 客户端内置了一个 html 文件,里面有一些通信的接口,每次都往这个文件添加,然后编译到手机上进行测试。
非常的不方便,每次要添加接口都要修改本地内置的 html 测试文件,然后进行编译,但并不是所有人都可以正确的编写这个 js 测试接口。
1.3 上线运行
线上页面出现问题,客户端同学是比较抓瞎的:
-
页面地址是啥? -
问题发生时 js 调用了 native 什么接口?参数是什么?有没有问题? -
是离线包引发的问题吗?是否可以通过关闭和打开离线包进行验证和定位问题? -
···
一方面是我们的日志上报还不够全面,另一个是没有方便的小工具可以直接查看,只能通过本地调试 app 查看断点数据和本地 log,排查定位问题的链路比较长,有些低效。
另外一个显著的问题就是线上我们的 WebView 出现了安全问题,有骗子利用转转没有限制页面打开或者限制页面存在漏洞的情况,引导用户在 App 内打开骗子网页完成支付,用户被骗投诉到平台,给我们的口碑也造成了不小的影响。
2. 混乱之治
上面的问题已经暴露,加之本身 WebView 也开始面临着不少问题,重新设计一个全新 Hybrid 架构体系迫在眉睫。建设这样新体系囊括了客户端 WebView 的重构,以及相关平台的建设,再到 JS SDK的优化,涉及到了整个大前端基建层面。
我们在 2020 年中的时候,基本已经完成了新的 Hybrid 体系设计,截止到目前,整体是这样的:
2.1 WebView 重构
历史 WebView 面临着诸多问题,比如
-
存在大量历史废弃代码 -
代码冗余,业务耦合严重 -
Native接口设计存在不合理的情况 -
代码全部集中在主工程内 -
无法对多 App 提供能力 -
代码过于集中不便维护 -
Webview本身功能、白名单、Cookie管理等混合在一起,且不具有对外扩展性 -
缺乏 Hybrid 接口的分组设计以及权限控制,缺乏安全性
在 Android 和 iOS 两端,我们重写了 WebView,并积极的采用了 Kotlin 和 Swift 语言。重写之后,将上面面临的问题就全部解决掉了。上线之后,经过一段时间的灰度,对存在的问题进行不断优化和调整,最终进行了全量,且将项目的 legacy webview 代码全部删除下线。
至此,我们在混乱治理中,迈出了最重要的第一步。
2.2 DSBridge的兼容引入
2020 年 5 月 6 号,转转和找靓机合并,找靓机的整个 WebView 体系和转转还不太一样。找靓机使用的基于开源方案 DSBridge 的通信方式。
DSBridge这套开源方案其实挺优秀:
-
同时支持同步调用和异步调用 -
支持以类的方式集中统一管理 API -
支持 API 命名空间 -
支持调试模式 -
支持 API 存在性检测 -
支持进度回调:一次调用,多次返回 -
支持 Javascript 关闭页面事件回调 -
支持 Javascript 模态/非模态对话框
优点有很多,但这些优点并不是我们考虑的重点,合并融合后,我们需要考虑的重点是:
-
找靓机侧 H5 如何运行在转转 App 上 -
转转侧 H5 如何运行在找靓机 App 上
核心就是如何统一 WebView,更多的是考虑如何让找靓机的 WebView 平滑过渡到转转的 WebView 体系中来。
转转使用自定义的通讯方案,有 200多 个 API,找靓机采用了 DSBridge 通讯方案有 100 多个 API。要考虑到 H5 在多 App 的运行该怎么做呢?
-
统一的 WebView 容器这个是必须的,意义重大,意味着后续多 App 共享核心的 WebView 架构体系,享受统一的功能升级和 Bug 修复,享受统一的平台能力 -
统一的基础能力接口,如路由、缓存、多媒体、设备、界面、位置、分享等等,这些能力独立出单独的组件,进行多 App 复用 -
部分暂时无法统一的接口能力,JS Adapter 抽象出 H5 接口,内部进行多 App 差异化调用,抹平差异 -
考虑到历史接口兼容问题,存量的历史接口,仍然保持原有的通信方案,新增接口统一按照转转目前推荐的标准形式进行新增
在 App 早期,使用 iOS 7 系统的用户还有一定的占比,为了兼容这些用户,那时候我们并没有使用 WKWebView。
UIWebView 我们采用的是基于 iFrame url 拦截解析获取到执行函数名和参数的方式来实现 JS 调用 Native。后面随着 iOS 7 用户的缩小,以及 iOS 7 带来的额外维护性问题,我们逐步放弃了 iOS 7,应用最低的支持版本从 iOS 7 变成 iOS 8(现在最低iOS 11),此时历史 UIWebView 的通信接口 API 作为存量,很多业务在用,也一直保留到了现在。
取而代之的是采用 WKWebView。毕竟 WKWebView 相比 UIWebView 有着非常大的优势,独立进程和更好的 JS 引擎带来的性能提升以及 WKWebView 在稳定性、内存管理、兼容性、安全性、后台加载等方面的表现都更优于 UIWebView。
所以,在转转 App 中,由于 iOS 7 的历史原因,存在着两套通信方式:一个是兼容 UIWebView 的 iFrame url 拦截解析通信的方式,另外一个就是 WKWebView 的 WKScriptMessageHandler 协议通信方式。
融合初期,为了支持找靓机的 H5 通信,转转 iOS 这边移植新增了 DSBridge 的实现,使得找靓机的 H5 完美运行在转转 WebView 容器之上,此时,接口调用的 Native 能力仍然是找靓机侧的。
后面随着融合的不断加深,基础能力也在不断的扩大,仅有一些少数存量的接口仍然会调用到找靓机的 DSBridge 老接口 API,大部分都会调用到客户端提供的基础能力 API 以及找靓机新增的转转式规范接口 API。
2.3 平台的建设
在整个 Hybrid 体系建设中,平台的建设是不可忽视的。我们不断收集一线的反馈,和前端支撑组不断磨合,由前端支撑组打造出统跳、接口管理、离线包、预渲染等平台。这几个平台和客户端关系紧密,还有一些其他平台如前端的性能平台、异常监控平台、报警平台等等,和客户端这边关系不大,这里就不展开说了。
-
统跳平台:在 Hybrid 中进行路由跳转的信息,由统跳平台统一管理维护
-
接口管理平台:解决 Native 提供的接口能力的增删改查问题,修改后,一键同步更新到前端文档页面。
-
离线包和预渲染管理平台:解决离线包配置和预渲染配置的平台
2.4 规范!规范!规范!
无规矩不成方圆,没有规范,就容易引发破窗效应与混乱,长期下去,积重难返。
2.4.1 WebView基本规范
WebView Cookie、UA、Url Query、通信格式规范,详情见:转转Hybrid-SDK重构和实践 一文中的『转转解决方案』部分。
2.4.2 安全规范
骗子通过聊天向用户发送一些骗子网页,用户中招被骗钱后投诉到平台,这样的情况我们遇到了不止一个。后面我们经过白名单的多次升级,逐步完善了整个白名单体系。
从添加域名的审批,到域名的定时清理,以及精细到 url path 级别的授信级别,使得近两年来,骗子通过钻白名单漏洞去行骗的的情况几乎被完全封杀掉。
2.5 大禹 - 重点解决多 App 接口表现不一致
我们经常收到前端同学的吐槽:
-
为什么同一个接口在转转iOS、Android上表现不同呢! -
为什么同一个接口两端回调的数据格式还有这么多差异! -
为什么同一个接口转转 App 和找靓机 App 入参和行为表现这么多不一致呢! -
为什么???别说啦别说啦💦(逃~)
有一段时间,我们会被拉入各种各样的群里,面对群里抛出的类似上面的问题,解决起来非常的心累。
不行了,得专门搞一下了。遂立项大禹,治水行动开始。
治水基本方针:
-
能统一的基础能力如扫码、分享等等,客户端要统一起来在基础能力层实现,避免转转、找靓机各自实现导致的参数、行为、回调的不一致。虽然前端 JS Adapter 可以做一层适配,但工作会非常繁杂,在客户端层面统一也有利于多 APP 基础能力的复用。 -
对于强业务实现的一些能力,比如登录 login,仍然交由各自 App 实现,这种情况,JS Adapter 需要抹平差异。
经过大禹多期的优化,我们将页面栈管理、分享、相册选图、日历、扫码、通讯录、通用存储、剪贴板、系统权限等多个基础能力,进行了统一。没有数据统计,这个带来了多大的收益,但是明显的感觉,被拉群的次数少了,之前的一些长期被吐槽的接口也很少再收到吐槽了。
但大禹治水不能停下来,这是一件需要长期坚持做下去的事情。
3. 性能优化和开发调试体验提升
3.1 初期的优化
性能优化其实不是最后才开始做的,而是从头到现在,一直在进行,只是阶段性的重点不太一样。
早期我们在大前端领域优化手段做的比较有限,包括了
-
公共资源预加载 -
使用了离线包加快资源的访问速度,缩短页面打开的白屏时长 -
离线包图片骨架屏优化白屏问题
除了上面几点,前端同学也在积极的尝试各种优化方案,包括但不限于:
3.2 Kraken的尝试
2021 年是 Flutter 高速发展的一年,但是官方的 Flutter for Web 并不如人意,此时阿里的 Kraken 北海(目前的WebF)方案的出现,让我们看到了另外的一种可能。
Kraken 方案上层采用 Web 技术栈进行开发,无论是 Vue 还是 React 又或者其他如 Angular,通通不限制,底层使用 Flutter 引擎自渲染,保证多端的一致性,使用 AOT 编译将 Kraken 编译成机器码,提供接近原生的性能。由于上层基于 W3C 标准实现,所以拥有非常庞大的前端开发者生态。
我们联合前端同学,在 Kraken 方向做了一些尝试:将转转内的“附近的人”页面,试着用 Kraken 实现。
通过这个页面的改造,我们看到了 Kraken 还存在了许多不足,还不足以支撑我们在线上大规模使用。
存在的问题包括但不限于下面列举的一些:
-
表现不一致问题 -
CSS 定位、布局表现与浏览器表现不一致 -
部分 API 表现与浏览器不一致(getBoundingClientRect等) -
iOS,Android系统表现不一致 -
重大 Bug -
页面初始化渲染完成,动态修改元素样式,DOM不重新渲染 -
滑动监听计算导致 APP 崩溃 -
调试成本高 -
不支持 vue-router,单项目单路由 -
不支持热更新,npm run build 预览 -
不支持 sourceMap,无法定位源代码 -
真机调试只支持 element 和 network;dom 和 element 无法互相选中;无法动态修改 dom 结构,无法直接修改样式....... -
页面白屏,假死 -
安全性问题 -
无浏览器中的“同源策略”限制 -
兼容性 -
npm包不兼容等
由于上面存在的问题带来的高额的开发调试以及维护成本,我们暂停了在 Kraken 方向的投入。但对后续也就是目前的 WebF 仍然保持着关注。
3.3 帝江 - 一次性能和开发体验的飞跃
3.3.1 性能提升
除去 Kraken 等方案的尝试,我们积极的在探索和寻找其他优化方式,最终确定了离线包、预渲染、预加载、预请求、接口请求代理、图片缓存共享等多个优化手段。
离线包消除静态资源下载耗时、预渲染消除⻚⾯初始化耗时、预加载提前缓存常用静态资源、预请求提前获取主屏数据解决⻚⾯跳动和⽹络请求链路⻓、图片缓存共享将 Native 图片缓存共享给 H5 提供视觉占位等,通过一系列多级优化方案,前端可以灵活的组合上面的优化手段,使得性能体验大大提升。
使用预渲染的页面,就是秒开的体验,平均首屏时间从 1500ms 下降到 400ms 左右。关于预渲染这块,后面有机会的话,我们会单独再开一篇文章一起深入聊聊。
上面提到的这些方案具有正交性,可以随意进行组合接入。
正常无优化的 H5 加载展示流程:
离线包:
预渲染+预请求:
无预渲染+预请求:
预渲染+预请求+图片缓存共享:
下图点开可以动
3.3.2 开发调试体验
我们很关注前端开发同学的开发、调试体验,以及在线上排查问题的时候,客户端能做什么可以更快的帮助定位问题。
基于这个简单的愿景,我们开发了 WebView 小工具。
概览:在概览中,我们可以清晰看到 API 接口日志,离线包,预渲染等优化手段的命中情况,此外我们也可以直接控制 eruda 调试工具以及 UI 走查小工具的开关,以及页面的 Url,Cookie,Query,UserAgent 等情况。
API接口日志: 详细记录了 JS call Native 的每一个接口,包括 request 和 response 详情。
预渲染: 记录了预渲染当前的下发配置,以及预渲染模版的当前状态,可以方便查看模版是不是已就绪还是渲染中或者被加入了黑名单。同时也添加了客户端的预渲染日志,方便排查页面为什么没有命中预渲染。
。
预请求:记录发出了哪些预请求,以及这些预请求的 request 和 response 详情。
离线包:开启和关闭离线包功能,删除离线包缓存,以及展示离线资源的命中记录。
接口代理:展示接口代理记录以及接口的 request 和 response 详情。
图片缓存共享:记录缓存共享请求,包含目标图片和命中图片地址。
后续规划
文档建设
持续优化前端的接口文档以及平台,在接口的用途、参数和响应描述方面力争清晰和准确,且对接口的升级改动形成变更历史等,方便追溯和兼容。
埋点统一和监控
统一大前端的埋点,建立更全面的埋点和监控,如页面白屏时长、大前端全链路埋点监控等,实现对 H5 页面的全方位把控。
性能优化
还有很多事情需要做,如
-
进一步提升离线包、预渲染的命中率 -
智能预渲染,减少不必要的模版渲染 -
启动时的预渲染,进一步降低对 App 主线程卡顿的影响 -
保持对 WebF 等技术的持续关注,探索应用的可能性 -
···
鸿蒙
HarmonyOS Next 的即将出现,意味着要基于纯鸿蒙系统重新构建一套 WebView 解决方案,整个架构体系和当前可能是一致的,但可能会融合鸿蒙系统的原子化服务以及多设备流转等特性,提升用户在 HarmonyOS 上的 Web 使用体验。