阅读 2446

转转Hybrid体系建设

前言

目前转转 App 是一个典型的 Hybrid App,采用的是业内主流的做法: 客户端内有大量业务页面使用 WebView 加载 H5 页面承载。

其优点是显而易见的,即:

  1. Web 页面上线频度满足快速迭代的业务需求,不受客户端审核和发版的时间限制
  2. 可以将各个业务线的开发工作分摊到各个业务的前端团队上,使得个业务线可以并行开发。

其存在的问题也是很明显的,比如加载性能问题,白屏问题,界面展示的局限性、操作的局限性,无法使用系统功能等。

转转 Hybrid App 架构

转转 Hybrid 架构设计如下图 image image

转转整个 Hybrid 容器架构设计分为四层

  1. 原生 Web 页面 这块就是大家常见的 Web 开发环境,可以通过 Vue、React 等实现。
  2. Web 增强 包括像离线包和 Web 页面容器等。
  3. 中间件层 中间件层使 Web 页面容器和转转底层框架有机结合起来,同时还提供各种生命周期机制、事件机制、扩展插件等内容。中间件层通过 JSBridge 将客户端以及中间件层提供的各种能力和 Web 前端代码进行联通。
  4. 转转基础架构 主要包含 native 相关的能力架构

通信模块 - JSBridge

JSBridge 是 WebView 容器的基石,桥接了 JS 环境与 Native,实现了 Native 代码和浏览器环境的双向通信。

Native 代码可以通过调用浏览器提供的接口运行 JS,从而实现调用 JS 函数、传递参数到 JS 环境等。

而浏览器到 JS 环境的通信是通过 Native 拦截浏览器的请求来实现。

通过 JSBridge 我们就解决了 WebView 界面展示的局限性、操作的局限性和无法使用系统功能等问题。

JSBridge 原理如下图 image

预加载模块 - 离线包系统

为了做到良好的用户体验,我们在容器中引入了离线包机制。通过离线包机制,我们将原有从线上加载的 Web 页面,提前下发到本地,通过读取 IO 或者内存来进行页面的渲染,达到接近原生的用户体验。

通过发布平台,我们可以将不同的 Web 页面离线包,以单独应用的形式,进行不同维度的下发,使原来 Native 发布模式,改为各业务线自行定制发布计划,自行制定发布标准,自行发布的并行发布形式,来满足业务的快速迭代。

Web 应用资源离线系统方案图 image

下面我们从前端构建、测试、发布和 APP 端策略 两个方向分别来分别阐述:

前端构建、测试和发布

我们采用腾讯 Alloy 团队出品的 Webpack 离线包插件:Ak-webpack-plugin,其可以根据配置,将 Webpack 的构建出的静态资源,压缩成映射了静态资源在 CDN 路径 URL 的 ZIP 压缩包。

同时在配置的过程中,也可以选择排除掉部份文件(比如图片,并不是所有构建出的图片都是用得到的)。其生成的压缩结构如下:

image

在此过程中,我们不需要关注资源之间加载的依赖关系,更不需要关注具体的业务逻辑。只需要关注 Webpack 构建后生成的资源文件夹的结构。

把生成和上传离线资源包的过程封装成在npm run build中,通过脚手架生成项目可以让业务方无感接入。

生成的离线资源如何上传呢? 我们利用持续集成和发布工具 Beetle(转转自研的 CI 系统,可以理解为 Jenkins)来自动发布。

在本团队的发布与上线流程中,Bettle 代替 Fe 工程师构建与部署前端项目。使前端项目也像传统的 App 与后端项目一样做到了开发与构建部署分离,提高了团队的效率。

而我们生成和部署离线包的操作,也交由 Bettle 替我们完成。当部署到测试环境时,Bettle 会把离线包发布到测试版的离线包环境中,当正式线上时,还会发布到正式环境。这样整个测试流程也可以测试离线包。避免离线包造成的一些问题。

APP 端策略

在客户端每次启动或重新打开一个 Webview 的时候,都会通过接口读取一份最新的各业务线的离线包与版本号的配置表。App 根据本地的配置文件和线上的做对比,下载需要更新的离线包资源。

资源包加载的优化

增量的资源更新(bsdiff/bspatch)

客户端内置(或者在 WIFI 环境下载)了各业务全量包的基础上,为了减少每次下载更新的资源包的体积,我们采用了增量更新策略。 该策略具体为:每次发布版本的时候,如果此业务线之前已有离线包,则通过离线系统生成差分包放在 CDN

增量更新的策略使用的是基于 node 的 bsdiff/bspatch 二进制差量算法工具 npm 包 bsdp。客户端下载差分包后使用 Bspatch 合成更新包。

单独控制各个业务线 Web 应用是否使用离线机制

