前端打包出来的静态文件名带有一串 Hash 值,主要是为了实现以下几个目的:
缓存有效性:当文件内容发生变化时,Hash 值也随之改变,这意味着浏览器能够识别文件的更新。如果文件内容没有变化,Hash 值保持不变,浏览器可以从缓存中加载文件,节省网络带宽和提高加载速度。
避免缓存问题:在开发和部署过程中,可能会遇到浏览器缓存旧的文件版本的问题。引入 Hash 值后,即使文件的路径没有变化,当文件内容更新时,Hash 值的变化会确保浏览器下载新的文件,避免了缓存所导致的问题。
版本控制:Hash 值可以作为文件的唯一标识,这样在文件名中就包含了版本信息。例如,如果你的构建工具生成了一个名为
app.abc123.js
的文件,当更新内容后会生成app.def456.js
。这使得开发团队和管理人员更容易跟踪和管理不同版本的静态资源。构建优化:在大型项目中,打包工具(如 Webpack)会根据文件的内容生成 Hash 值,这样可以优化打包过程,促进增量编译和更高效的构建。
提高安全性:通过结合文件的内容生成 Hash 值,可以降低某些类型的攻击风险。例如,攻击者如果想上传一个恶意文件来替换合法文件,就必须知道文件的具体内容才能生成正确的 Hash,这增加了入侵的难度。
总之,为静态文件名加入 Hash 值旨在提高前端资源的管理、性能和安全性。
以下是正文:
前端打包后,静态文件的名字被改成一串 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://juejin.cn/post/7418133347542597651
作者:Momen
欢迎长按图片添加 poetry 为好友
,我会第一时间和你分享前端行业趋势,学习途径,成长内容等等。2024年陪你一起度过!点击领取精心为您整理的前端面试资料
如果觉得这篇内容对你有帮助,我想请你帮我2小忙:
点赞和在看就是最大的支持❤️