本文会对目前流行的基于 JavaScript 的 web 跨端融合方案进行总结和分析,目标人群为 web 方向的从业者但是对跨端融合方案了解不多的人。

web 跨端融合简介

在 2015 年 React Native 发布之前,web 在移动端 APP 上主要通过 WebView 进行承载,其有许多优点,可以快速迭代发布,不特别受 APP 版本的影响,因此,一些快速发展的业务(包括前期的手机QQ、手机淘宝)大量采用了 WebView 内嵌 H5 页面的形式来推动业务。

但是这种方式缺点也比较明显,主要体现在以下两点:

  • 加载时间较长,包括 WebView 初始化的时间、网络请求的时间。
  • HTML 页面在性能上天然不如 Native 页面,无论怎么进行性能优化。

在 2015 年,Facebook 推出了 React Native,从而打开了 web 跨端融合的大门,后续在此架构基础上又出现了阿里巴巴的 Weex(2016)、腾讯的小程序(小程序实际上更偏 web 一点,和其他几类稍有不同,本文不作介绍)、 Hippy(2018)、Taro(Taro 其实更偏向解释翻译,和其他几类定位不同)等跨端融合解决方案,并且渐渐被用到越来越多的项目中,目前,跨端融合开发已经是一种比较主流的 web 开发模式,在阿里系应用、腾讯的微信、QQ浏览器、手机QQ均已经进行了大规模应用。

基本架构

虽然 web 跨端融合方案众多,除了上述提到的三种,还有各个公司的更多方案,但是一般来说跨端融合的技术架构都比较相近,我们可以通过下面这一个图来简单概括:

接下来,我们逐个进行简析:

  • 业务代码:即我们写的 React Native 代码、Weex 代码,一般来说,我们的业务代码需要经过框架工具或者打包工具(例如 webpack 配合 loader)进行打包,从而兼容一些 ES Next 的写法以及一些框架本身不支持的 Web 写法。
  • Javascript FrameWork:这部分主要是针对 Weex、Hippy 来讲的,Weex 声称支持 Vue、Rax 语法,而 Hippy 声称支持 React、Vue 写法,实际上,对于这些库而言,并不是直接将 React、Vue 引入到项目中,而是会对其源代码进行修改(Vue 有针对 Weex 平台的版本),而 Hippy 也是对 React 源代码进行了修改,例如,你写的一个createElement的操作,在 Web 平台中实际调用的是 document.createElement(tagName) 这个接口;而在 Weex 平台中实际执行的是 new renderer.Element(tagName)(renderer 由 Javascript Runtime 提供,并且最终和 Native 通信渲染上屏)。
  • Javascript Runtime:Runtime 的部分,主要是对外暴露了一些统一的接口,比如说节点的增删改查、网络请求的接口等,而这些借口,实际上是其“代理”的客户端的能力,通过客户端 JSAPI 的方式进行调用。另外,把 Runtime 和 FrameWork 进行抽离,也可以便于一个跨端方案适配多个框架,只需要将不同的 FrameWork 和浏览器交互的部分代码转换成 Runtime 提供的标准接口,就可以实现对不同框架的支持。
  • Core:这部分主要是对 Javascript 的解释执行,在 iOS 上一般是 JSCore(系统自带,给客户端提供了执行 JavaScript 程序的能力),而安卓上则可以采用 V8、X5 等。
  • 最下层则是分 Android 和 iOS 端去进行渲染。

发展现状

实际上,React Native 最初提出这种解决方案的时候,市面上并没有同类的产品,但是由于 React Native 的一些问题和其他原因,各个大公司基本都在实现自己的跨端融合方案,这里 React Native 的问题主要体现在:

  • 最主要的是协议风险。
  • React Native 打包出来的 JSBundle 较大,并且默认没有灵活的分包机制,需要自行解决相关问题。
  • 在部分组件比如 List 组件中,性能较差(据非官方说法,性能并不是 React Native 团队首要考察因素,但是国内团队一般都比较重视性能)。
  • 部分事件发送频繁导致性能损失、例如列表滚动事件、手势事件等。
  • 双端 API 大量没有对齐(这也和其 slogan 是‘learn once, write everywhere’ 而不是 ‘write once, run everywhere’ 相对应)。

而对于国内的 Weex 和 Hippy 框架,其都做了大量的性能优化解决了上述问题,并且规避了协议风险(Weex 采用了 Apache 2.0 协议,而 Hippy 即将开源)。

