专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
前端大全  ·  太荒谬了!千人公司一刀切禁用 ... ·  3 天前  
前端大全  ·  大家都该学学的埋点概念与使用 ·  3 天前  
奇舞精选  ·  vercel是如何做微前端迁移的 ·  3 天前  
奇舞精选  ·  vercel是如何做微前端迁移的 ·  3 天前  
前端大全  ·  预测一波,最近前端即将起飞! ·  1 周前  
前端大全  ·  折腾我2周的分页打印和下载pdf实现 ·  1 周前  
51好读  ›  专栏  ›  前端早读课

【第3406期】为什么前端打包出来的静态文件名字是一串 Hash 值?

前端早读课  · 公众号  · 前端  · 2024-11-02 08:00

正文

前言

前端打包生成的静态文件名包含哈希值是为了实现缓存优化和版本控制,确保浏览器加载最新资源,避免缓存污染。今日前端早读课文章由 @Moment 分享。

正文从这开始~~

前端打包后,静态文件的名字被改成一串 Hash 值(例如 app.abc123.js 或 style.abcdef.css),主要是为了缓存管理和性能优化。这是现代前端工程中常见的做法,通常由打包工具(如 Webpack、Vite 等)自动处理。

启发式缓存

在 Web 应用和浏览器缓存中,服务器通常会通过 HTTP 头部信息(如 Cache-Control、Expires)明确指示一个资源可以缓存多长时间。但有时这些指示可能缺失,或者某些资源的缓存控制信息不完整,客户端就会依赖启发式规则来确定该资源的缓存时长。这种规则可能基于资源的特征、文件类型,或者历史经验等。

启发式缓存的工作原理基于以下几个步骤:

  • 资源请求:客户端请求某个资源,如果该资源没有明确的缓存过期时间,系统会选择启发式缓存。

  • 估算缓存时间:基于启发式规则(通常与资源的响应头信息或者资源类型相关),估算资源应该缓存的时间。例如,系统可能会依据资源最后修改的时间、文件类型等来推断合适的缓存时间。

  • 缓存存储:估算出缓存的时长后,客户端会将该资源存储在缓存中,直到缓存时间过期为止。

  • 过期后重新请求:当缓存时间到期后,客户端将重新发出请求来获取最新的资源。

它的主要使用场景有以下两个方面:

  • 无明确缓存指示的资源:很多静态资源(例如图片、CSS 文件、JavaScript 文件)可能缺乏明确的 Cache-Control 或 Expires 指令。在这种情况下,启发式缓存会基于资源的类型、最后修改时间等规则来估计缓存时长。

  • 动态内容:某些动态生成的内容(例如 API 返回的数据)没有明确的缓存控制头,但服务器返回的内容在一定时间内不会频繁更新。启发式缓存可以帮助提高性能,减少重复的网络请求。

启发式缓存使用的规则因平台或浏览器实现不同而有所差异,但常见的启发式规则包括:

  • 基于 Last-Modified 头估算:如果资源包含 Last-Modified 头,浏览器或缓存代理通常会基于该时间来计算缓存过期时间。一个典型的规则是将 Last-Modified 的时间距离当前时间的一小部分(比如 10%)作为缓存时间。例如资源最后修改时间是 2 天前,系统可以设置一个启发式缓存时间为 2 天 * 10% = 4.8 小时。

  • 基于文件类型:不同类型的资源可以采用不同的启发式缓存策略。例如:图片、字体等静态资源通常可以缓存更长时间(如 1 天到 1 周),而 JavaScript、CSS 等资源,虽然也是静态的,但由于与功能直接相关,缓存时间可能会短一些(如数小时到一天)。

  • 缺省时间设定:如果无法基于其他头部信息推断,系统可能会采用默认的缓存时间,比如 1 小时或 24 小时。

浏览器默认缓存

当用户首次访问网站并请求 index.html 文件时,浏览器会同时解析并加载其中引用的 JavaScript、CSS 等静态资源。但浏览器已经考虑到了用户的体验:如果每次访问都重新请求这些静态资源,不仅加载时间变长,服务器压力也会增加,严重影响用户体验。为了优化这一过程,浏览器会默认缓存已请求过的静态文件,这种默认的缓存机制就是启发式缓存。除非明确设置了 no-store,否则浏览器会自动缓存静态资源,避免重复下载,加快页面加载速度。

通过给文件名加上 Hash 值(通常是文件内容的 Hash),一旦文件内容发生变化,文件名也会改变。浏览器会识别出这是一个新的文件,从而重新加载最新版本的文件,而不是使用旧的缓存文件。

例子:

  • 第一次构建生成:app.abc123.js

  • 修改代码后构建:app.def456.js

其中的 abc123 和 def456 就是基于文件内容生成的 Hash 值。具体来说,Hash 值是对文件内容进行哈希算法(如 MD5、SHA-256)处理生成的一个字符串。这个字符串独特地表示了文件的内容。如果文件内容有任何变化,生成的 Hash 值也会不同。

这样,文件名不同,浏览器会重新请求最新的文件。在没有使用 Hash 的情况下,如果文件名不变而内容变了,可能会导致缓存污染问题。浏览器可能还会继续使用老版本的文件,导致用户访问的页面无法正确展示新功能或修复的 bug。

通过文件名中的 Hash,可以确保浏览器总是加载最新的资源,避免老版本的缓存文件污染应用。

