汽车之家主机厂离线化 H5 Hybrid 实践
1.背景
H5 页面做秒开优化是业务的常规操作,一般正常通过网络请求的 H5 页面,我们都是围绕资源加载速度优化展开。优化手段主要分两个方向,一个是提升网络速度,一个是减少资源大小。
提升网络速度,一般的手段有 DNS 预解析、多域名、升级 HTTP2、使用 CDN、SSR。而即使有静态资源的网络缓存,HTML 也只能用协商缓存,需要消耗一次网络请求。这也注定了无法避免因网络问题导致的页面白屏时间较长的问题,在我们真实的数据中也能得到印证,无论怎么优化,页面的 1.5 秒开稳定在 90% 以上非常困难。
因此,如果想实现 95% 以上甚至 99% 以上秒开,离线化 H5 是必然的选择。同时根据历史经验,随着 iOS 和 Android 手机的性能不断提升,Webview 的渲染性能也不断提升,目前大部分手机的 H5 离线化渲染都可以实现无白屏体验,无限接近原生的交互体验。
2.收益
在实践过程中,我们分两个场景构建离线化 H5 基座,一个是由 H5 开发的新 APP,一个是汽车之家 APP。
主机厂内部有一个应用,第一版是 Native 和 H5 的混合开发,会通过网络请求资源,虽然有网络缓存,但是第一次打开也很慢,非常影响用户体验,内部性能监控平台显示首屏平均耗时 1s。后面全面改造成内置离线 H5 应用,Native 只提供桥功能,首屏平均耗时减少到 237ms,加载速度提升 4 倍,大部分情况下实现无白屏体验。业内曾经做过人眼识别白屏的最小时间测试,当降到 200ms 左右时,人眼几乎无法识别白屏。
新 APP 我们主要使用 H5 开发,而不是用 Flutter 或者 RN 等技术,最主要的原因是人才储备不足,在业务场景并没有特别复杂的原生体验情况下,我们发现业务迭代,对客户端的依赖大大减少,沟通成本降低,迭代效率有明显提高,团队不需要面对复杂的 Flutter 和 RN 引擎,也不需要熟悉客户端的开发模式。
汽车之家 APP 中,由于历史原因,不能预置离线包 H5,所以只能有选择性地进行动态预加载,而这会导致在开屏等特殊场景下,访问速度仍然不高。整体上首屏时间从 1240.4ms 缩短为 505.4ms,加载速度提升一倍。
3.技术架构
离线化 H5 Hybrid 架构上,主要分成四大模块:H5 离线包管理工具、APP 开发工具、Native 运行时和 Webview 运行时。
4.离线包管理工具
H5 离线包管理工具包含:离线包管理平台、之家云打包脚本。离线包管理平台包含:APP 应用管理、H5 应用管理、发布回滚、开关控制四大功能。通过和之家云发布流水线联动,可以实现网络版和离线版 H5 的版本同步,发布操作也实现同步,由于之家云功能限制,回滚目前还不支持联动。
管理的物料包括:H5 离线包(H5 代码、离线包配置文件)、APP 离线包配置文件。APP 配置文件,在没有预置离线包的情况下,下载资源是阻塞性的。为了提升下载速度和可用性,APP 配置文件也被维护成了一个 CDN 上的 JSON 文件。
4.1
离线包发布流程
在之家云发布平台,使用离线包管理 CLI 工具,执行上传离线包命令,并配置一个离线包发布的 Webhook,即可实现自动化离线包发布。离线管理平台会同步之家云平台的离线包版本号。
4.2
离线包设计
和普通的 H5 打包文件相比,离线包新增了一个专属的离线包配置文件 config.json,同时会把资源打包成 gzip 压缩包,从而提升整体资源的下载速度。
资源目录
h5id
├── js/
├── css/
├── img/
├── pages
│ ├── index.html
│ └── list.html
└── config.json(配置文件)
配置文件
interface HybridConfig {
// 离线 H5 APP ID
h5id: string;
// 匹配页面和静态资源的规则
mapping: {
[env: string]: {
pages: Resource[];
resources: Resource[];
}
};
package: {
// 离线包资源目录路径,根目录相对路径
file: string;
// 包含的文件,和 excludes 互斥,只能同时有一个
includes: {
ext: string[];
file: string[];
};
// 不包含的文件
excludes: {
ext: string[];
file: string[];
};
// Native APP 版本适配
appRules: {
// 离线包管理平台的应用 ID
[appid: string]: {
// [最小版本,最大版本]
ios: [string, string];
android: [string, string];
}
};
}
}
interface Resource {
// 拦截到的请求 url 的规则,不提供 http 或者 https,支持单个文件和文件目录。
// 例如:example.com/page,example.com/static/img.png
remoteUrl: string;
// 和 downloadUrl 必须有一个存在,相对离线包所在目录的文件路径。
// 「path」值会替换掉请求 url 中的「remoteUrl」字符串。
path?: string;
// 和 path 必须有一个存在,指定下载资源的 url,
// 下载后存放在离线包的 vendor 目录下。
// 并把存储 path 同步到配置文件的 path 字段。
downloadUrl?: string;
// 可选的 mime type,如果不提供,通过文件名后缀自动补偿
contentType?: string;
}
4.3
打包命令行工具
打包发布脚本发布到之家私有源,以脚手架命令形式调用,提供打包、上传命令;
业务方结合自身编译上线流程进行调用,上传完成则自动进行发布;
前端静态资源按照页面/工程纬度打包成zip;
zip包含js/css/img/pages/config.json配置文件;
// 脚本安装
npm i @auto/dt-fe-cli
// 编译上传
// 指定脚本的配置文件,打包并上传至服务器,默认配置文件为 config.json,可以使用 --config 指定配置文件
dt-fe-cli offline --config hybrid-config.json
4.4
管理平台
为了更好管理离线包,我们提供了一个简洁的管理后台,用来管理 H5 应用和 APP 应用的关系,记录之家云编译好的离线包,同时提供 APP 配置给客户端查询。为了提高 APP 配置下载速度和可靠性,我们用 CDN 上的 JSON 文件来存储 APP 配置。
►4.4.1 APP 配置
interface APPConfig {
appid: string;
version: string;
updateTime: string;
isIosEnable: boolean;
isAndroidEnable: boolean;
H5Apps: H5App[];
}
interface H5App {
// H5 应用的 APP ID
h5Id: string;
versions: H5Config[];
lastVerison: {};
latestUrl: string;
}
interface H5Config {
version: string;
// true,开启离线化
isEnable: boolean;
// true,开启 iOS APP 离线化
isIosEnable: boolean;
// true,开启 Android APP 离线化
isAndroidEnable: boolean;
// true,需要预先加载
isPreLoad: boolean;
pages: Resource[];
appRules: AppRules;
downloadUrl: string;
}
interface Resource {
// 拦截到的请求 url 的规则,支持单个文件和文件目录。例如:/page,/static/img.png
remoteUrl: string;
// 和 downloadUrl 必须有一个存在,相对离线包所在目录的文件路径。
// path 会替换掉请求 URL 中的「remoteUrl」字符串
path?: string;
// 和 path 必须有一个存在,指定下载下载资源的 url,下载后存放在离线包的 vendor 目录下。
// 并把存储 path 同步到配置文件的 path 字段。
// 下载失败,则该匹配规则失效自动访问网络资源
downloadUrl?: string;
// 可选的 mime type,如果不提供,通过文件名后缀自动补偿
contentType?: string;
}
interface AppRules {
// ios APP 的开始和结束版本,最大版本可设置 infinite
ios: [string,string];
android: [string,string];
}
►4.4.2 管理平台截图
5.客户端设计
作为整体 Hybrid 离线包应用架构中的重要一环,端内 Hybrid 离线包 SDK 包括 Webview 管理、离线包管理、Bridge 三个模块。
5.1
Webview管理
定制 Hybrid 浏览器,设置可通过特定 Scheme 协议打开;
如果没有离线资源,可以降级 HTTP 请求,也可以选择阻塞下载;
通过 H5 应用映射表匹配当前页面 Url 和缓存资源,存在缓存资源时,Hybrid 浏览器拦截 H5 所有资源请求,执行本地缓存逻辑:命中缓存时直接返回本地资源;未命中缓存则交还给WebView进行默认处理;
关闭 Hybrid 浏览器时触发离线包管理逻辑,进行资源更新。
5.2
离线包管理
APP 预置:APP 打包时可以使用命令行工具批量下载需要预置的离线包,并集成到 APP 中;
预加载:有时候出于 APP 体积的考虑,我们不能预置所有离线包,为了提高离线包的加载体验,可以开启预加载,在 APP 启动后的空闲时间主动进行离线包下载;
更新:根据唯一性原则,同一个 H5 应用同时只保留一份离线资源;
磁盘空间管理:及时删除下载失败或已解压完成的 ZIP 包;清理旧版本离线资源;结合 LRU 算法进行离线包缓存上限管理。
环境隔离
5.3
环境区分及降级处理
H5 应用区分测试、生产环境,不同环境匹配不同离线资源;
通过预支的字段开关可以控制是否启用离线包逻辑,开关关闭时直接使用线上资源。
5.4
Hybrid 离线包方案的下一步规划
Hybrid 方案在预加载模式下取得了较好的效果,有效的提升了 H5 页面的秒开率,后续将在以下几个方面继续提升 Hybrid 方案的能力,更好的为主机厂相关业务助力。
增加并完善预置离线包能力:在 APP 大小可控的前提下,在 APP 内预置关键页面的离线包,弥补预加载逻辑在第一次打开时命中率低的不足。APP接入预置离线包后,页面第一次打开时预计资源命中率提高到100%。完成相关方案如下:
增加 One Shot 能力(类小程序):支持在 Hybrid 浏览器首次访问 H5 应用时,实时下载离线资源包并匹配离线资源。APP接入One Shot 能力后,页面第二次打开时预计资源命中率提高到100%。相关方案如下:
6.踩过的坑
6.1
Post 请求丢失 Body
iOS 系统,浏览器拦截协议有 NSURLProtocol 和 WKURLSchemeHandler 两种,并且都存在 Post 请求丢失 Body 的问题,针对 Post 请求都需专门处理。
开始时使用的是 NSURLProtocol 协议,优点是可挑选处理请求,不需要处理的可抛回浏览器,但是在特殊场景下出现问题:NSURLProtocol 是全局拦截,打开后所有浏览器都会进行拦截,所以在多个浏览器同时存在,并且非Hybrid浏览器还有发送 Post 请求时,Post 请求 Body 会丢失,导致请求失败。NSURLProtocol 的全局拦截问题无法解决,于是又将目光移向 WKURLSchemeHandler,开始了完全自己实现Http请求。
6.2
无侵入式拦截
WKURLSchemeHandler 需要解决的问题有很多,包括Cookies,重定向,Post请求支持等。最开始沿用了NSURLProtocol Post 请求处理方法,业务方将Post请求通过桥的方式,扔给Native进行请求,但紧接着就遇到另一个问题:需要业务方配合改造。作为一个通用平台,接入成本过高是一个致命问题,直接影响业务方接入的意愿。
实现无侵入式拦截,是我们必须要解决的问题,最终通过多次实验,采用了JS注入拦截的方式,具体流程主要为以下几点:
若命中离线,加载网页时开启Handler拦截,注入 Fetch / XMLHttpRequest 拦截请求脚本;
发送请求时,Post请求通过Bridge发送给Native,Get请求又传递给原生应用进行存储处理WKURLSchemeHandler;
Handler拦截的请求若命中离线,走本地资源匹配,匹配到后模拟请求返回H5,未命中时Native发送请求;
Post请求,Native通过桥拿到url和Body信息,通过原生请求发送,结果透传给H5。
7.总结
随着移动设备整体性能的持续提升,离线化 H5 Hybrid技术架构在加载和交互体验方面取得了显著改善。在许多标准应用场景中,它能够提供接近原生应用的用户体验。此外,它的跨平台、低成本、充足的人才资源、丰富的生态系统和动态更新等优势,使其在与其他跨平台解决方案的比较中脱颖而出。
作者|