面试官:为什么前端打包出来的静态文件名字是一串 Hash 值 ?

文摘   2024-11-04 08:49   中国香港  

(姐妹们,有时间可以帮我心上人来一刀,想让她开心一下,太难啦)

前端打包出来的静态文件名带有一串 Hash 值,主要是为了实现以下几个目的:
  1. 缓存有效性:当文件内容发生变化时,Hash 值也随之改变,这意味着浏览器能够识别文件的更新。如果文件内容没有变化,Hash 值保持不变,浏览器可以从缓存中加载文件,节省网络带宽和提高加载速度。

  2. 避免缓存问题:在开发和部署过程中,可能会遇到浏览器缓存旧的文件版本的问题。引入 Hash 值后,即使文件的路径没有变化,当文件内容更新时,Hash 值的变化会确保浏览器下载新的文件,避免了缓存所导致的问题。

  3. 版本控制:Hash 值可以作为文件的唯一标识,这样在文件名中就包含了版本信息。例如,如果你的构建工具生成了一个名为 app.abc123.js 的文件,当更新内容后会生成 app.def456.js。这使得开发团队和管理人员更容易跟踪和管理不同版本的静态资源。

  4. 构建优化:在大型项目中,打包工具(如 Webpack)会根据文件的内容生成 Hash 值,这样可以优化打包过程,促进增量编译和更高效的构建。

  5. 提高安全性:通过结合文件的内容生成 Hash 值,可以降低某些类型的攻击风险。例如,攻击者如果想上传一个恶意文件来替换合法文件,就必须知道文件的具体内容才能生成正确的 Hash,这增加了入侵的难度。

总之,为静态文件名加入 Hash 值旨在提高前端资源的管理、性能和安全性。

以下是正文:


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

接下来我们来详细讲解一下这个知识点。

启发式缓存

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

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

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

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

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

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

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

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

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

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

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

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

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

浏览器默认缓存

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

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

例子:

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

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

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

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

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

Hash 值的作用

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

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

20240925093513

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

20240925093700

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

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

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

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

第三方库如何处理

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

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

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

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

在 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



最后:
React Hook 深入浅出
CSS技巧与案例详解
vue2与vue3技巧合集
VueUse源码解读


JavaScript 每日一练
每天一道JavaScript 实战题,让大家平时多多积累实用的知识,提高开发效率,才有更多的时间摸鱼。
 最新文章