13 高级进阶:保证首次加载为秒开的离线包设计
上一讲我整体介绍了 Hybrid 的性能优化整体分析,简单介绍了离线包、Webview、骨架屏、SSR 及接口预加载等优化方案,这一讲我就来专门介绍下其中的离线包。
离线包的作用我在上一讲已经提到过了,它可以最大程度地摆脱网络环境对 H5 页面的影响。有关利用它来保证页面秒开,我也是 2018 年才真正有过实践。
记得那是我们业务 App 2.0 升级的时侯,技术 VP 报了一个手机首页访问时间长的问题,我们详细定位性能平台后给出了一个回复是,弱网环境下导致白屏时间过长。因为之前常规的优化手段已经做了,但效果还是不明显,技术 VP 就问,如果是弱网的问题,为啥淘宝首页还可以打开?我们仔细验证了一下,发现原来淘宝使用了离线包。于是我们开始了离线包实现之路。
我们先看一下离线包的整体实现方案。
上图左边是各模块对应的角色,有 FE 工程师(也就是前端工程师)、Admin、用户,右边是具体做的事情。
具体来说,离线包的前端源码主要包括 HTML、SCSS、Img 等内容,FE 工程师在需求开发过程中,先从 GitLab 中下载前端工程到本地,通过 CI/CD 将离线包版本上线。接下来,为了便于管理这个离线包,我们需要开发一个它的管理后台,使用的技术包括 Egg 框架和 MongoDB。
最后,我们将离线包存储在 CDN 上,当用户进入 App 向服务器发起静态资源请求时,客户端会拦截下来并根据内置离线配置,请求离线包管理后台。离线包管理后台返回结果,客户端决定是直接使用全量包,还是请求差分包。当这部分工作完成后,客户端将请求代理到它内置的离线资源中,返回给用户页面内容。
整个过程包含离线包生成、离线包管理后台、离线包部署及优化三部分。接下来我就详细介绍下。
离线包生成
如何生成离线包呢?我们可以把需要离线的资源,如首页要用到的 JS、CSS 和图片,通过 webpack 插件(ak-webpack-plugin,腾讯 Alloy 团队出品)打包生成压缩包。这个过程大致分为三步。
第一步,将前端项目从 Git 仓库中 clone 出来,然后打出一个 offline 分支。
第二步,拷贝离线包专用的 webpack 配置文件到项目中,我们在此开源项目的基础上做了一些修改。比如,修改对应的 package.json 文件,这样在本地测试时可以直接将离线包拷贝到测试机上,以方便进行测试。
"builduploadtest": "node build/uploadtest.js",
"buildupload": "node build/upload.js”,
第三步,通过 npm i 安装所需的包,并执行命令 npm run build 查看效果,然后同步修改config/offline.js 中的对象 URL 为页面真实 URL,修改导出静态资源的路径为真实的 CDN 资源路径。这一步决定了离线包的资源对应关系,如果出问题,线上会出现 404 ,所以千万要注意。
下方代码中 offlinepath 对应的就是你需要拦截的静态资源路径,拦截这个路径后, 客户端从对应本地的离线包中加载资源。示例代码如下:
{
"bizid": 13,
"date": "1513681326579",
"ver": "20171219185710",
"offlinePath": [
"c.58cdn.com.cn/youpin/activities"
]
}
从中你可以看到,线上资源路径和离线包资源路径映射对应,我们设计时把 URL path 转换成了本地的文件目录,可以让客户端代理时规则解析更简单。
offline
|-- c.58cdn.com.cn
| |-- youpin
| |-- activities
| |-- index.html
| |-- js
| |-- index.js
在这过程中,我们不需要关注资源之间加载的依赖关系,比如首页资源依赖,也不需要关注具体的业务逻辑是列表页还是详情页,只需要关注 webpack 最终构建出的内容结构即可。
另外,如果一些资源不需要走离线包,比如非首屏的图片资源,可以选择 webpack 排除的方式设置。做完这些操作后,我们可以将离线包功能封装进脚手架里,这样在初始化一个项目时就可以直接接入离线包。
前面提到过,离线包是下载本地之后生成的,那如何保证离线包的内容是最新的呢?这就要用到离线包管理后台了。
离线包管理后台
离线包管理后台主要是提供离线包监控及配置管理的平台,我们可以通过它查看某条业务的离线包使用情况,比如是否在使用,离线包版本是多少,启用时间多长等等。除此之外,还可以通过它开启和关闭离线包。
那这个离线包管理后台怎么搭建呢?
首先要注意的是,管理后台的技术栈选型最好和性能平台保持一致,这样可以尽最大程度与性能平台相互复用组件和页面模板。具体的选型,我使用 React 技术栈进行开发,模板使用 Ant design,服务端采用 Node.js 的方案,后台存储采用 MongoDB 来实现。在这个后台上,主要通过三个核心页面——全局页、离线包列表页、详情页来完成对离线包的管理功能。
首先是全局页,它提供离线包管理功能,可以开启和关闭离线包。由于这个开启和关闭是针对所有离线包的,所以它的权限设置要高一些,防止误操作带来的线上问题。
其次是离线包列表页,主要是对所有的离线包资源进行展示和操作。这个列表页包括展示业务名称、版本号、包类型、发布时间、在线情况。此外这个列表页上还有可以点击在线和离线的按钮,方便离线包上下线操作,以及进入详情页的链接。
离线包列表页里包类型是我们实现的一个大亮点,离线包的类型一般包括差分和全量包,我们可以通过它们大大减少用户的流量使用,降低下载离线包的时间。
具体来说,在 App 里面,我们会先内置一份全量包作为基线版本。但如果我们判断当前版本不是最新的话,一种做法就是下载一个全新的版本,第二种做法是下载一个差分包的版本,第三种做法是直接绕过离线包,请求线上接口。
显然第三种最不可取,它等于没有了离线包优化的效果,第二种呢,全量包体积都比较大,以我们公司业务为例,将近 600K 的大小,而如果用差分包,平均 200K 左右的大小,并不影响性能。
那怎么实现差分包呢?这就需要用到 BSDP 了——一个基于二进制 diff 的 Node 工具包。它有两个核心模块:bsdiff 和 bspatch。
其中 bsdiff 是个库函数,用于对源文件和目标文件求 diff,生成差分包。当我们要升级离线包版本(比如升级首页金刚位功能),就可以使用它来发布一个差分包,放在 CDN 上 ,同时生成配置文件,如 2021030701 放到管理配置后台上。这样客户端请求静态资源之前,先查看本地配置文件,发现配置文件是 2021030606 版本的,就直接去请求对应的差分包。而 bspatch 主要是用来根据差分包的内容,合并本地版本成一个新的全量包。
最后是详情页。详情页主要是方便我们查看下载离线包检查内容是否正确,以及设置业务优先级。为什么要设置业务优先级呢?
因为随着业务规模的扩大,每条业务都想使用离线包来提升页面性能,离线包体积就越来越大,这时候必须得设置优先级,给流量大的业务优先使用离线包,其他业务暂停使用。我们现在仅针对金刚位( App 首页核心导航位)提供离线包功能。
离线包部署及优化
离线包的部署流程见下图:
前端工程师也就是图中的 FE,将前端工程打包,生成离线包的入口页面 index_sonic.html (支持离线包的index.html),然后通过前端的静态资源发布系统(我们公司使用的是 beetle,类似jerkens 上线,增加了 web 界面)上线到 CDN。
接下来,FE 将静态资源(如 index.js、home.css、banner.jpg)打包成全量离线包到 CDN,然后同步增加离线管理后台的配置,离线管理后台会根据基础包生成差分包上传到 CDN。
这就是整体的部署流程。如果离线包功能异常(如出现无法访问),该怎么快速解决?
我建议一定要做好离线包的开关功能。在出现问题时,通过在离线包后台操作,及时关掉离线包功能,就可以及时确保用户功能恢复正常。
比如,我们在某一次详情页升级项目中,发现用户客户端在下载离线包时出现网络问题,导致无法解压,进而页面内容无法展示,当时我们的做法就是立即关闭离线包功能,保证了用户正常访问。
小结
好了,以上就是离线包的实现方案,这里面有两个注意事项。
第一,在 iOS 系统,我们经常会用到 WKWebView ,此时如果要实现离线包,必须解决 WKWebView 下面的请求拦截难题,这时可以借助私有 API 方案来实现。
第二,问题的诊断定位流程和原来不一样了。平常的定位问题只需要抓包,查看 source 即可,而在离线包的问题诊断,需要我们先抓包getofflineconfig 接口,找到对应的 bid,然后根据 bid 找到正确的配置项,点击配置项进入详情下载离线包,最后解压离线包确认代码是否正确。
下面为你留一个思考题:
使用离线包功能的页面,前端工程师上线操作过程中遇到问题时如何回滚?
欢迎在评论区和我交流,下一讲我讲介绍瞒天过海的骨架屏及 SSR 优化手段。
# 精选评论
# 潘:
请问通过bspatch计算出来的差分包,和本地包合并后是怎么删除本地其他多余垃圾文件的?
# 讲师回复:
赞发现问题,现在使用的是定期清理的方案