为了更好的监控离线包服务端和客户端的运行情况,并且降低使用离线资源带来的不可预料的风险,将隐患做到可控。 我们在每一个业务上都加入了使用离线资源的开关和灰度放量的控制。

数据一致性校验与数据安全性

为了防止客户端下载离线资源时数据在传输过程中出现窜扰,导致下载的离线包无法解压,我们在服务端通过接口中将资源包的 md5 值告知客户端,客户端下载后通过计算得到的资源包的 md5 值,与之比较,可以保证数据的一致性。

同时为了保证传输过程中,资源文件不被篡改,我们将上述的 md5 值通过 RSA 加密算法进行加密。在服务端和客户端分别使用一对非对称的密钥进行加解密。

离线包批量下载

在启动 App 时,App 会集中批量的下载各个业务线的离线包资源,我们在存放离线包资源的 CDN 中使用了 HTTP/2 协议,这样客户端与 CDN 只需要建立一次连接,就可以并行下载所有的资源。

在需要下载离线包个数较多的情况下,可以比传统的 HTTP1 有更快的传输速度。 同时,客户端只需要运行一次下载器。减少多次运行下载时对手机 CPU 的消耗。

基础包包自动更新

当增量包的体积达到基本包的 60% 以上。我们就认为需要更换基础包。

离线包回退机制

在实际情况中,为了避免用户下载离线资源或者解压资源失败等问题,导致无法加载相应的离线资源,我们设计了回退机制,在

  • 本地内置的 Base 包(ZIP 文件)解压失败的时候
  • 离线系统接口超时
  • 下载离线资源失败
  • 增量的离线资源合并失败等情况下

遇到上述情况时,我们会转而请求线上文件。

WKWebView 离线包实现

iOS 系统存在两种 Webview 类型分别是 WKWebViewUIWebViewUIWebViewiOS 2.0 就有,而 WKWebViewiOS 8.0 才有,毫无疑问 WKWebView 将逐步取代笨重的UIWebView

UIWebView 有很多问题,比如占用过多内存等。 WKWebView 提升是在多方面的, 比如网页加载速度有很大的提升,内存提升也非常明显,支持更多的 HTML5 的特性等。

转转在很早的时候就切换到 WKWebView。但是WKWebView 不能像安卓一样, 直接拦截请求。WKWebView 中实现离线包一共有三种方案,分别是

  • 私有 API 方案
  • 自定义协议方案
  • LocalWebServer 方案

转转在实践中都做了尝试,其中遇到了很多问题,这里就不单独展开了,大家可以关注我们,期待我们的下一篇文章《WKWebView 离线包实践》。

离线资源管理平台

对于接入了离线系统的各个业务线的前端工程生成的不同时间版本的离线包,我们需要有一个直观明晰的方案来对其进行管理。

于是,我们开发了离线资源管理平台,对接离线后台系统,系统界面如下图 image

其主要的功能包含有:

  • 查看与管理各个业务线信息及其离线功能的灰度放量的比例。 对于新接入离线系统的前端工程,灰度放量可以使得部分用户先使用其离线的特性,并防止不可预料的问题发生在全体用户上。

  • 通过业务线,版本号,发布时间等条件,查询各个版本的离线资源包的列表及其详细信息。 如 离线包的类型,体积,上线时间等属性。

  • 并在此基础上允许将某版本的离线包下线,以实现离线资源版本的回滚功能。

  • 针对全局的离线功能,提供了离线功能的开关。

公共资源预加载

在离线包系统中,因为我们是按业务线划分,所以并没有公共资源包。因此,我们设计了一个利用浏览器缓存策略来缓存公共静态资源的策略。

App 端为提高 WebView 打开的效率,会使用 WebView 复用池策略。其策略的第一步就是在 App 启动时会初始化一个备用的 WebView。

我们正是利用了初始化的 WebView,来加载一些公共的资源,这样可以让之后的页面再加载该资源时,可以使用 HTTP 缓存。

我们开发了一个管理平台,用来实时的管理 App 需要加载的公共资源文件。 image

页面接口预请求

我们通过离线包解决了资源加载的问题,但是首页的接口请求也是一个需要优化的地方。

现有 Web 页面加载流程可以简单概括如下:先是加载 JS,在生命周期中请求数据,等待数据返回,渲染页面。

那我们是否可以通过客户端提前帮忙请求接口,然后直接从客户端把数据取回来?答案是肯定。

PS:接口预请求的方案,适合首页请求了多个接口的项目,如果只有一两个接口,效果没有明显提升。

如何告诉客户端我们需要请求这些接口

我们通过离线包的配置文件上新建一个ajax字段:

ajax: {
  "mode": "hash",
  "split": "#",
  "routes": [
    {
      "router": "/content/index",
      "requestOffline": true,
      "route":[
        {
          url: '/zzgift/creditandexchangecount',
          key: 'creditandexchangecount',
          des: '获取星星信息',
          method: 'post',
          isNeedLogin: false,
          params: {}
        }
      ]
    }
  ]
}
复制代码

