非覆盖式静态资源发布

2,685 阅读9分钟
原文链接: mp.weixin.qq.com

本文为计蒜客前端团队负责人 Jasin Yip 在 CODING 技术小馆北京站的演讲内容整理。


很高兴今天来到这里,我之前在大学里面给学生做讲课,这是第一次面对开发者。之前对于学生,我讲前端的东西他们是很懵的状态,希望今天不会。我做选题的时候,我想大家都在聊 Vue、React、Angular 等框架的东西,但像前端工程这种很原子性的东西大家很少做分享。于是会出现,懂的人都懂了,不懂的人也没有人去分享,我觉得这其实会造成一个技术断层。我很希望把这些原子性的东西分享给初级的工程师,或者一些他们想改进,但是他们又不知道怎么改进的人。这个非覆盖式静态发布,这是前端工程这个领域一个主要的话题,它属于构建方面,属于缓存管理的话题。

今天讲这个非覆盖式静态资源发布之前,我们先来看一下什么是覆盖式的静态资源发布。


我们从最简单的页面开始,这是一句话而已,我们引用了一个 foo.css,它只是给背景加了一个颜色而已。如果放到浏览器里面它的请求看起来会是怎样的?大家都经常看见,foo.css 的状态码是 200,一切看起来很完美。


然而真的是这样吗?我们想一下我们在真实生产环境中,不可能只有一个静态资源,我们有很多静态资源,我们可能是几十 K 几百 K 甚至几 M 那么大,我们有这么多东西加载,用户每一次访问我们的页面都要加载,时间肯定很长,而且浪费人家的流量。我们说怎么才能节省这些流量?我们怎么才能加快加载速度?我们计算机领域经常利用的手段就是缓存。

首先浏览器自己自带一个缓存,他会在第二次加载同一个资源的时候,他会问一下服务器这个资源有没有更新?没有更新的话,那么服务器返回 304 的状态码,告诉他不需要加载这个资源,直接读本地资源。


这种缓存我们称之为协商缓存,这种协商缓存节省很大流量,因为不需要再完整下载,浏览器只问一下服务器而已,他只跑一个来回。看起来优化了很多,但我们说能不能这个请求也把它干掉?这里我们就要在 HTTP response headers 里加 Expire 或者 Cache-Control 来设置强制缓存。开启强制缓存之后,我们可以看到这个时候 foo.css 的 size 是没有的,它会写 form disk/memory cache,因为是直接读的本地资源,所以它的加载就会非常快。


我们第一次部署就这么优雅的完成了,但是,怎么更新代码呢?

因为我们的代码是从本地读的,那么如果我们迭代——刚才说到互联网产品是不断迭代的——的时候,如何更新代码这是一个问题。有人说我们这样,在后面加一个项目版本号,项目有更新的时候,我们读一下项目版本号,那么得到一个这样的,在后面加一个 hash 串,我们可能会觉得这样很完美。


因为它更新的时候就会改版本号,这个时候浏览器知道这个缓存已经失效,那么重新从服务器拉一次。但是,我们刚刚也说,我们会有很多文件,很多静态资源,如果现在 foo.css 改的话,我的项目版本号就会更新,我依然会把所有其它资源的版本号改一遍。这意味着虽然我只更新一个文件,但是所有的文件都需要重新下载,这样很浪费。我们如何更新一个文件,让用户只下载一个文件,我们如何实现精确一个文件粒度的更新呢?我们会想一个想法,能不能让资源后缀和资源内容关联起来,如果内容没有变化的话,我们后面的一个后缀就不改变了;如果有改动,我们就把后面的戳改动。这里有一个算法就是数据摘要算法,这个算法大家听的比较少,如果我把这些名词列出来,你们就可能会知道:MD5、SHA1、 SHA256。这些算法会根据不同的文件内容来生成不同的 hash。我们把它加在后面是这个样子。这个时候我们发现如果它的文件内容改了,这样我们精确到文件的力度去更新代码。


那么还有一个问题没有解决,先更新页面还是先更新静态资源

假设我们先更新页面再更新静态资源,但是页面完成资源没有更新,这个过程会出现什么问题?新页面里我们加载旧的资源,页面和资源对应不上,会有页面混乱,还有执行会报错。还有一个问题,错误加载的资源会被浏览器再一次强制缓存起来,除非用户通过强制刷新把这个缓存去掉,否则缓存过期才能更新这个资源。

