「福利」 ✿✿ ヽ(°▽°)ノ ✿:文章最后有抽奖,转转纪念T恤,走过路过不要错过哦
目前转转 App
是一个典型的 Hybrid App
,采用的是业内主流的做法:客户端内有大量业务页面使用 WebView
加载 H5 页面承载。
其优点是显而易见的,即:
其存在的问题也是很明显的,比如加载性能问题,白屏问题,界面展示的局限性、操作的局限性,无法使用系统功能等。
转转 Hybrid
架构设计如下图
转转整个 Hybrid
容器架构设计分为四层
JSBridge
将客户端以及中间件层提供的各种能力和 Web 前端代码进行联通。native
相关的能力架构JSBridge 是 WebView 容器的基石,桥接了 JS
环境与 Native
,实现了 Native
代码和浏览器环境的双向通信。
Native
代码可以通过调用浏览器提供的接口运行 JS
,从而实现调用 JS
函数、传递参数到 JS
环境等。
而浏览器到 JS
环境的通信是通过 Native
拦截浏览器的请求来实现。
通过 JSBridge 我们就解决了 WebView 界面展示的局限性、操作的局限性和无法使用系统功能等问题。
JSBridge 原理如下图
为了做到良好的用户体验,我们在容器中引入了离线包机制。通过离线包机制,我们将原有从线上加载的 Web 页面,提前下发到本地,通过读取 IO 或者内存来进行页面的渲染,达到接近原生的用户体验。
通过发布平台,我们可以将不同的 Web 页面离线包,以单独应用的形式,进行不同维度的下发,使原来 Native 发布模式,改为各业务线自行定制发布计划,自行制定发布标准,自行发布的并行发布形式,来满足业务的快速迭代。
Web 应用资源离线系统方案图
下面我们从前端构建、测试、发布和 APP 端策略 两个方向分别来分别阐述:
我们采用腾讯 Alloy 团队出品的 Webpack 离线包插件:Ak-webpack-plugin
,其可以根据配置,将 Webpack 的构建出的静态资源,压缩成映射了静态资源在 CDN 路径 URL 的 ZIP 压缩包。
同时在配置的过程中,也可以选择排除掉部份文件(比如图片,并不是所有构建出的图片都是用得到的)。其生成的压缩结构如下:
在此过程中,我们不需要关注资源之间加载的依赖关系,更不需要关注具体的业务逻辑。只需要关注 Webpack 构建后生成的资源文件夹的结构。
把生成和上传离线资源包的过程封装成在npm run build
中,通过脚手架生成项目可以让业务方无感接入。
生成的离线资源如何上传呢?我们利用持续集成和发布工具 Beetle(转转自研的 CI 系统,可以理解为 Jenkins)来自动发布。
在本团队的发布与上线流程中,Bettle 代替 Fe 工程师构建与部署前端项目。使前端项目也像传统的 App 与后端项目一样做到了开发与构建部署分离,提高了团队的效率。
而我们生成和部署离线包的操作,也交由 Bettle 替我们完成。当部署到测试环境时,Bettle 会把离线包发布到测试版的离线包环境中,当正式线上时,还会发布到正式环境。这样整个测试流程也可以测试离线包。避免离线包造成的一些问题。
在客户端每次启动或重新打开一个 Webview 的时候,都会通过接口读取一份最新的各业务线的离线包与版本号的配置表。App 根据本地的配置文件和线上的做对比,下载需要更新的离线包资源。
客户端内置(或者在 WIFI 环境下载)了各业务全量包的基础上,为了减少每次下载更新的资源包的体积,我们采用了增量更新策略。该策略具体为:每次发布版本的时候,如果此业务线之前已有离线包,则通过离线系统生成差分包放在 CDN。
增量更新的策略使用的是基于 node 的 bsdiff/bspatch
二进制差量算法工具 npm 包 bsdp
。客户端下载差分包后使用 Bspatch
合成更新包。
为了更好的监控离线包服务端和客户端的运行情况,并且降低使用离线资源带来的不可预料的风险,将隐患做到可控。我们在每一个业务上都加入了使用离线资源的开关和灰度放量的控制。
为了防止客户端下载离线资源时数据在传输过程中出现窜扰,导致下载的离线包无法解压,我们在服务端通过接口中将资源包的 md5 值告知客户端,客户端下载后通过计算得到的资源包的 md5 值,与之比较,可以保证数据的一致性。
同时为了保证传输过程中,资源文件不被篡改,我们将上述的 md5 值通过 RSA 加密算法进行加密。在服务端和客户端分别使用一对非对称的密钥进行加解密。
在启动 App 时,App 会集中批量的下载各个业务线的离线包资源,我们在存放离线包资源的 CDN 中使用了 HTTP/2
协议,这样客户端与 CDN 只需要建立一次连接,就可以并行下载所有的资源。
在需要下载离线包个数较多的情况下,可以比传统的 HTTP1 有更快的传输速度。同时,客户端只需要运行一次下载器。减少多次运行下载时对手机 CPU 的消耗。
当增量包的体积达到基本包的 60% 以上。我们就认为需要更换基础包。
在实际情况中,为了避免用户下载离线资源或者解压资源失败等问题,导致无法加载相应的离线资源,我们设计了回退机制,在
遇到上述情况时,我们会转而请求线上文件。
iOS 系统存在两种 Webview
类型分别是 WKWebView
和 UIWebView
,UIWebView
自iOS 2.0
就有,而 WKWebView
从 iOS 8.0
才有,毫无疑问 WKWebView
将逐步取代笨重的UIWebView
。
UIWebView
有很多问题,比如占用过多内存等。WKWebView
提升是在多方面的, 比如网页加载速度有很大的提升,内存提升也非常明显,支持更多的 HTML5 的特性等。
转转在很早的时候就切换到 WKWebView
。但是WKWebView
不能像安卓一样, 直接拦截请求。WKWebView
中实现离线包一共有三种方案,分别是
转转在实践中都做了尝试,其中遇到了很多问题,这里就不单独展开了,大家可以关注我们,期待我们的下一篇文章《WKWebView 离线包实践》。
对于接入了离线系统的各个业务线的前端工程生成的不同时间版本的离线包,我们需要有一个直观明晰的方案来对其进行管理。
于是,我们开发了离线资源管理平台,对接离线后台系统,系统界面如下图
其主要的功能包含有:
查看与管理各个业务线信息及其离线功能的灰度放量的比例。对于新接入离线系统的前端工程,灰度放量可以使得部分用户先使用其离线的特性,并防止不可预料的问题发生在全体用户上。
通过业务线,版本号,发布时间等条件,查询各个版本的离线资源包的列表及其详细信息。如 离线包的类型,体积,上线时间等属性。
并在此基础上允许将某版本的离线包下线,以实现离线资源版本的回滚功能。
针对全局的离线功能,提供了离线功能的开关。
在离线包系统中,因为我们是按业务线划分,所以并没有公共资源包。因此,我们设计了一个利用浏览器缓存策略来缓存公共静态资源的策略。
App 端为提高 WebView 打开的效率,会使用 WebView 复用池策略。其策略的第一步就是在 App 启动时会初始化一个备用的 WebView。
我们正是利用了初始化的 WebView,来加载一些公共的资源,这样可以让之后的页面再加载该资源时,可以使用 HTTP 缓存。
我们开发了一个管理平台,用来实时的管理 App 需要加载的公共资源文件。
我们通过离线包解决了资源加载的问题,但是首页的接口请求也是一个需要优化的地方。
现有 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 界面体验问题有一个很大的问题就是页面白屏
转转通过图片骨架屏来解决页面白屏的问题,在 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
当客户端打开 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
路由,然后根据id
和imgName
来从本地取出文件
然后根据 zzSkeleton + 10001 + pic.png
读取本地的图片,并展示在 WebView 上
后台配置图片的时候,可以根据不同的分辨率来配置不同的图片,类似 App 配置启动图的逻辑。在客户端请求配置文件时,会传入设备分辨率,接口会根据分辨率返回最接近的图片。
对于 App 中的每一个页面、或者是每一个行为,我们都能使用一个 URI
来定义,这个 URI
在转转内部叫做统跳。
转转的统跳全部由统跳平台来管理,通过统跳平台可以更好的测试、分享和管理这些统跳地址。
系统比较简单,通过动态表单来添加参数。一键生成二维码来快速的测试。
目前转转 Hybrid 整体的技术方案,包括离线包、图片骨架屏等,已经无痛的接入业务中使用,很好的解决了页面加载性能问题,白屏问题,界面展示的局限性、操作的局限性,无法使用系统功能等问题。
当然技术在不断的发展。转转 Hybrid 建设在追求技术进步的道路上。还有很多的地方需要提升。
提供完整的 SPA 体验
目前页面跳转基本是重新打开一个 WebView,这样就损失了一些项目的性能。我们计划通过前端接管原生的路由的方式,让端内做到真正的 SPA 体验。
接口请求和埋点通过客户端发送
转转现在的请求和埋点都是通过前端自己发送的。但是存在埋点丢失等问题。我们计划打通客户端和前端的请求发送。同时前端也可以利用客户端的安全鉴权等能力。
预告下,接下来我们会陆续发布转转在 微前端、Umi、组件库 等基础架构和中台技术相关的实践与思考,欢迎大家关注,期望与大家多多交流
转发本文并留下评论,我们将抽取第 10 名留言者(依据公众号后台排序),送出转转纪念 T 恤一件,大家转发起来吧~