前端更新之道:探索覆盖式与非覆盖式策略
关于前端资源的更新,这是前端开发领域中一个极其引人入胜的话题。通常,我们作为开发者,大部分时间都专注于业务逻辑的实现、页面交互的设计与优化,而对于如何将这些更新部署到服务器、如何确保用户能够访问到最新版本的内容这类问题,往往不会过多地涉足。
在深入探讨前端资源更新的策略之前,有一个不可忽视的重要话题——更新的风险:缓存。
缓存 - 锋利的双刃剑
浏览器缓存是大家非常熟悉的概念。它的主要作用是缓存资源,从而提升下一次加载的速度。
然而,为什么浏览器缓存有时会带来风险呢?在探讨这个问题之前,我们先来详细了解一下,为什么我们需要缓存。
在遥远的 4G 年代,网速感人,加载 JS、CSS 等静态资源文件,都需要花费较长时间,浏览器将长时间处于白屏状态,影响用户体验。
并且 JS、CSS 这些静态资源文件,其实在很长的一段时间内,内容都不会更新,如果每次页面刷新都去服务器请求内容,其实是没有必要的,而且也会增加资源服务器的访问压力。试想一下,如果每次加载页面都需要等待十几秒钟的时间,甚至更久,那该是多糟糕的事情。
所以,HTTP 缓存应运而生,当浏览器使用 HTTP 缓存后,若资源命中缓存,则会从本地缓存中拉取资源,而非从服务器拉取资源。
如何更新资源
好了,请求方面的优化已经达到变态级别,那问题来了:你都不让浏览器发资源请求了,这缓存咋更新?
这里有一个突破点,刚刚我们也提到了,
HTTP 缓存是针对 URL 进行
,因为 URL 是资源的唯一标识符。也就是说我们通过修改文件的 URL 访问路径,浏览器并会认为这是全新的资源,从而向服务器发起请求,从而获取最新的数据。
好了,我们现在尝试一次资源发布:
假设我们当前的线上版本是 1.0.0,更新资源时,将 HTML 文件里面关于静态资源的文件引用修改为 1.1.0,因为 URL 地址发生了变更,所以浏览器并会认为这是新资源,从而发起新的服务器资源请求,至此,我们的页面就得到了更新,更新为了 1.1.0 版本。
OK,问题解决了么?!当然没有!我们思考下这种情况:
页面引用了 3 个 CSS,而某次上线只改了其中的a.css,如果所有链接都更新版本,就会导致b.css,c.css的缓存也失效,那岂不是又有浪费了?!
我们不难发现,要解决这种问题,必须让 url 的修改与文件内容关联,也就是说,只有文件内容变化,才会导致相应 url 的变更,从而实现文件级别的精确缓存控制。
什么东西与文件内容相关呢?我们会很自然的联想到利用数据摘要要算法
对文件内容进行求摘,将摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。
好了,我们把 url 改成带摘要信息的:
这回再有文件修改,就只更新那个文件对应的 url 了,想到这里貌似很完美了,但实则不然,
现代互联网企业,为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到 CDN 节点上,也就是说我们的 HTML 页面和静态资源需要分开部署。
那我们是先上线页面,还是先上线静态资源?
先部署页面,再部署资源:
我们优先部署 HTML 页面,紧接着部署静态资源,如JS、CSS,但是在二者部署的时间间隔内,可能就会有用户访问页面,此时仅更新了页面,资源未被更新,就会在新的页面结构中加载旧的资源。那在新的页面结构中加载旧资源会有什么问题呢 ?
假设我们这次上线的代码经过了重构,将原先的 a.js
、b.js
、c.js
的内容进行重构,那重构之前,必须按照 a、b、c
的顺序来加载,那重构之后,必须按照 c、b、a
的顺序来加载,因为后者依赖前者的代码,那现在我们更新了页面,没有更新代码。 其结果就是:用户访问到了一个白屏的页面,除非手动刷新,否则在资源缓存过期之前,页面会一直执行错误。
如果先部署资源,再部署页面:
我们在将前面那个情况搬过来讲,优先部署态资源,紧接着部署 HTML 页面,两者部署时间间隔之内,资源先被更新了,此时重构后的资源,加载顺序必须是 c、b、a
, 但是由于 HTML 页面还是老页面,需要按照 a、b、c
的顺序来加载,那此时新用户访问页面时就会出现白屏。
但如果是老用户的话,是不受影响的(因为缓存仍然生效),HTML 中静态资源引用地址并未变更,所以即使我们更新了静态资源,老用户的浏览器仍然会使用当前缓存文件。
所以,归根结底:先部署谁都不行!本质的问题是页面和资源的新老程度不匹配导致的问题,
访问量不大的项目,可以让研发同学苦逼一把,等到半夜偷偷上线,先上静态资源,再部署页面,看起来问题少一些。但是,没有这样的绝对低峰期
,只有相对低峰期
,晚上冲浪的用户仍然会受到影响。为了稳定的服务,还得继续追求极致啊!
每次到了发布,工程师就要熬夜,身体也是越熬越差、头发也是越熬越少,下定决心,我们一定要解决这个办法。
覆盖式发布 VS 非覆盖式发布
这个奇葩问题,起源于资源的覆盖式发布
,用待发布资源覆盖已发布资源,就会出现这种问题。解决它也好办,就是实现非覆盖式发布
。
覆盖式更新:覆盖式更新是一种通过在资源文件的 query 中添加摘要信息(如哈希值)来强制浏览器更新资源文件的方法。这种方式会导致每次更新时直接覆盖旧文件,因此浏览器会下载新的资源版本。这种方法虽然简单,但存在一些问题:
- 风险高:每次更新都会直接覆盖旧版本文件,如果新版本存在问题,会导致整个应用无法正常运行。
- 回滚困难:由于旧版本文件被覆盖,若需要回滚到旧版本,需要额外的备份和恢复操作。
- 用户体验差:用户在更新期间可能会遇到资源加载失败或中断的情况,影响使用体验。
因此,覆盖式更新虽然可以强制升级,但在实际应用中并不推荐。
非覆盖式更新:非覆盖式更新通过将摘要信息(如哈希值)放到资源文件的发布路径中来实现。这种方式在每次更新时,旧文件和新文件都会共存,不会相互覆盖,从而实现平滑升级。具体优势包括:
- 风险低:旧版本文件仍然存在,如果新版本有问题,可以快速回滚到旧版本,确保服务的稳定性。
- 平滑升级:用户可以在不同时间段逐步获取新版本资源,不会因为版本更新而导致资源加载失败或中断。
- 更好的用户体验:在后台更新文件,不影响用户的当前使用,确保应用的连续性和稳定性。
从上图中,我们可以明显看到两者的区别。在覆盖式发布场景下,即使进行多次发布,文件 a
始终只有一个版本,文件内容会被覆盖更新。
而在非覆盖式发布场景下,文件的摘要信息被直接打入文件名中,即使进行多次发布,也会保留多个版本,文件不会被覆盖,始终保持多个版本共存。
在这种情况下,先全量部署静态资源,再部署页面,可以确保资源的版本与页面代码配套,这样可以完美解决版本管理的问题。
在资源非覆盖式发布的场景下,回滚过程相对简单友好。我们可以将每次页面发布的最近一次 Git CommitId
作为文件夹的名称,并将对应的资源统一存入该文件夹中。
当需要回滚时,只需将 Nginx
的资源目录指向需要回滚的文件夹,即可快速、安全地完成回滚操作。
非覆盖式发布就真的完美了吗
-
磁盘空间占用: 非覆盖式发布需要同时保留多个版本的资源文件,这会占用大量的磁盘空间,尤其是在资源更新频繁的情况下。这可能导致存储成本增加,并需要定期清理旧版本的资源文件以释放空间。
-
安全和权限管理: 多版本资源的管理需要严格的权限控制,确保只有授权用户才能发布和回滚版本。同时,旧版本资源的保留可能会引发安全隐患,需要定期检查和清理过期资源。
-
文件迁移的复杂性: 当我们的服务器发生变更或者我们不得不将当前文件迁移至其它服务器时,因为文件的数量众多,对于如何安全迁移也是一项非常大的挑战。
总结
我们详细分析了前端资源更新的两种主要策略:覆盖式更新
和 非覆盖式更新
。通过覆盖式更新,开发者可以直接替换旧的资源文件,实现快速简便的更新,但这种方法可能会导致缓存问题和版本控制的复杂性。另一方面,非覆盖式更新,通过引入新的资源文件并保留旧文件,提供了更为安全、可控的更新过程,尽管这可能需要更多的存储空间和细致的版本管理。开发者可以根据自己的需求和资源状况,选择最合适的前端资源更新策略。
参考
转载自:https://juejin.cn/post/7386231613866409995