我们说这个不行的话我们换一个思路,先更新静态资源再更新页面。在静态资源更新完成,页面没有被更新过程中,有缓存的用户是正常的。这个时候读本地的缓存,但是如果没有缓存的用户会怎样?依然是会页面混乱和执行错误,因为在旧的页面加载新资源。当然这部分的用户在我们部署完成之后是可以正常访问,但是这个过程,无论是更新页面还是更新静态资源,都会造成用户感知


于是老板说,半夜再更新吧。

因为那时候没有人用,这个时候程序员说你是凯丁吗(Are you kidding)?而且我们也不能写一个定时的脚本去跑,因为我们的 UI 测试是很难完全自动化的。我们可能上线之后再测一下,如果有问题得回滚或者马上修,那么你就能加班。我们说真的没有办法了吗?我们想一下导致这个问题的罪魁祸首是什么?对,真相只有一个就是覆盖式的静态资源发布策略从根本上有问题。


我们回到这个页面,虽然我们为每个文件都加了戳,但是我们的文件名没有改变,我们更新代码的时候,文件是直接被覆盖掉的,所以会造成这些问题,怎么更新都不是。我们想,能不能把这些戳加到文件名上面呢?这样的话,我们更新的时候,不会把旧的文件覆盖掉,因为它们的文件名是不一样的。这样的策略我们称之为非覆盖式静态资源发布策略。再来一道送分题,先更新页面,还是先更新静态资源?先是资源对吧,没错。


非覆盖式发布策略好处都有什么?第一是用户无感知,我们可以平滑升级;第二是缓存管理精确到文件;第三是可以设置超长的缓存时间,比如一百年;甚至,我们还可以支持灰度更新。让这个用户加载这个页面,那个用户加载那个页面,他们引用资源不一样,他们的表现不一样,这样可以灰度更新。那么有哪个网站用这个策略?Facebook,他们都是加戳了。github 也是这样,我们今天的举办方 Coding 也是这样的,计蒜客也是这样。



说到这里,非覆盖式静态资源发布的话题已经聊完了,但是我们做完了这个还可以做什么?我们可以留意到,计蒜客我们有三个 JS 文件名,第一是 jsk-base,第二是 jsk-widget,第三是 pack。


这三个是我设计的计蒜客静态资源管理三层结构。第一层 base 就是基础层,会放置一些我们很久不会更新的东西。第二是组件层,我们自己有一套计算 UI,这是组件层放的东西,它的更新频率比基础层快一点这;还有就是业务层,业务层就是一周更新 N 次,就是业务相关的事情。

那么怎么实现?我手动配置了一个 resource.json 的文件,我们把 jQuery、Ajax-setup 等写到 base 层,把 JisuanUI、CodeMirror 等写到 widget 层。

style 也是一样的。刚才说完了基础层和组件层,我们再看看业务层。比如我用PHP 的方法,然后我们弄成这样,一个逗号在中间,把他们打包在一起生成一个。我们下面会传进去,然后加上业务代码加载,我们最后生成出来是这样的,比如说这里有 base,这里有 widget,这里有 App。


当然开发过程中不会打包成这样,会一个一个文件的加载。在外面我们封了一层,虽然我们把他们加进来,但是他们在不同的文件中,这样非常容易调试。我分享的话题就是这些,最后卖一个我自己的广告,前端工程师的自我修养,是我周二讲的知乎 Live,大家有兴趣的可以报名。


- Q/A -

Q:非覆盖式的更新,如果新的技术比较频繁,会有很多旧代码,怎么解决?

A:硬盘不值钱,而且一年下来也不会有多大,你觉得不舒服可以定期一年清一次。

Q:你不是有组件层,组件层的时候代码量是比较多的,打包的时候也是打包在一起。如果改的稍微少一点,你还要重新打包,这样的话会不会比较浪费?

A:我之所以分三层就是为了区分不同更新频率的层,我希望尽可能在多页面利用他们的缓存。我们加载第一个页面的时候我们可能加载三个,第二个页面前两个可以复用,我只是加载业务层。