Hash 值的作用

那既然知道了浏览器会有默认的缓存,当加载静态资源的时候会命中启发式缓存并缓存到本地。那如果我们重新部署前端包的时候,如何去请求新的静态资源呢,而不是缓存的静态资源?这时候就得用到 hash 值了。

下面模拟了掘金网站的静态资源获取,当请求静态资源的时候,实际访问的是服务器中静态资源存放的位置:

返回即是当前请求静态资源的具体内容:

第一次访问时,浏览器会请求服务器的资源并将其缓存到本地,比如 a035f68.js 文件会被缓存到浏览器的磁盘或内存中。接下来,当用户刷新页面时,浏览器会优先从缓存中读取资源(如从 disk cache 或 memory cache),以加快加载速度。

然而,如果前端重新部署后,假设 a035f68.js 这个文件名称保持不变,浏览器就无法知道这个文件已经更新了,因为浏览器默认会使用缓存中的资源,除非缓存已过期或被明确设置为不缓存。这种情况下,浏览器不会主动去请求服务器上的最新资源,导致页面无法加载到最新的内容,影响用户体验。

浏览器的缓存机制是通过资源的文件名来判断的。如果文件名没有发生变化,并且缓存策略允许缓存,且缓存未过期,那么浏览器将直接使用缓存中的资源。相反,如果文件名发生了变化,或者缓存设置要求重新验证资源,浏览器才会去服务器请求最新的静态资源,确保用户看到的是最新的内容。

优化后的描述重点强调了浏览器是通过资源名称、缓存策略和过期时间来判断是否使用缓存还是请求服务器资源的,这也是为什么前端打包后使用带有 Hash 值的文件名来保证资源更新。

第三方库如何处理

对第三方库的 Hash 处理主要涉及缓存优化和避免不必要的重新下载。这类库(如 React、Lodash 等)通常不会频繁更改,因此你希望尽可能利用缓存,但在库版本升级时,确保能获取最新的版本。

为了更好地处理第三方库的 Hash,你可以将第三方库(如 React、Lodash 等)打包到单独的文件中,而不与业务代码混合。通常可以通过 Webpack 的 splitChunks 插件或类似工具将库代码和应用代码分开。这样做的好处是:

  • 第三方库文件名的 Hash 值只与库的内容相关,而与业务代码无关。

  • 如果业务代码更新了,而第三方库没有变化,浏览器可以继续使用缓存中的第三方库文件,不必重新下载。

在 Webpack 中,可以通过以下方式配置 splitChunks:

 module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all",
},
},
},
},
};

这会将第三方库打包成一个单独的 vendors.[hash].js 文件,避免每次业务代码变更时都重新生成第三方库的 Hash。

另一种策略是将常见的第三方库通过 CDN 加载,而不包含在项目的打包文件中。这么做可以让这些库由 CDN 提供缓存,并且减少你本地项目的打包体积。例如,React、Vue、jQuery 等非常稳定的库都可以直接通过 CDN 引入。

在 Webpack 中,使用 externals 来避免将第三方库打包到项目中:

 module.exports = {
externals: {
react: "React",
"react-dom": "ReactDOM",
},
};

在 HTML 文件中通过 CDN 引入:

 <script src="https://cdn.jsdelivr.net/npm/react@17/umd/react.production.min.js">script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.production.min.js">script>

这样,React 和 ReactDOM 就会直接从 CDN 加载,不会打包进最终生成的 JS 文件中。

对于第三方库,文件名的 Hash 是基于文件内容生成的,因此库的版本一旦发生变化,Hash 值也会发生变化。为了确保 Hash 稳定且合理,你可以通过锁定第三方库的版本来控制库文件的变化。

在 package.json 中锁定依赖的版本号,例如:

 {
"dependencies": {
"react": "^17.0.0",
"lodash": "^4.17.21"
}
}

使用 package-lock.json 或 yarn.lock 文件确保构建环境的一致性,防止库的版本随意变动,导致每次打包的 Hash 都不一致。

通过锁定库的版本,如果库内容没有变动,Hash 也不会变化,从而浏览器可以继续使用缓存中的版本。

如果第三方库发生了更新(例如,你升级了 React 版本),生成的文件名的 Hash 值自然会发生变化。这时,浏览器会请求新的文件,而不是使用缓存中的旧版本。

这种机制可以确保在你明确升级第三方库时,浏览器会自动加载最新的版本,而不会被缓存机制阻挡。这也是为什么通过文件名 Hash 控制缓存是非常有效的方式:只有文件内容实际改变时,Hash 才会变化,而如果没有更新,文件名就保持不变,缓存继续有效。

总结

前端打包时使用 Hash 值作为静态文件名,主要是为了缓存优化、版本管理和避免缓存污染。当文件内容发生变化时,打包工具会生成不同的 Hash 值,确保文件名唯一,从而强制浏览器加载最新版本的资源,避免加载旧缓存文件引发的问题。同时,如果文件内容没有变化,文件名保持不变,浏览器可以继续使用缓存中的资源,从而减少网络请求,提升加载性能和用户体验。通过这种方式,前端应用可以高效地管理静态资源,保证用户始终访问到最新内容。

在线代码协同编辑器:https://github.com/xun082/online-edit

关于本文
作者:@Moment
原文:https://juejin.cn/post/7418133347542597651

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。