洋河:静态资源增量更新实战解析

向作者提问
美团技术团队官方账号
查看本场Chat

2018年8月1日,周一晚20:30,美团高级工程师刘洋河带来了主题为《前端遇上 Go: 静态资源增量更新的新实践》的交流。以下是主持人张帅整理的问答实录,记录了作者和读者间问答的精彩时刻。


内容提要:

  • 不能提前把这个版本针对最近若干个版本的 Diff 离线算出来然后存起来吗?
  • 这么复杂的增量更新,是怎么保证用户端老文件+补丁得到的新文件是可运行的?有出现过问题吗?
  • 对于静态资源来说,做增量的成本还是有些高了,为什么不考虑Service Worker?
  • Webpack 打包可以根据文件的 Hash 来单独处理文件后缀,所以每次更新代码可以只有确实修改过的代码才没办法利用缓存。这样的方法不适用你们的场景吗?
  • 补丁一般都是根据版本号来打的,如1.1的下一个版本是1.2,而自己才是1.0,怎么办?

问:不能提前把这个版本针对最近若干个版本的 Diff 离线算出来然后存起来吗?

答:其实我们是考虑过这种方式的,但是我们主要的场景是在 C 端,C 端用户访问产品的频次千差万别,于是用户浏览器中的老版本,数量很庞大,我们发版次数越多,,这个数量就越大,离线计算从技术上来讲是可行的,但由于数量多,业务方上线一次要准备的版本对就太多了。目前我们这种即时计算的模式,由于有缓存和前置的 CDN,实际每个版本对也只计算一次而已,为了防止业务上线时的瞬时冲击,我们对最热的若干版本预先进行了离线计算,这个版本数控制在个位,因此不容易影响业务方的发版时间,所以离线计算也是有的。


问:这么复杂的增量更新,是怎么保证用户端老文件+补丁得到的新文件是可运行的?有出现过问题吗?

:最早做这个事情的时候,假想的是不会出现问题,但后来还是出了,我们发现部分用户浏览器中有脏缓存,老文件有破损,打了补丁之后,新文件也是破损的,因此设计了两种方案来防止补丁打上之后出错,第一个方案是 try...catch,就是把业务方的代码包裹在 try...catch 里,如果发现错误,我们就放弃缓存,走全量更新,但是这种方案有缺陷,有时业务方代码自己逻辑有误,导致缓存被白白浪费,所以有了第二种方案,第二种方案是对每个新文件给出一个校验和,打完补丁之后,用校验和再验算一遍,如果不符合,就走全量,反之就可以安心使用补丁了。


问:对于静态资源来说,做增量的成本还是有些高了,为什么不考虑 Service Worker?

答:我们这套方案在客户端的 SDK,其实不只是做增量更新,如果不启用增量更新,也是有 Local Storage 和 Service Worker 组成的缓存功能,Service Worker 并不是不用,而是和增量更新配合使用,Service Worker 的工作模式是类似代理的形式,JS 主线程发起网络请求,会经由 Service Worker 所在线程,Service Worker 拦截请求之后,可以走全量更新的形式拉取静态资源,也可以走增量更新的形式拉取增量补丁,这样即使是用户的设备支持 Service Worker,依然可以减少更新的成本,而对于服务器来说,因为同样是吐出补丁,并没有额外的开发成本,另一方面,Service Worker 的兼容性比较有限,还有很多设备是不支持的,因此也不能只靠 Service Worker。


问:Webpack 打包可以根据文件的 Hash 来单独处理文件后缀,所以每次更新代码可以只有确实修改过的代码才没办法利用缓存。这样的方法不适用你们的场景吗?

答:这个分两部分来说,Webpack 打包是我们主要针对的场景,讲道理,每个文件,只要内容一样,文件名就应该一样,根据HTTP Cache,这个文件应当被长久地缓存下来,但现实是 HTTP Cache 在移动端的持久率没有想象中那么好,浏览器针对 HTTP Cache 的内容,保持的周期比较短。

另外 Webview 当中一些实现也会清理 HTTP Cache,这时候我们更想手动控制,所以自己做缓存的意义依然是有的,其二是发展的比较好的业务,JS 是经常更新的,刨去 Vendor 这样可能长期不变的 JS 文件,还有 APP 这样每次都会变的,但每次都变的文件,不代表文件的内容全都作废,很多时候是增改删一些东西,这是增量更新发挥用处的主要场景,因此有版本戳的 JS 和增量更新,并不矛盾。


问:补丁一般都是根据版本号来打的,如1.1的下一个版本是1.2,而自己才是1.0,怎么办?

答:我先说一下增量的作用范围,目前我们主要针对的 Webpack 来做,对于 Webpack 打包的文件来说,会有不同的 Chunk,每个 Chunk 在编译好之后会产出对应的 JS 文件,比如 APP 这个 Chunk,最终会产出 app.hashA.js,那么我们就认为,APP 这个文件的版本号是 HashA,如果 APP 的内容发生变化,那么下一次产出的文件就是 app.hashB.js,因此就出现了 HashA->HashB 这样的版本变迁,业务方继续更新就会出现 HashC、HashD 等,对于我们的服务来说,我们不区分版本之间的先后关系,我们只是对比不同的文件,来生成增量补丁。

因此,HashA 和 HashD 也可以生成合法的补丁,HashB 和 HashC 也可以,对于浏览器用户来说,无论他之前持有的版本号是什么,他都可以得到一份和最新版匹配的补丁,因此不需要逐次升级,而是一步到位的,版本依赖依然由 Webpack 来解决,我们是工作在 Webpack 的抽象层次之下的,我们针对的文件,指的是打包后的文件,而不是打包前。


本文首发于Gitchat,未经授权不得转载,转载需要与GitChat联系。

微信扫描登录