13. 重学webpack——一文彻底了解hash、chunkhash和contenthash的区别

推荐:《webpack学完这些就够了》

《webpack学完这些就够了》该专题主要讲述webpack4.x从入门到成仙的学习笔记,配置和原理应有尽有。现在由于webpack5.0的诞生,打算从现在开始从0开始写一遍webpack5.0的学习笔记,与大家一起分享学习。

以下是本节正文:

一文彻底了解hash、chunkhash和contenthash的区别

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

1. 文件指纹

文件指纹指的是打包后输出的文件名和后缀

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

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

2. hash、chunkhash和contenthash的区别

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:
  • 什么是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. 各类哈希的应用

  • 题目一:以下能否打包成功,如果不能,是什么原因?

    • 答案:

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

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

    • 答案:

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

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

    • 答案:

      存在缺陷。

      首先,为什么我们需要使用哈希,主要是为了使用缓存,哈希不变,采取缓存的文件,哈希变了,采取新的文件,这样能够大大提高请求效率。

      但是由于上面的配置是多入口文件打包,文件名使用hash的话,会导致其中一个入口文件有变化,项目的hash值就会变化,这样就使得没有变化的入口的hash也变了。那么没有变化的文件就无法读取缓存了。

      所以,在这里使用hash是不合适的,可以采取chunkhash,chunkhash是根据入口文件和入口文件的依赖生成的,如果main发生变化,那么main对应的chunkhash会变化,而vendor不变的话,vendor对应的chunkhash是不变的,这样对于不变的文件依旧可以读取缓存。

  • 题目四:已知在./src/index.jsimport './a.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的内容来变化,内容变了哈希就变,内容不变哈希就不变。

    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'
    		}),
        ]
    }
    

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
    
  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值