webpack文件指纹中hash、chunkhash和contenthash的区别

471 阅读7分钟

webpack文件指纹中hash、chunkhash和contenthash的区别

在webpack打包中,通常哈希的种类有项目hash、chunkhash、contenthash。而哈希一般是结合CDN缓存来使用的。通过webpack构建之后,生成对应文件名自动带上对应的MD5值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服务器从源服务器上拉取对应数据,进而更新本地缓存。

1. 文件指纹

文件指纹可以由以下占位符组成:

占位符名称含义
ext资源后缀名
name文件名称
path文件的相对路径
hash每次webpack构建时生成一个唯一的hash值
chunkhash根据chunk生成hash值,来源于同一个chunk,则hash值就一样
contenthash根据内容生成hash值,文件内容相同hash值就相同

2. hash、chunkhash和contenthash的区别

hash

hash:一整个项目,一次打包,只有一个hash值

chunkhash

什么是chunk?

从入口entry出发,到它的依赖,以及依赖的依赖,依赖的依赖的依赖,等等,一直下去,所打包构成的代码块(模块的集合)叫做一个chunk,也就是说,入口文件和它的依赖的模块构成的一个代码块,被称为一个chunk。 所以,一个入口对应一个chunk,多个入口,就会产生多个chunk 所以,单入口文件,打包后chunkhash和hash值是不同的,但是效果是一样的

什么是chunkhash?

每个chunk打包后,会产生的哈希,叫做chunkhash

举个例子:

错误示例:

// webpack.config.js
module.exports = {
    entry: {
        entry1: './src/entry1.js',
        entry2: './src/entry2.js',
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.[hash:8].js' // 这样是错误的,因为hash,整个项目只有一个,但是我们这边入口有两个,打包生成的文件肯定也是两个,这两个肯定是不一样的,所以如果用hash的话,两个文件名就一样的,这是不对的,所以,可以换成'bundle.[chunkhash:8].js'
      },
}

正确示例:

// webpack.config.js
module.exports = {
    entry: {
        entry1: './src/entry1.js',
        entry2: './src/entry2.js',
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.[chunkhash:8].js'
      },
}

contenthash

这个哈希只跟内容有关系,内容不变,哈希值不变。

举个例子:

// a文件
require('./b')
console.log(1)

// b文件
console.log(2)
console.log(3)

// 那么其实内容就是下面三句代码,contenthash是对下面的内容进行哈希提取
console.log(2)
console.log(3)
console.log(1)
----------------------------------------------------------------------
// 那么其实,跟以下是一样的:
// a文件
require('./b')
console.log(3)
console.log(1)
// b文件
console.log(2)
// 那么这时候内容也是下面三句代码。contenthash也是对下面三句代码进行哈希提取
console.log(2)
console.log(3)
console.log(1)

hash、chunkhash、contenthash三个哈希应用场景举例见下文!!!

3. 各类哈希是如何生成的?

hash是如何生成的,这里指得是对某个文件生成一个hash?

let crypto = require('crypto');
let content = fs.readFileSync('a.jpg'); // 读取文件
let hash = crypto.createHash('md5').update(content).digest('hex').slice(0, 10); // 生成10位hash

webpack中的hash是如何生成的?

let crypto = require('crypto');

// 伪代码
let entry = {
    entry1: 'entry1', // 值'entry1'代表entry1这个文件,伪代码
    entry2: 'entry2', // 值'entry2'代表entry2这个文件,伪代码
}

let entry1 = ’require("./depModule1")'; // 代表entry1中引入了depModule1,伪代码
let entry2 = ’require("./depModule2")'; // 代表entry2中引入了depModule2,伪代码

let depModule1 = 'depModule1'; // 代表depModule1的内容是depModule1
let depModule2 = 'depModule2'; // 代表depModule2的内容是depModule2

let hash = crypto.createHash('md5')
			.update(entry1)
			.update(entry2)
			.update(depModule1)
			.update(depModule2)
			.digest('hex');
// 这就说明,webpack的hash是对每个依赖模块进行update得到的,只要任意依赖的模块由改变,那么hash值就会改变

chunkhash是如何生成的?

let crypto = require('crypto');

// 伪代码
let entry = {
    entry1: 'entry1', // 值'entry1'代表entry1这个文件,伪代码
    entry2: 'entry2', // 值'entry2'代表entry2这个文件,伪代码
}

let entry1 = ’require("./depModule1")'; // 代表entry1中引入了depModule1,伪代码
let entry2 = ’require("./depModule2")'; // 代表entry2中引入了depModule2,伪代码

let depModule1 = 'depModule1'; // 代表depModule1的内容是depModule1
let depModule2 = 'depModule2'; // 代表depModule2的内容是depModule2

let chunkhash_of_entry1 = crypto.createHash('md5')
                            .update(entry1)
                            .update(depModule1)
                            .digest('hex');
