前端打包后,静态文件的名字被改成一串 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 值,确保文件名唯一,从而强制浏览器加载最新版本的资源,避免加载旧缓存文件引发的问题。同时,如果文件内容没有变化,文件名保持不变,浏览器可以继续使用缓存中的资源,从而减少网络请求,提升加载性能和用户体验。通过这种方式,前端应用可以高效地管理静态资源,保证用户始终访问到最新内容。