首先,用户点击打开 Webview,App 请求 Ajax 列表的接口并且把本地的上次请求的数据删除,同时加载 WebView。

JS 在请求接口时,先通过 JSBridge 取数据,Key 支持多个值一起传。

如果数据在 JSBridge 请求之前返回,把数据写入本地对应 Key 的 Value,JSBridge 读取后返回数据。

如何没有返回,读取本地 Key 为空,直接返回空。

接口返回的数据 App 端不用做任何判断和处理。如果没有取到页面在次通过 JS 发起请求,之后的超时重新请求等策略保持一致。

图片骨架屏

Hybrid 界面体验问题有一个很大的问题就是页面白屏 image

转转通过图片骨架屏来解决页面白屏的问题,在 WebView 初始化时,客户端把一张设计师出的的图片,覆盖在 WebView 上面。当页面 WebView 加载完成,或者前端通知客户端加载完成。客户端通过渐隐动画来隐藏图片,达到完美的过渡效果。

读取配置文件

客户端在启动时读取图片骨架屏的配置文件:

// 传入设备分辨率ratioWidth:400, ratioHeight:500
{
  "code":0, // code 是0 代表请求成功   -1 代表图片骨架屏功能关闭
  "data":{
    "m.zhuanzhuan.com/enjoy-given/eg/index.html": {
      "rege": '#/content/index'
      "routes":{
        "#/content/index": {
          "downloadUrl": 'https://m.zhuanzhuan.com/pic.png?400*500'
          "imgName": 'pic.png',
          "id": '10001',
        }
      }
    },
  "msg":""
  }
}
复制代码

客户端在 App 启动的时候,读取接口。

如果是首次使用,遍历配置文件,建立路径,下载图片。

如果是二次使用,则对比两次的配置文件,如果图片名称不同,删除 id 文件夹下面的图片,下载新的,如果相等,保持不变。

客户端在内存中建立文件

zzSkeleton/10001/pic.png
复制代码

解析 url 并展示

当客户端打开 webview 时,读取 url

例如:https://m.zhuanzhuan.com/enjoy-given/eg/index.html?__isonshowpro=1&metric=rhtjEzu4TBGu6npSjDyXcQ282171v&fromGiftPage=cateList&fromCateId=0#/content/index?ada=asdad

取出m.zhuanzhuan.com/enjoy-given/eg/index.html(location.host + location.pathname 在接口返回的配置文件中取出

{
  "rege": '#/content/index'
  "routes":{
    "#/content/index": {
      "downloadUrl": 'https://m.zhuanzhuan.com/pic.png?400*500'
      "imgName": 'pic.png',
      "id": '10001',
    }
  }
}
复制代码

根据 rege 解析出#/content/index路由,然后根据idimgName来从本地取出文件

然后根据 zzSkeleton + 10001 + pic.png 读取本地的图片,并展示在 WebView 上

如何解决图片拉伸的问题

后台配置图片的时候,可以根据不同的分辨率来配置不同的图片,类似 App 配置启动图的逻辑。在客户端请求配置文件时,会传入设备分辨率,接口会根据分辨率返回最接近的图片。

App 统一跳转平台

对于 App 中的每一个页面、或者是每一个行为,我们都能使用一个 URI 来定义,这个 URI 在转转内部叫做统跳。

转转的统跳全部由统跳平台来管理,通过统跳平台可以更好的测试、分享和管理这些统跳地址。

image

系统比较简单,通过动态表单来添加参数。一键生成二维码来快速的测试。

image image

Hybrid 总结与展望

目前转转 Hybrid 整体的技术方案,包括离线包、图片骨架屏等,已经无痛的接入业务中使用,很好的解决了页面加载性能问题,白屏问题,界面展示的局限性、操作的局限性,无法使用系统功能等问题。

当然技术在不断的发展。转转 Hybrid 建设在追求技术进步的道路上。还有很多的地方需要提升。

  • 提供完整的 SPA 体验 目前页面跳转基本是重新打开一个 WebView,这样就损失了一些项目的性能。我们计划通过前端接管原生的路由的方式,让端内做到真正的 SPA 体验。

  • 接口请求和埋点通过客户端发送 转转现在的请求和埋点都是通过前端自己发送的。但是存在埋点丢失等问题。我们计划打通客户端和前端的请求发送。同时前端也可以利用客户端的安全鉴权等能力。

福利部分

预告下,接下来我们会陆续发布转转在微前端、Umi、组件库等基础架构和中台技术相关的实践与思考,欢迎大家关注,期望与大家多多交流。文章在 “大转转FE” 公众号也会发送,并且公众号有抽奖活动,本文奖品是转转纪念T恤一件,欢迎大家关注 ✿✿ヽ(°▽°)ノ✿