文件指纹是什么?怎么用?

发布时间 2023-11-17 17:29:55作者: 柯基与佩奇

Webpack 中的静态资源文件指纹

在 webpack 中如何给静态资源加 hash 值:每次构建过程都会生成一个新的 hash,所以一般用于做版本控制;chunkhash 是基于内容生成的,但是 webpack 把所有类型的文件都以 js 为汇聚点打成一个 bundle,改了 css 也会导致整个 js 的 hash 发生改变,所以最好通过 ExtractTextWebpackPlugin 把 css 独立抽取出来;chunkhash 只能用于动态导入的 chunk,这样每次 build,入口静态导入的文件还是会生成新的 hash,所以还需要 webpack-md5-hash 插件来完善该功能。

1.如何添加文件名添加指纹

文件的 hash 指纹是前端静态资源实现增量更新最常用的方案。

在 Webpack 编译输出文件名(output.filename)的配置中,对于单个入口起点,filename 会是一个静态名称。

当通过多个入口起点(entry point)、代码拆分(code splitting)或各种插件(plugin)创建多个 bundle,提供了多种模板字符串替换方式,来赋予每个 bundle 一个唯一的名称。

如果需要为文件加入 hash 指纹,Webpack 提供了两个模板字符串可供使用:hash 和 chunkhash。

代码示例如下:

// 使用每次构建过程中,唯一的 hash 生成
filename: "[name].[hash].bundle.js";
// 使用基于每个 chunk 内容的 hash:
filename: "[chunkhash].bundle.js";

2. hash 和 chunkhash

hash

在文档中找到了几个处关于 hash 的定义:

  • Using the unique hash generated for every build;
  • [hash] in this parameter will be replaced with an hash of the compilation
  • The hash of the module identifier

翻译过来就是每个构建过程生成的唯一 hash。

chunkhash

chunkhash 的解释是:Using hashes based on each chunks' content; 翻译过来就是基于每个 chunk 的内容而生成的 hash。

3. chunk

chunk 在 Webpack 中的含义,简单讲就是模块。

  • chunk 的解释还是在[1.0 的文档]chunk中能更深刻的理解。
  • output.chunkFilename决定了非入口(non-entry) chunk 文件的名称。 从这句话中也可以看出来 chunk 就是模块;只不过模块又分入口 chunk 文件和按需动态加载的 chunk。

4.compilation 和 compiler

Webpack 官方文档中How to write a plugin章节有对 compilation 的详解。

compilation 对象代表某个版本的资源对应的编译进程。当使用 Webpack 的 development 中间件时,每次检测到项目文件有改动就会创建一个 compilation,进而能够针对改动生产全新的编译文件。compilation 对象包含当前模块资源、待编译文件、有改动的文件和监听依赖的所有信息。

与 compilation 对应的有个 compiler 对象,通过对比,可以帮助对 compilation 有更深入的理解。

compiler 对象代表的是配置完备的 Webpack 环境。 compiler 对象只在 Webpack 启动时构建一次,由 Webpack 组合所有的配置项构建生成。

简单的讲,compiler 对象代表的是不变的 webpack 环境,是针对 webpack 的;而 compilation 对象针对的是随时可变的项目文件,只要文件有改动,compilation 就会被重新创建。

5. hash 的问题

理解了 compilation 之后,再回头看 hash 的定义:

[hash] is replaced by the hash of the compilation.

compilation 在项目中任何一个文件改动后就会被重新创建,然后 webpack 计算新的 compilation 的 hash 值,这个 hash 值便是 hash。

entry: {
  index: './src/index.js',
  print: './src/print.js'
},
output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].[hash].js',
  publicPath: '/'
},

第一次 build 的结果:

所有的文件名都会使用相同的 hash 指纹

第二次 build 的结果

2 个 js 文件任何一个改动都会影响另外 1 个文件的最终文件名。上线后,另外 1 个文件的浏览器缓存也全部失效。这肯定不是想要的结果。

那么如何避免这个问题呢?答案就是 chunkhash!

6. chunkhash 怎么使用

output.filename不会影响那些「按需加载 chunk」的输出文件。对于这些文件,需要使用 output.chunkFilename选项来控制输出。

根据 chunkhash 的定义知道,chunkhash 是根据具体模块文件的内容计算所得的 hash 值,所以某个文件的改动只会影响它本身的 hash 指纹,不会影响其他文件。

配置 webpack 的 output 如下:

// webpack.config.js配置
output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name][hash].js',
  chunkFilename: '[name][chunkhash].js',
  publicPath: '/'
}
// index.js动态引入chunk
import(/* webpackChunkName: "lodash" */ "lodash")
  .then(function (_) {
    console.log(_);
  })
  .catch();

build 的结果:

每个文件的 hash 指纹都不相同,上线后无改动的文件不会失去缓存。

7.hash 应用场景