另外值得一提的是,Weex 和 Hippy 都可以在 web 端进行运行,一般可以作为降级方案使用,从而真正做到了“一份代码”,三端运行。

性能优化

实际上,采用目前的跨端融合方案的体验已经比采用 WebView 的方案强太多了,但是性能优化是没有止境的,随着页面复杂度的提高以及用户体验的要求,实际上目前这类跨端融合方案采用了以下几个方向的性能和用户体验优化:

减少网络请求

在我们上述提供的架构图中,一般而言对于一个这类页面,业务代码是通过网络请求加载的,这个时候在加载上主要省去的是 WebView 的初始化时间,这其实是不够的,所以我们也可以采用将业务代码提前下发并存在用户本地,打开的时候只需要从本地拉取并执行代码,这样可以减少相关的网络请求阻塞,优化加载时间。

另外,减少网络请求还体现在对资源的缓存上,对一个页面中所采用的图片等资源文件进行 LRU 策略的缓存,从而防止重复的请求(在传统的 WebView 的方案上,也可以采用对 WebView 增加 Hook 的方式实现)。

当然,以上两点在 WebView 的方案上也可以采用。

降低通信成本

我们从上文的架构图中可以看出,这里的层级实际上比较多,如果不同层级的通信数据较多,并且有比较频繁甚至重复的编解码操作,肯定会有很大的开销,从而影响性能,所以,在不同层级之间做好数据的传递,并且防止重复的编解码操作是比较重要的。

这里可以优化的细节其实比较多,我们举一个 Hippy 的例子:

在 Hippy 架构中,jsRuntime 会生成一个 jsObject 对象树(即需要渲染的 DOM 信息),其在经过 JSBridge 时需要通过JSON.stringify 进行序列化,而在 Java(andriod) 接收端,则需要先将其变成一个 JsonObject,最终转化成 HippyMap,这里实际上是有重复的编解码操作的,我们看看 Hippy 的优化策略:

图片来自 IMWeb 2018

通过 hippybuffer 的方式减少通信的数据量,并且防止重复的编解码操作,可以有效提高性能。

减少通信次数

为了减少在通信方面的消耗,我们除了降低通信的成本,还可以做的就是减少通信次数,当然,前提是不影响用户体验。

这方面可以减少的通信消耗,其中一个方面是频繁的事件通信,我们知道,事件的触发是在 native 端的,但是事件处理的逻辑代码实际上是在 js 层来完成的,在这方面的通信,React Native 就因为频繁的通信从而影响了性能。

我们可以优化的地方在于,首先减少没有绑定回调函数的事件通信,一般而言这部分通信是不必要的,其次是多次通信可以进行合并,比如说 list 滚动回调函数、以及动画通信,我们可以通过配置驱动代替数据驱动的方式(即一次向客户端传递整个配置,后续相同事件可以直接在客户端进行处理),来减少通信次数。

这方面 Hippy 和 Weex 都有大量细碎的实践,在此便不具体介绍了。

降低首屏时间

在原来的 WebView 页面中,我们为了增强用户体验,防止用户进来之后看到白屏,可以采用服务端渲染的方式,将渲染好的页面返回给客户端,同时优化了首屏请求,也防止了客户端设备较差造成JS执行时间较长的情况。

在跨端融合方案中我们仍然有类似的解决方案,在不考虑离线包的情况下(即只考虑业务代码从远程加载的情况),我们也可以由服务端渲染好再返回,Weex 便采用了类似的方案,不过其做的更加彻底,在服务端将代码结果编译成 AST 树并转化成字节码(OPcode),在客户端解析后直接生成虚拟 DOM:

图片来自 IMWeb 2018

客户端级别的其他优化

客户端的优化有一部分是本来客户端开发就会面临的内容,也有一部分是和混合方案有关的优化,比如 Flex Render 的优化,不过这方面的内容一般而言和前端关系不是非常密切,笔者作为初级前端工程师,对这方面的内容还并不熟悉。

框架选型

本文的最后一部分,介绍框架选型。

对于各类跨端融合的方案,其相对于 WebView 都有非常大的性能提升,因此在前期,无论选择什么框架都能够看到成效,这里也并不进行特定的框架选型推荐,但是一般认为,如果是从 Vue 的项目切换,Weex 会更合适一点,而如果从 React 项目切换,在确保没有证书风险的情况下可以采用 React Native,否则可以尝试原生支持 React 的 Hippy。

以上。