let chunkhash_of_entry2 = crypto.createHash('md5')
                            .update(entry2)
                            .update(depModule2)
                            .digest('hex');

// 这就说明,webpack的chunkhash是对每个入口自己的依赖模块进行update得到的,每个入口对应一个chunkhash,入口文件和依赖有所改变,那么这个入口对应的chunkhash就会改变

contenthash是如何生成的?

let crypto = require('crypto');

// 伪代码
let entry = {
    entry1: 'entry1', // 值'entry1'代表entry1这个文件,伪代码
    entry2: 'entry2', // 值'entry2'代表entry2这个文件,伪代码
}

let entry1 = ’require("./depModule1")'; // 代表entry1中引入了depModule1,伪代码
let entry2 = ’require("./depModule2")'; // 代表entry2中引入了depModule2,伪代码

let depModule1 = 'depModule1'; // 代表depModule1的内容是depModule1
let depModule2 = 'depModule2'; // 代表depModule2的内容是depModule2

let entry1File = entry1 + depModule1;
let contenthash_of_entry1 = crypto.createHash('md5')
                            .update(entry1File)
                            .digest('hex');

4. 各类哈希的区别,或,各类哈希如何选择?(面试题)

  • hash、chunkhash、contenthash,首先生成效率越来越低,成本越来越高,影响范围越来越小,精度越来越细。
  • hash是一整个项目,一次打包,只有一个hash值,是项目级的
  • chunhash是从入口entry出发,到它的依赖,以及依赖的依赖,依赖的依赖的依赖,等等,一直下去,所打包构成的代码块(模块的集合)叫做一个chunk,也就是说,入口文件和它的依赖的模块构成的一个代码块,被称为一个chunk。
  • contenthash是哈希只跟内容有关系,内容不变,哈希值不变。与chunkhash的区别可以举上面contenthash的例子,同时可以说明contenthash跟内容有关,但是chunkhash会考虑很多因素,比如模块路径、模块名称、模块大小、模块id等等。

5. 各类哈希的应用

  • 题目一:以下能否打包成功,如果不能,是什么原因?
module.exports = {
    entry: {
        main: './src/index.js',
        vendor: ['lodash']
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[hash].js'
    }
}

不能,因为这是多入口文件打包,会生成多个文件,但是由于hash是根据项目生成的,一个项目对应一个hash,所以会导致生成的文件同名,webpack不允许这么做,所以不能打包成功。

  • 题目二:以下能否打包成功,如果不能,是什么原因?
module.exports = {
    entry: {
        main: './src/index.js',
        vendor: ['lodash']
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[hash].js'
    }
}

能,因为虽然是多入口文件打包,会生成多个文件,并且即便hash一样,由于filename是根据name和hash共同决定的,name是entry的key,key不同,所以生成的文件不同,所以可以打包成功。

  • 题目三:上面的打包方式是否存在缺点,如果存在,则应该怎么优化?
module.exports = {
    entry: {
        main: './src/index.js', // 这里有引入a.css
        vendor: ['lodash']
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[chunkhash].js'
    },
    plugin: [
        new MiniCssExtractPlugin({
			filename:'css/[chunkhash].css'
		}),
    ]
}

存在缺陷。

首先,output中的chunkhash是没有问题的,问题在于plugin的css的chunkhash。

这里css应该使用contenthash。原因如下:

由于已知入口文件main引入了a.css,那么插件MiniCssExtractPlugin会将css从入口文件main中抽离出来打包成单独的css文件,由于是从入口文件main中抽离出来的,所以main的chunkhash和css的chunkhash是一样的,因为chunkhash是根入口文件和入口文件的依赖相关。

这就存在了一个问题:当我main中的js代码发生了变化,那么这个chunkhash就变了,这样css的哈希也就跟着变了,但事实上,css并没有做修改,所以不需要变化哈希值。

所以,这里的css的哈希就可以使用contenthash,根据css的内容来变化,内容变了哈希就变,内容不变哈希就不变。

6. 其他问题:

问题一:一个入口文件就只能生成一个文件吗?

不是的,main可以生成两个文件,比如main.js和main.css,css是用插件从main.js中抽离出来的

问题二:生成hash的时候,对同一个内容,多次update,结果一样吗?

不一样。.update(a).update(b)相当于.update(a+b)

let hash1 = crypto.createHash('md5').update(content).digest('hex') let hash2 = crypto.createHash('md5').update(content).update(content).digest('hex')

console.log(hash1 !== hash2) // true

let hashA = crypto.createHash('md5').update('123').digest('hex') let hashB = crypto.createHash('md5').update('1').update('2').update('3').digest('hex')

console.log(hashA === hashB) // true

7. 参考文章

文章转自13. 重学webpack--一文彻底了解hash、chunkhash和contenthash的区别_wx5c04c8a88fd20的技术博客_51CTO博客,如有侵权,请联系删除