前言
随着互联网的发展,手机已经成为互联网的最大入口,这一点相比大家都习以为常了。手机上的软件五花八门,大家点击下图标就可以进入到丰富多彩的应用之中,虽然操作流程都是一样的,但是底层技术不一样的。按照开发技术来分,App可以分为三大类。
- 原生应用-native App
- Web 应用-Web App
- 混合应用-Hybrid App
这三类APP的技术模型都不一样,各有优缺点,可以根据业务需要选择其技术模型。从小编自我感知的趋势来看,Hybrid App 的应用范围更广一些,主要体现在小程序、RN等技术体系的兴起,这也是本篇文章重点介绍的技术体系。
混合 App (hybrid App)顾名思义就是原生 App 与 Web App 的结合。它的壳是原生 App,但是里面放的是网页。 可以理解成,混合 App 里面隐藏了一个浏览器,用户看到的实际上是这个隐藏浏览器渲染出来的网页。
Hybrid App 一般分为一下几层:
- 网页层,由 HTML + CSS + JS 组成。
- WebView层,又称网页引擎层,是原生对内置浏览器的管理层
- 原生外壳,又称容器。
其中网页层和WebView层的桥接一般基于 JS bridge 来实现。Hybrid App 最突出的特性是 Webview 可以通过 JS Bridge 来调用底层操作系统的所有 API,可以突破浏览器的限制实现更加酷炫的交互效果和功能。
这里大家可能比较疑惑 Hybrid H5 决定性优势是什么?上面提到的酷炫效果,原生开发可以做的更好啊,性能也更优秀。做过原生开发的同学肯定了解原生发版的困难,要兼容两套系统、发版要审核(IOS赶上国外节假日的话,要哭死😭既是苦恼也是羡慕,今年除夕不放假😭),H5的发版则是更加灵活,更加适应现在快速多变的市场需求的,非技术之罪。
Hybrid H5 虽然有很多优点,缺点也很明显。
- 体验和原生相差甚远;
- 开发调试困难、要基于原生的壳子调试;
- 由于操作系统、手机厂商之间差异,引起多种多样的兼容问题;
回到本文的主题-Hybrid H5 原生体验优化实践,本周主要是讨论如何改造旧 Hybrid H5 项目实现与原生体验一致的展示效果,老树换新芽。
实战业务场景
本人在实际业务开发过程中,承接一个旧 Hybrid H5 项目的质量治理工作,希望这个APP能够更好服务客户,实际上就是希望体验更好一点。
我简单梳理下了该项目的项目架构,十分简单,一个原生壳子,内部所有页面均由 H5 来实现,不同模块页面之间的集成采用的微前端架构。随着项目体积逐渐扩张,其页面性能急速下滑、且crash率居高不下。这里介绍 crash 率高的原因,由于是微前端架构因此任何子应用加载失败、JS边界错误都会导致整个应用crash,无法恢复只能依赖于客户杀掉应用进程重新进入APP,anyway,大家都快崩溃了。
如上图(业务保密用其他APP截图代替)所示,其页面体系由登录页、首页瀑布流、二级详情组成,其中首页瀑布流有5个,底部常驻一个底部操作栏,用于切换;
WebView 的初始化过程:
H5的渲染过程:
其页面加载过程如下:
-
主应用加载
- 前端资源加载
- Layout 布局初始化
- 用户信息从原生存储同步
-
子应用加载-五个主页访问
- 先远程加载子应用的资源清单
- 前端资源加载
- Webpack异步chunk框架加载
- 数据请求
-
异步路由加载-命中经过分包的页面
- 前端资源加载
- 数据请求
从上面的介绍,你可以认为这是将PC端的SPA框架搬到了移动端了,其框架名称也十分有意思-SPA架构;该架构也不是全都一无是处的,其中比较关注的优点:
-
开发比较高效,数据共享基于全局 Redux-store 即可(无微应用加载无JS隔离)
-
除了首次加载之外后续页面访问和切换较快、无白屏,体验近似原生;
在整个拆解过程中,我将优化的目标确定:
-
整体页面 LCP 数值减少 30%;
-
首页Tab的体验接近原生,Tab 持久化,滚动切换,体验对标招商银行 APP;
-
SPA 架构拆解为 MPA 架构,开发和构建成本尽可能减少;
-
页面 Crash 率即页面白屏率,下降到单日个位数;
架构设计
原生改造
在经过和原生开发同事、产品经理沟通之后,首先确定了针对APP首屏渲染优化的设计,经过长时间的迭代最终形成了下下面的APP首屏渲染的过程。
其中依赖于原生开发的强力支持,对底层 JS Bridge 的架构进行了优化,新增了很多优秀体验设计:
- 增加针对 Web 的文件存储和内存存储功能,H5可以依赖原生 Action 读取全局数据;
- 增加新开 WebView 容器的能力,二级页面打开采用新开Webview;
- 五个首页 Tab WebView 常驻,并且增加滑动切换;
- 核心后台接口在冷启动阶段交由原生托管,用户信息ready之后启动 WebView 的加载;
- 弹窗、公告、推送等在APP内需要持续监测的交互交由原生实现,简化功能设计;
这里前端资源缓存能力也很重要,旧工程架构中已经实现了该能力就不再赘述。
这个流程设计解决了以下瓶颈:
1、登录页更快,且可以离线启动;
2、核心数据接口在冷启动阶段发起,尽可能早的准备好数据和存储好数据,前端渲染可以更早开始;
3、微前端架构拆除,只需要加载当前页面资源,性能更好;
目标很明确,就是尽可能压缩客户等待页面的时间。在实现过程中并不是一帆风顺的,遇到各种各样的 bug 和实现瓶颈,本文篇幅有限没办法面面俱到,有兴趣的朋友们可以把遇到的问题在评论中说明下,我会尽快回复的。
H5 改造
在"实战业务场景"中我们提到微前端架构随着项目规模持续增大之后,性能开销和 Crash 率令人崩溃,接下来介绍下如何逐步干掉微前端框架,适配 Webview 多开的业务场景。这部分是压力最大的也是付出的成本最高的,目标是逐步切换,页面逻辑保持最小改动,解耦主子应用的关联。
新框架相对于老框架最大的挑战在于微前端的拆除和二级页面跳转改成Webview 多开(前后页面缺少能够通信的基础)对框架和业务逻辑都具有很大的挑战。
- 模拟页面生命周期和路由事件
首先原应用状态存储是采用host的Redux-store来进行存储和更新触发,其次使用react-router监控路由变更实现页面之间的联动,再者EventEmitter进行公共组件(公告、弹窗、岗位切换)的广播。在新框架中,这部分能力使用原生能力来承接;
再则将原生的Webview的active、二级页面返回上一级页面、noactive,传入 JS 环境,通过 HOC withRouter注入容器组件,形成 "componentDidActivate"、"componentOnGoBackLoad"、"componentDidLeave"等生命周期。同时在内存模拟 history 的堆栈,在即将返回到常驻首页时,清除所有二级页面 WebView,减少原生内存开销,同时维护生命周期。
- 模拟全局存储
交由原生在冷启动阶段请求的核心接口数据会注入到原生存储空间,首页容器初始化之后也会将 redux-store global 空间下数据同步过去,这样二级页面在初始化阶段就会读取 global 数据注入到本页面的 redux-store 中,兼容旧的业务逻辑。
- History API替换
之前路由history.replace、go、goback、push等API,其内部执行逻辑替换成原生的 WebView 的操作,并且作为 React-Context 注入到组件内,尽可能减少改动;
其次增加灰度配置进行局部替换,即 push 逻辑先判定是否属于已经 MPA 改造的独立页面路由,若属于则打开的新的 WebView 跳转该 MPA 页面路由,否则调用默认 History.push 触发默认的子应用加载和异步路由加载,实现业务开发和工程改造并行。
这里难度最大的问题要解决调换环路的问题,就是你跳转 A、B、C、D、B这种场景,最后的B是新开 Webview 还是要返回 Webview 栈中的 B 页面呢?这个是依据场景其正确的选择是不一样的。我们选择的方案是按照 B 的地址是首页 Tab 地址,是则返回首页 Tab,并销毁当前 Webview 内所有页面,否则继续新开 Webview 容器;缺点则是要对部分流程表单场景下页面的跨页面协作的逻辑进行整改。
鉴于文章篇幅有限,以上技术方案仅仅介绍的抽象设计,代码设计就不贴了。
成果
经过接近一年的努力,整体方案的实现接近尾声,已经开始生产APP的灰度发布了,目前整体运行没有发现重大问题,领导和开发对其体验反馈整体是正向的,终于可以开香槟庆祝了🎉🎉🎉。
预期改造目标实现情况:
- 首先是 MPA 改造 + 工程构建优化将 LCP 的中位数从 2435ms 下降到 1213ms;
- 首页体验优化则是在此基础上将原首页瀑布流的 LCP 中位数从 3093ms 下降到 2617ms,其他首页 LCP 中位数 分别为 944ms、1167ms、1432ms 基本靠近了秒开的体验;
- 除第三方集成的子应用依旧采用微前端加载,自有维护工程均已解耦,开发按照工程独立维护业务;
- 页面 Crash 率下降到个位数,且无需杀掉 APP,点击页面恢复按钮即可重试