作者介绍
Slmyer,货拉拉高级前端工程师,负责货拉拉司机平台运力相关Web开发工作,在打包构建以及性能优化方面有较丰富经验。
注:头图来自 unsplash.com
一、背景
为了提高用户体验,对于如何提升页面加载性能一直都是老生常谈的话题,常见的方案就是通过分包来处理资源加载的大小、开启 gzip 压缩、控制并发请求、懒加载、预加载以及服务端渲染等等,具体方案就不一一赘述。
针对 APP 内嵌的 H5 页面,还有一种比较常见且有效的方式就是接入离线包,通过把页面加载需要的静态资源提前下载到客户端本地,避免页面加载时静态资源网络请求的开销,从而来提高页面的加载速度。
目前货拉拉自研的离线包 SDK 已经上线推广,货拉拉司机侧于前段时间成功接入了离线包,所以写一篇文章来总结下 H5 项目接入离线包的实践经验。
二、离线包工作流程
货拉拉自研离线包 SDK 的工作原理可参考这篇文章:货拉拉H5离线包原理与实践 - 掘金
本文更多站在前端的角度,来讲述 H5 项目接入离线包时所需的改造和实践经验。
总体上,针对要接入离线包功能的 H5 项目,需要进行以下三步:前端工程化平台开启项目离线包功能,增加 URL query 参数告知客户端离线包的业务名(bisName),客户端配置开启离线包功能。
1. 前端打包处理流程
针对于开启离线包功能的项目,在正常打包完成之后,会对打包好的静态资源文件的引用路径配置做替换处理(即 publicPath
替换成相对路径)并生成离线包配置文件(离线包版本等信息),最后打包成 zip 包提供下载。
2. 客户端处理流程
根据 H5 页面的 URL query 参数来判断是否需要开启离线包功能,通过 bisName 查询离线包后台是否需要开启或者更新离线包,从而进行离线包的下载、解压,二次进入页面时离线包生效。
三、接入工作
目前货拉拉自研的离线包 SDK 是使用加载本地文件路径的方式,但是 H5 跑在本地文件路径(即 file://
协议)下可能存在一些问题,所以在接入离线包前,我们需要对 H5 项目进行改造。
接下来就是要介绍我们 H5 项目的具体改造工作,为了使接入改造对原有项目的侵入性以及改动量较小,我们主要做以下几点改造。
1. 路由改造工作
目前我们团队中大部分 H5 新项目使用的是 History 路由模式,考虑到如果接入离线包需要将入口地址更改成 Hash 模式,可能会遗漏一些地址的修改导致页面跳转失败问题。所以这边通过在路由实例生成之前,通过 URL 参数 offweb
来动态判断使用 Hash 或 History 的路由模式。
2. 离线包模式下的路由懒加载
因为我们是项目整体接入,在打包时我们不能统一的将 publicPath
(静态资源引用路径)修改成相对路径来配合离线包,对于正常线上模式或者离线包的场景下前端工程化平台会帮我们替换 publicPath
变量的地址为 CDN 地址或相对路径。所以我们在借助前端工程化平台替换注入的 publicPath
变量的能力下,不做任何变动,但是要额外处理下离线包模式下的路由懒加载。
关于为什么要使用路由懒加载:
通常情况下,webpack 会根据动态 import
以及 entry
入口来进行 splitChunks
分包。在离线包的场景下,我们通过更改 publicPath
来解决异步组件的引入路径问题,从而使在离线包场景下,首屏加载的资源文件通过路由懒加载来控制文件大小,从而加快首屏渲染速度。
通过引入 publicPath.js
判断是否是离线包环境,然后动态去设置异步静态资源的 __webpack_public_path__
来覆盖默认的 publicPath
(对 entry
无效),来解决异步组件加载问题,配置参考这里。
// publicPath.ts
const isOffline = window.location.origin.includes('file://');
(__webpack_public_path__ as any) = isOffline ? './' : '/';
__webpack_public_path__
可以设置file-loader
的publicPath
,
Umi
应用开启动态路由模式以及publicPath
,参考 runtimeHistory、runtimePublicpath
通过本地打包测试,使用路由懒加载相对与未使用路由懒加载的情况下,首页加载资源大小减少了约 150KB(不含 gzip 压缩)。
3. 接口跨域处理
目前我们线上模式的请求链路大致为H5 → 网关 → 后台服务
,大部分的项目都是通过一级通用网关来处理跨域问题,但是由于离线包模式下需要网关设置允许 origin: null
的场景跨域,直接在通用网关处理可能存在安全问题。
我们的解决方案大体是后端服务接入二级网关,通过增加自定义请求头来标识具体的后端服务,从而定制 CORS 跨域规则。所以在离线包情景下,我们的请求链路变成了如下。
离线包文件 → 通用网关 → 二级网关(根据请求头标识具体服务) → 具体服务
最后,在前端请求后端接口时,带上以下几个自定义 headers,以此来解决接口跨域问题。
headers: {
'offweb': '后端服务名',
'hll-appid': '后端服务名',
},
4. 关于 localStorage
由于 localStorage
的 origin
限制,我们选择降级处理不使用 localStorage
, 利用第三方库来存储页面状态。如需要持久化存储,可以借用客户端的能力或者使用后台服务来存储。
5. URL query 参数
我们需要在 H5 项目链接增加 offweb=业务名
参数来告知客户端当前页面需要开启离线包功能,从而进行接下的离线包流程。
H5 的 URL 链接手动添加参数的工作可能会比较繁琐,因为某个 H5 可能会有很多个入口,也可以选择让客户端配置映射关系自动追加离线包标识的 query 参数。
6. 收集数据
至此,对于离线包的项目改造工作,我们基本上是改造完成了。在这个基础上我们可以增加一些埋点来对离线包与线上模式做一些对比分析,统计加载时长,离线包与非离线包的对比,最后我们补充上相应埋点上报就大功告成了。
// 记录离线包或线上模式加载时长
window.addEventListener('load', () => {
// 区分是否离线包模式下的访问
const isOffline = window.location.origin.includes('file://');
const track_name = `driver_bbs_dclub_${
isOffline ? 'offline' : 'online'
}_loadtime`;
// 自定义埋点上报
const time = window.performance.now();
sensors.track(track_name, {
loadTime: time,
});
});
四、效果对比
1. 司机行为分项目
小拉司机 iOS 端于6月7日上线离线包功能,13日开启版本全量之后,行为分项目用户离线包的占比 5.54%。同时 H5 页面加载速度,从之前的平均 1.3s 降低到了 400ms,提升了 900ms 左右。
离线率 (iOS端):
行为分页面加载时长:
2. 司机社区项目
司机社区从6月23日开启离线包模式后,从进入页面到发起第一条业务请求的时间从平均的 1.2s 降低到了 600ms 左右,提升 600ms。同时页面加载完成时间也从 1.4s 降低到了 650ms 左右,提升了 750ms。
首次请求前的等待时间:
页面加载时长:
五、总结
通过相关的埋点上报数据,可以看到接入离线包后,我们 H5 项目的页面加载速度有明显提升,进入页面到首次请求前的等待时间也更少了。为了提升用户体验,接入离线包也不是唯一的手段,在离线包的同时,我们同样可以做打包优化和代码优化。