接上文所述,webpack 的 hash 字段是根据每次编译 compilation 的内容计算所得,也可以理解为项目总体文件的 hash 值,而不是针对每个具体文件的。

webpack 针对 compilation 提供了两个 hash 相关的生命周期钩子:before-hash 和 after-hash。源码如下:

this.applyPlugins("before-hash");
this.createHash();
this.applyPlugins("after-hash");

hash 可以作为版本控制的一环,将其作为编译输出文件夹的名称统一管理,如下:

output: {
  filename: "/dest/[hash]/[name].js";
}

8.chunkhash 的问题

webpack 的理念是一切都是模块:把所有类型的文件都以 js 为汇聚点,不支持 js 文件以外的文件为编译入口。所以如果要编译 style 文件,唯一的办法是在 js 文件中引入 style 文件。如下

import "./style.css";

webpack 默认将 js、image、css 文件统统编译到一个 js 文件中,

这样的模式下有个很严重的问题,当希望将 css 单独编译输出并且打上 hash 指纹,按照前文所述的使用 chunkhash 配置输出文件名时,编译的结果是 js 和 css 文件的 hash 指纹完全相同。

可以借助extract-text-webpack-plugin将 style 文件单独编译输出。从这点可以看出,webpack 将 css 文件视为 js 的一部分。

new ExtractTextPlugin("[name].[chunkhash].css");

但是不论是单独修改了 js 代码还是 css 代码,编译输出的 js/css 文件都会打上全新的相同的 hash 指纹。这种状况下无法有效的进行版本管理和部署上线。

好在 extract-text-webpack-plugin 提供了另外一种 hash 值:contenthash。顾名思义,contenthash 代表的是文本文件内容的 hash 值,也就是只有 style 文件的 hash 值。这个 hash 值就是解决上述问题的银弹。修改配置如下:

new ExtractTextPlugin("[name].[contenthash].css");

编译输出的 js 和 css 文件将会有其独立的 hash 指纹。

再考虑一下这个问题:如果只修改了 style 文件,未修改 index.js 文件,那么编译输出的 js 文件的 hash 指纹会改变吗?

答案是肯定的。

修改了 style 编译输出的 css 文件 hash 指纹理所当然要更新,但是并未修改 index.js,可是 js 文件的 hash 指纹也更新了。这是因为上文提到的:

webpack 计算 chunkhash 时,以 index.js 文件为编译入口,整个 chunk 的内容会将 style.css 的内容也计算在内。

9.chunk-hash

webpack 计算 chunkhash 时,以 index.js 文件为编译入口,整个 chunk 的内容会将 style.css 的内容也计算在内:

body{ color: #000; } alert('I am main.js');

chunk-hash 并不是 webpack 中另一种 hash 值,而是 compilation 执行生命周期中的一个钩子。

chunk-hash 钩子代表的是哪个阶段呢?请看 webpack 的 Compilation.js源码中以下部分: 代码

for (let i = 0; i < chunks.length; i++) {
  const chunk = chunks[i];
  const chunkHash = crypto.createHash(hashFunction);
  if (outputOptions.hashSalt) chunkHash.update(outputOptions.hashSalt);
  chunk.updateHash(chunkHash);
  if (chunk.hasRuntime()) {
    this.mainTemplate.updateHashForChunk(chunkHash, chunk);
  } else {
    this.chunkTemplate.updateHashForChunk(chunkHash, chunk);
  }
  this.applyPlugins2("chunk-hash", chunk, chunkHash);
  chunk.hash = chunkHash.digest(hashDigest);
  hash.update(chunk.hash);
  chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
}
this.fullHash = hash.digest(hashDigest);
this.hash = this.fullHash.substr(0, hashDigestLength);

webpack 使用 NodeJS 内置的 crypto 模块计算 chunkhash,具体使用哪种算法与讨论的内容无关,只需要关注上述代码中 this.applyPlugins("chunk-hash", chunk, chunkHash);的执行时机。

chunk-hash 是在 chunhash 计算完毕之后执行的,这就意味着如果在 chunk-hash 钩子中可以用新的 chunkhash 替换已存在的值。如下伪代码:

compilation.plugin("chunk-hash", function (chunk, chunkHash) {
  var new_hash = md5(chunk);
  chunkHash.digest = function () {
    return new_hash;
  };
});

webpack 之所以如果流行的原因之一就是拥有庞大的社区和不计其数的开发者们,实际上,遇到的问题已经有先驱者帮解决了。插件webpack-md5-hash便是上述伪代码的具体实现,需要做的只是将这个插件加入到 webpack 的配置中:

// webpack.config.js

var WebpackMd5Hash = require("webpack-md5-hash");

module.exports = {
  // ...
  output: {
    //...
    chunkFilename: "[chunkhash].[id].chunk.js",
  },
  plugins: [new WebpackMd5Hash()],
};