H5 因其“天生”的跨平台、实时更新、便于传播等特性,始终是各家 APP 承载内容的重要手段之一。但因为 web 技术本身的限制,在功能、性能以及体验方面与 native 仍有必定的差距,例如受限的硬件拜访能力、差强人意的离线功能等。而基于 WebView 的混合模式,借助 native 的加强是比较通用的处理方法。本文将围绕这些痛点,分享下网易资讯客户端在资源离线、JsBridge 通信、接口预请求三个方面的优化实践。
1、资源离线
1、介绍
Web 页面性能优化的重点之一便是静态资源的加载耗时,而传统的离线方法都难以处理首次加载的问题。在 APP 内,咱们能够经过 native 将资源离线到本地,能够很好的弥补这个缺陷。实现的基本思路是把 web 页面的静态资源生成 Zip 包,客户端在合适的机会拉取 Zip 包到本地解压并持久化存储。当用户拜访时,经过拦截 WebView 发出去的页面请求,直接返回对应的本地文件,这般就能够实现本地加载,缩短页面资源加载时间。
大体流程如图:
2、离线实现
当咱们确定了采用页面 Zip 离线加载方法,咱们需要制定一套完整流程来完成这个方法。实现全部离线方法分三部分内容: Web 页面 Zip 包生成工具离线管理系统客户端离线实现
2.1、Web 页面 zip 包生成工具
2.1.1、工具介绍
Web 页面 Zip 包生成工具重点是将页面生成 Zip 包,同步配置离线页面信息。
思虑到 web 页面多、依赖资源多,手动打包涵易出错且效率低下。因此呢咱们研发了一款脱离业务、方便灵活、多项目可用的打包工具,方便咱们将本地项目页面或线上页面生成 Zip 包的工具。
亦便是说配置一个页面的入口文件或线上页面位置: [{
name: index, // 页面名叫作 url: [https://example.com/index],
src: ./dist/index.html // 本地页面入口文件
}]
// 或
[{
name: index, // 页面名叫作
url: [https://example.com/index] //页面线上位置}]
最后输出客户端可用的离线页面关联信息:
[{
name:index,
url: [//example.com/index],
zipUrl: https://assets.example.com/static/example.20190525_1020.zip,
md5: md5md5md5md5md5md5md5md5md5md5md5md5
}]2.1.2、工具实现
Web 页面生成 Zip 包工具所有环节均为自动化,每一个环节经过中间件方式实现,满足区别场景,方便定制化需要。整体分为通用和定制两部分。
通用部包含: 拷贝页面依赖生成Zip包判断包的完整性获取 Zip 包的 MD5 值生成 Zip 包版本号
定制部分为: 确定待更新 Zip 包上传 Zip 包到 CDN更新离线数据、Zip 包版本数据
功能流程如下图:
实现(build.js): const { del, zip, diff } = require(./index);
const{ upload, version, extend, equal, Compose, copy, check } =require(../utils)
const config = require(../tool.config);
const common = () => {
const compose = new Compose();
compose.use(async(context, next) => {// 通用规律(拷贝打包,包校验,生成版本,获取md5)
next();
});
return compose;
};
const build = list => new Promise((resolve, reject) => {
if (!Array.isArray(list)) {return reject(The "list" argument must be of type array, list);
}
common().use(async (context, next) => {
// 定制规律(确定需要上传的zip包,上传zip包,更新数据库)
next();
}).exec({
list
}).then(res => resolve(res)).catch(err => reject(err));
});
constcompose = common();
build.use = compose.use.bind(compose);
build.exec = compose.exec.bind(compose);module.exports = build;2.1.3、生成的 Zip 包
Zip 包里有和客户端朋友约定好了 zip 包的详细内容和目录结构,对应页面的页面入口文件(index.html)和其他包括了页面依赖资源,页面资源目录结构和线上保持一致,这般能够方便客户端匹配查询,简化客户端处理规律。
Zip 包结构如下:
hot-content_20190808150211.zip
│ ── 163
│ └── frontend
│ └── hot-content
│ └── js
│ └── app.10882524.js
│ └── css
│ └── app.412b0635.css
│ ── index.html
2.2、离线管理系统
离线系统重点功能职责: 为离线工具供给打包信息及离线包信息存储为 APP 供给离线数据页面离线数据在线管理
为扩展其他制品运用,离线管理系统完成为了多制品、多用户的设计。除工具自动更新数据外,还能够在系统里添加数据,对数据进行改删操作。离线数据保存近期5个版本,倘若发掘线上 Zip 包有问题,能够快速回滚到上个版本。
核心功能如下图:
2.3 、客户端离线实现
客户端离线实现是全部离线方法最重要的一环,重点分为两大内容: 离线资源更新拦截资源返回
咱们设计一个离线资源管理器做总调度来处理离线资源的更新和拦截返回规律。离线资源管理器按照配置的离线信息创建一个动态管理器,会安排每一个 URL 对应的页面入口文件,静态资源(css,js,image)目录,要拦截的静态资源域名。
2.3.1、更新实现细节
APP 在获取离线配置分主动和被动,主动更新是在每次 APP 起步后经过接口获取离线配置信息,被动经过 push 更新。
离线配置包括了所有已配置的页面离线信息,在咱们获取到这些离线信息后,读取本地配置缓存进行比对,按照页面名叫作来确定离线文件的更新策略是什么,远端配置无本地配置有则认为当前页面离线包是被删除的,直接删除本地对应的离线页面入口文件;倘若发掘两个配置中同名页面 Zip 包的 MD5 值不一致则认为此页面离线包是更新了的;仅有远端有配置则认为是新增;而后交给下载管理器下载并解压 Zip 包,下载解压完成通告离线资源管理器更新本地离线缓存配置。
更新重点流程:
2.3.2、拦截资源返回细节
咱们会统一拦截所有网络请求,经过离线资源管理器来处理拜访规律。需要处理的拦截返回分: 页面文件(html)依赖的静态资源(js、css、image)
当 APP 在 Webview 发起页面请求时,咱们会先拦截当前页面请求,获取到页面的 URL 位置,按照离线管理器中配置,进行查询有没有匹配的本地页面入口文件,有则直接返回入口文件,否则放行请求线上资源。
页面的加载会伴同着依赖资源的加载,获取请求 URL,倘若在静态资源拦截域名内,则替换域名的 origin 为本地的静态资源目录进行查询。倘若找到,获取文件扩展名,设置返回的文件类型直接返回。
拦截返回的主流程如下图:
2.3.3、其它
为了保证全部 Zip 离线的高可用性,APP 端会对每一个环节显现的错误进行上报,以便快速定位并修复问题。
和离线关联的错误类型有: 获取离线配置接口网络错误获取离线配置接口数据解析失败Zip 包请求网络错误Zip 包解压错误Zip 包 MD5 值 APP 端和前端不一致Zip 包解压手机空间不足
如显现上面任何一种错误都不会更新本地离线资源和离线配置。
3、小结
为了加快页面展示速度,咱们做了服务器渲染,合并减少 Request 请求,做 gzip 压缩,安排 CDN,做缓存,引入 Service Worker 等优化办法,但还是不可很好的处理首次请求白屏过长的问题。而经过 Zip 离线方法,在用户第1次拜访时本地已然有对应的离线资源了,这般大大的缩短了资源加载时间,减少白屏时间。
这儿经过一个测试页面来看下资源离线的前后数据对比,为了模拟真实的项目状态,咱们在页面中挂载了 jQeury、Bootstrap.js/css、 以及 js-bridge 等静态资源。运用 iPhone 6s 机型,分别在区别网络环境下对测试页面进行拜访,记录多组首次拜访总耗时,最后取平均值。
从图中数据能够看出,通过运用离线方法,在各样网络环境下加载页面静态资源均不受网络状况影响,相比从远端加载整体有 75% 的提高。需要说明的是,测试页面中无掺杂业务规律,仅纯粹的资源加载,因此效果会比较显著。
2、JsBridge
虽然随着 WebView 的逐步更新,赋予了 web 丰富的功能,但思虑到兼容性以及全部 APP 交互体验的统一,大部分的业务场景下咱们仍需要借助 native 的功能。例如: 视图层面 - 注册、登录、认证、注销组件、视图路由...存储层面 - 用户信息、设备信息、业务状态、缓存...网络层面 - 请求 header、代理转发、预请求...APP 层面 - 唤起、设置、push、跨 APP 操作...系统层面 - 底层 API 的调用其它辅助功能
良好的混合架构能降低设计成本,减少前后端工作量,快速发布迭代,提高稳定性和用户体验。而 JsBridge 正是负责 web 和 native 通信的核心。
1、介绍
JsBridge 的设计和实现有各样各样的版本,这儿简单梳理几个要点:
1.1、Web To Native
重点采用注入 web 可调用的办法,或进行拦截: iOS UIWebView - JavaScriptCoreiOS WKWebView - WKScriptMessageHandlerAndroid - addJavascriptInterface(4.2以下有安全漏洞)URL 拦截(URLScheme)JS 办法拦截(alert、prompt、confirm、console.log)
1.2、Native To Web
直接执行 web 暴露的全局办法就可: iOS - stringByEvaluatingJavaScriptFromString (兼容但没法捕捉错误)iOS UIWebVIew - JSContext evaluateScriptiOS WKWebView - evaluateJavaScriptAndroid - loadUrl(没法获取返回结果)Android 4.4+ - evaluateJavascript(能够获取返回结果)
2、重构前状态
重构前,端内 web 调用 native 的方式较为混乱: foo:// - 无参数调用bar://encodedParams - 所有参数转 JSONString 并 encodebaz://param1/param2 - 分割单个参数window.xxx(params) - 注入办法直接调用
对应的回调亦是多种多样,web 暴露全局办法由 native 调用,亦有直接经过注入办法返回: window.foo_success(result) / .foo_fail(error) - 区别函数表率区别状态window.bar_done(result) - 从结果集中区分区别状态result = window.xxx(params) - 直接返回
混乱的实现方式引起运用了海量的协议头,同期注入了海量的全局办法,还存在有些运用的时序问题。况且运用一个功能需要晓得协议头、参数格式、传参方式、回调方式、怎样区分成功/失败,显然过于繁杂了。
3、重构思路
借这次重构,最终有机会对两端通信的 API 做一次全盘整理,而怎样设计统一规范的 JsBridge 是这次重构的重点目的。
从单一职责的方向思虑,JsBridge 只应处理一件事:把信息在正确的通道传递给对方,不关心详细业务;而业务研发只聚焦功能自己,无需思虑怎样传递。
3.1、对叫作设计
首要需要知道的是,完整的 JsBridge 功能应该是双向互通的: web 调用 -> native 接收调用并返回结果 -> web 接收结果native 调用 -> web 接收调用并返回结果 -> native 接收结果
抽象之后应该具备两个通道,每一个通道发送的信息都包括“调用”和“结果”两种内容。而每一端需要具备四项能力:
调用(invoke)- 调用另一端的办法接收(receive)- 接收执行结果注册(register)- 注册办法等待另一端的调用回调(callback)- 回传执行结果
信息都是经过异步回调的方式进行传输,这般在满足更加多的业务场景时,功能实现方亦不消关心功能自己是同步还是异步,只需在得到结果的时候丢给 JsBridge 就可。
3.2、信息通道
基于信息传递需具备的能力,咱们来思虑采用哪种方式实现。日前端内的状况,iOS 采用 WKWebview,Android 仍需兼容系统 4.0+,因此最后实现:
3.2.1、Web To Native iOS - WKScriptMessageHandlerAndroid - 4.2及以上 采用 addJavascriptInterface,其它运用 URLScheme
简单来讲便是优先运用 native 注入的方式,更通用的 URLScheme 用来兜底。之因此无统一运用 URLScheme,重点是思虑性能方面,native 注入的办法调用速度更快;况且在并行调用时 URLScheme 方式需要做有些 hack 处理,对效率亦有必定影响。
3.2.2、Native To Web
统一由 web 暴露一个全局的接收办法供 native 调用。
综上,只需要在全局环境注入三个办法(iOS 和 Android 注入的办法限于实现方式区别未强制统一)和一个协议。
3.3、数据结构
详细到信息本身,咱们需知道应该包括那些内容。每一个业务或功能的 API 实现基本都能够简化成:办法名、所需参数、回调和错误信息。而因为信息通道的合并,为了区分信息是“调用”还是“结果”,需增多一个 ID 标示,同期利用该 ID 还能够确定是哪一次“调用”,以处理异步的对齐问题。最后的数据结构如下: // Invocation
{
"name": "foo",
"params": {...},
"callbackId": "cb_1"
}
// Result
{
"responseId": "cb_1",
"result": {
"data": {...},
"errorMsg": "",
"errorData": ...,
"errorDesc": ...
}
}其中: callbackId - 显示该信息是一次“调用”,而 responseId 显示是“结果”,况且两者是对应的name - 调用的办法名params - 调用的办法所需的参数,统一运用对象格式,利于扩展errorMsg 用于区分此次处理是不是成功,其它附庸 error* 是有些可选的扩展data是处理成功后的返回数据
思虑到兼容性,信息体采用 JSONString 类型,倘若是经过 URLScheme 发送还需要对信息体做一次 encode。
在确定了信息通道(怎么发)和数据结构(发什么)之后,JsBridge 其实已然能够工作了。但在实质运用场景下,咱们仍需要思虑以下几个方面。
3.4、可用性
3.4.1、JsBridge 是不是可用
Native 办法注入是由于系统底层实现的,当 WebView 构建 Window 环境时会相应的挂载要注入的办法,当 web 页面运行时,注入办法已然可用;而对 URLScheme 的监听机会更早,且跟 Window 无直接相关。因此咱们能够在页面的任何地区直接发起调用,而无需进行双方“握手”,perfect!
3.4.2、API 是不是可用
随着业务迭代,native 支持的办法在区别的版本下会产生差异,比起运用版本号来判断,统一暴露出一个检测 API 是不是可用的办法则更为方便。但 JsBridge 的调用均为异步,两个异步嵌套会让运用变的繁琐。咱们期望能像浏览器自带 API 同样能够直接运用,因此最后采用由 native 向 Window 环境中注入当前所支持的 APIList 方法。但两端的实现有些差异: iOS 经过 WKScriptMessageHandler 注入的办法没法直接拿到返回值,改由 WKUserScript 注入 jsAndroid 4.2 及以上仍利用 addJavascriptInterface 注入办法,能够直接拿到返回值Andoird 4.2 以下运用 loadUrl 在页面加载的多个周期尝试注入 js
3.5、命名空间
一般 native 供给的功能大都是偏向通用的,但某些业务场景会需要比较定制的混合功能(例如业务数据同步、UI 交互联动),显然与通用功能放在一块不太合适,况且会增多命名包袱。咱们能够把通用功能放在一个池子里,任何业务都能够继承自己所需的,而后再实现自己定制的部分,组合成一套新的 APIList,并利用命名空间加以区分,这般定制业务能够更加灵活而不消担心对其它业务做成影响。Web 端直接调用指定命名空间下注入的办法,或在 URLScheme 中增多一级 namespace 便能够拜访到指定业务下的对应办法。
3.6、详细实现
在 web 端的详细实现中,对信息传递部分进行了封装,抹平了 iOS、Android 中的差异,并运用 Promise 来承接设计中的双向异步、状态区分(成功、失败)等特性。
Invoke
jsBridge.config({
namespace: common, // 设置命名空间
...
})
if (jsBridge.isAvailable(foo)) { // 判断是不是可用
jsBridge.invoke(foo, params) // 只需关心办法名和参数就可
.then(data => {...})// 成功数据
.catch(error => {...}) // 失败处理
}Register
jsBridge.register(bar, params => {
...
// 直接返回结果
return result
// Or 返回一个异步操作
return new Promise(...)
})4、小结
总体来讲,JsBridge 的设计并不繁杂,只需要确定职责范围,针对性技术选型,再处理一部分兼容问题就可,剩下的便是详细业务功能的实现和联调了。
另一针对 URLScheme 有两个问题需要弥补下:
4.1、URLScheme 特殊处理
在运用 URLScheme 时,会遇到“并发”问题,即同期发送多次调用,仅最后一个能正确传递到 native 端,前边的都被忽略了。针对这个问题咱们能够运用纯 web 的处理方法,每次调用都创建一个新的 iframe,但思虑到频繁地创建销毁 DOM 元素,对全部页面会导致必定的影响,最后采用队列 + 确认的方法。
Native 每收到一次协议信息(不管里边包括多少调用),立即发送接收成功的 confirm 到 web 端,而后 web 端检测等待 confirm 这时期是不是有累积新的调用,倘若有就一次性发走。
另一,URL 是有长度限制的,因此针对尤其繁杂的调用是有可能丢失数据的,这亦是非常多 JsBridge 实现采用 web 发送通告(有新信息),native 自己来取的策略。但这就增多了一个取数据的环节,思虑到这种状况极少,况且 URLScheme 只是咱们的 fallback 方法,咱们最后选取忽略这个问题。
3、实质应用
基于资源离线和 JsBridge 的设计,WebView 容器的基本功能已然实现,之后按照规范对 native API 进行完善,全部重构就算完成为了。除了常规的 API 外,咱们还实现了 web 端请求代理功能,以处理在实质的业务场景中数据请求存在的种种问题。
1、请求代理
利用 request 协议办法,将请求参数传递给 native 端,native 端封装好请求并发送,最后将返回数据挂载到 request 的回调中返回给 web 端。利用该办法能带来以下几个方面的提高:
1.1、预请求
在加载页面的同期,native 端按照配置参数(合并在离线配置中)提前发送请求并暂存,web 端运行时发送一样请求时,native 端经过比对将已然暂存的请求数据返回(倘若请求还未完成则等待完成后返回)。经过简单的并行,缩短了用户的感知时间,尤其针对首屏有数据依赖的页面,性能提高较为显著。
1.2、统一业务 header
为了方便和统一,在 native 端封装了一套通用的请求 header,包括有些常用的设备信息、用户登录信息等,用于后端接口的数据查找和校验。而 web 端想保持一致的话,有些 header 信息需要先从 native 端取,而后自动封装发送,存在必定耗时且较为繁琐。利用 request 协议办法,web 端能够省略通用 header,统一由 native 端注入,既方便又能保持数据的一致性。
1.3、统一日志管理
Native 端在处理 request 协议办法时,将请求和返回数据一并插进日志队列,集中存储,能更清晰的记录用户的行径和请求状态,改善了 web 端难以落日志的痛点,针对反馈问题的排查有很大帮忙。
1.4、跨域
省去了跨域带来的种种麻烦。
2、WebView 预创建
除了资源加载,容器(WebView)的创建和初始化亦存在必定的耗时,对此,咱们创建了一个 WebView 的“池子”,当 APP 起步时,在主序列任务完成后,会主动创建一个 WebView 放进“池子”中,以供后续 web 页面的运用。同期,每当“池子”中的一个 WebView 被运用了便会再创建一个留作备用,依次循环。这就保准了,每次 web 页面加载时,总有一个 ready 状态的 WebView 能够立即运用。
3、性能对比
协同以上种种特性,咱们最后来看下,经过安排离线和预请求来提高性能的前后对比数据。基于章节二中的测试页面,咱们在静态资源加载完毕后追加了一次数据请求,这亦更接近真实的业务场景,一样在多种网络环境拜访多次取均值:
从图中能够看出,受到业务接口的影响,平均整体耗时由 942.9ms 优化到了 391.1ms,优化幅度在 50% 上下,接口的平均耗时要高于静态资源的记载耗时。但随着业务规律繁杂度的提高,资源加载和接口预请求的并行时间会进一步增多,收益亦会相应的提高更加多。
4、线上实例
经过线上实例来对比,能够更直观的感受到用户体验的变化。视频中在一样环境下拜访同一个 H5 页面(首次打开和二次打开),其中左侧为普通加载,右侧开启离线和预请求,以 WebView 所在原生视图推进的瞬间对齐时间线。
能够看出,经过离线和预请求的优化,显著的缩短了首次打开的页面白屏时间。
4、结语
在如今各样混合框架/语言激烈竞争的大前端环境下,基于 WebView 的 H5 Hybrid 凭着简单、通用、轻量、稳定等特性,仍占据着重要的地位。而借助资源离线、数据预请求、native 办法注入等优化方法,对其在性能和硬件能力上的短板进行加强,使其能够承接更加多的业务场景。
作者:毛仪桓 李贺柯
源自-微X公众号:网易传媒技术团队
出处 :https://mp.weixin.qq.com/s/AV2SwFfwwJH7xyrIBJemgw
|