从源码看webpack的hash策略

9,197 阅读7分钟

近期发现webpack在多台机器上打包同一份代码生成的hash不一样,查看社区没有文章深入说明hash生成策略 ,所以把webpack源码撸了一遍,定位到是hash生成时包含有项目的绝对路径导致,最后编写一个webpack插件解决了该问题,本文主要讲解hash的用法和原理以及如何解决多机器hash不一致等坑。

webpack的hash策略

前端同学众所周知静态资源首次被加载后浏览器会进行缓存,同一个资源在缓存未过期情况下一般不会再去请求,那么当资源有更新时如何通知浏览器资源有变化呢?资源文件命名hash化就是解决该问题而生;webpack是现在前端的主流构建工具,所以本文主要是讲述webpack构建后文件名的hash策略;webpack分为hash、chunkhash、contenthash这三种hash,下面我们依次讲述一下三种hash的使用和原理。

hash

使用webpack构建时hash是使用最多的一种,webpack构建后整个项目的js和css输出文件的hash都相同;例如一个项目有6个组件,需要把组件1、2、3作为代码块(chunk)输出一组js和css文件,组件4、5作为代码块(chunk)输出一组js和css文件,webpack如下配置:

output: {
    path: path.resolve(__dirname, OUTPUT_PATH),
    filename: '[name].[hash].js',// 使用hash
    publicPath: '/dist/webpack/'
  }

通过webpack构建完后输出的第一组js、css文件的hash相同,并且第二组和第一组的hash也相同,下图是hash在项目中的效果:

hash2

所以只要某一个文件被修改,所有输出文件的hash都会跟着变化;因此它有一个弊端,一旦修改了某一个文件,整个项目的文件缓存都会失效。

chunkhash

chunkhash相对hash影响范围比较小,使用chunkhash时,每一个代码块(chunk)输出文件对应一个hash,某源文件被修改后,只有该源文件所在代码块(chunk)的输出文件的hash会变化;例如一个项目有6个组件,需要把组件1、2、3作为代码块(chunk)输出一组js和css文件,组件4、5作为代码块(chunk)输出一组js和css文件,webpack如下配置:

output: {
    path: path.resolve(__dirname, OUTPUT_PATH),
    filename: '[name].[chunkhash].js', // 使用chunkhash
    publicPath: '/dist/webpack/'
  }

通过webpack打包构建完后输出的两组hash不同,但是每一组内部js和css的hash相同,下图是chunkhash在项目中的效果:

hash3

contenthash

当使用mini-css-extract-plugin插件时还可以使用contenthash来获取文件的hash,contenthash相对于chunkhash影响范围更小;每一个代码块(chunk)中的js和css输出文件都会独立生成一个hash,当某一个代码块(chunk)中的js源文件被修改时,只有该代码块(chunk)输出的js文件的hash会发生变化;例如一个项目有6个组件,需要把组件1、2、3作为代码块(chunk)输出一组js和css文件,组件4、5作为代码块(chunk)输出一组js和css文件,webpack如下配置:

output: {
    path: path.resolve(__dirname, OUTPUT_PATH),
    filename: '[name].[contenthash].js', // 使用contenthash
    publicPath: '/dist/webpack/'
  }

通过webpack打包构建完后输出的两组hash不同,而且每一组内部js和css的hash也不同,下图是contenthash在项目中的效果:

hash4

三种hash的区别

hash类型 区别
hash hash是根据整个项目构建,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值
chunkhash chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的代码块(chunk),生成对应的哈希值,某文件变化时只有该文件对应代码块(chunk)的hash会变化
contentHash 每一个代码块(chunk)中的js和css输出文件都会独立生成一个hash,当某一个代码块(chunk)中的js源文件被修改时,只有该代码块(chunk)输出的js文件的hash会发生变化

webpack的hash原理

webpack的hash是通过crypto加密和哈希算法实现的,webpack提供了hashDigest(在生成 hash 时使用的编码方式,默认为 'hex')、hashDigestLength(散列摘要的前缀长度,默认为 20)、hashFunction(散列算法,默认为 'md5')、hashSalt(一个可选的加盐值)等参数来实现自定义hash;下面依次讲述三种hash生成策略。

webpack的三种hash生成策略都是根据源码内容来生成,只是该源码已经被webpack封装成能在webpack环境中运行的代码了,包含每一个源文件的绝对路径;webpack会在build阶段根据源码给对应的模块(module)生成一个_buildHash(后续根据该值生成模块的hash),如下图所示可以看到源码中包含绝对路径。

hash1

webpack在seal阶段生成三种hash,最后根据output的配置决定使用哪种hash,webpack通过执行Compilation.createHash函数来生成hash。

hash和chunkhash的生成过程

下面主要讲一下hash的生成过程,其中chunkhash的生成过程包含在其中。webpack生成hash的第一步是获取Compilation下面的所有modules,把所有的module在build阶段生成的_buildHash作为内容生成一个新的hash值;然后获取到所有的代码块(chunks),分别把代码块(chunk)中包含的module的hash作为内容生成代码块(chunk)的hash,该hash就是配置chunkhash时需要使用的hash值;最后把所有代码块(chunks)的hash作为内容生成一个hash就是最终的hash,如下源码所示。

// 非源码,代码有删减
createHash() {
		// 把所有的module根据在build阶段生成_buildHash来生成一个新的hash值
		const modules = this.modules;
		for (let i = 0; i < modules.length; i++) {
			const module = modules[i];
			const moduleHash = createHash(hashFunction);
			module.updateHash(moduleHash);
		}
		// clone needed as sort below is inplace mutation
		const chunks = this.chunks.slice();
	
	// 给所有的chunks分别生成一个hash
		for (let i = 0; i < chunks.length; i++) {
			const chunk = chunks[i];
			const chunkHash = createHash(hashFunction);
			try {
				chunk.updateHash(chunkHash);
				// chunk中包含的所有module的hash作为内容生成一个hash值
				template.updateHashForChunk(
					chunkHash,
					chunk,
					this.moduleTemplates.javascript,
					this.dependencyTemplates
				);
				chunk.hash = chunkHash.digest(hashDigest);
				
				// 把所有的chunks的hash作为内容
				hash.update(chunk.hash);
        
				// 生成contentHash
				this.hooks.contentHash.call(chunk);
			} catch (err) {}
		}
		// 生成hash
		this.fullHash = hash.digest(hashDigest);
		this.hash = this.fullHash.substr(0, hashDigestLength);
	}

contenthash生成过程

contenthash生成跟前两种hash生成不一样,它是通过mini-css-extract-pluginJavascriptModulesPlugin插件生成的hash;mini-css-extract-plugin是webpack打包构建时把css类型的module单独分类出来的插件,使用该插件时会为css类型的文件单独生成hash;它会把代码块(chunk)中所有类型为css/mini-extract的module的hash作为内容生成chunkhash。

     // mini-css-extract-plugin插件的css文件hash生成的钩子函数
     
     compilation.hooks.contentHash.tap(pluginName, chunk => {
        const { outputOptions } = compilation;
        const { hashFunction, hashDigest, hashDigestLength } = outputOptions;
        const hash = createHash(hashFunction);
        // 把chunk中所有类型为`css/mini-extract`的module的hash作为内容生成hash
        for (const m of chunk.modulesIterable) {
          if (m.type === MODULE_TYPE) {
            m.updateHash(hash);
          }
        }
        const { contentHash } = chunk;
        // 把生成的内容放入chunk对象的contentHash中
        contentHash[MODULE_TYPE] = hash.digest(hashDigest).substring(0, hashDigestLength);
      });

contentHash钩子触发时会调用JavascriptModulesPlugin插件注册的contentHash事件,把代码块(chunk)中所有类型为函数的module的hash作为内容生成hash。

// JavascriptModulesPlugin插件为js生成contentHash的钩子函数

compilation.hooks.contentHash.tap("JavascriptModulesPlugin", chunk => {
				// ...此处有删减
					for (const m of chunk.modulesIterable) {
						if (typeof m.source === "function") {
							hash.update(m.hash);
						}
					}
					chunk.contentHash.javascript = hash
						.digest(hashDigest)
						.substr(0, hashDigestLength);
				});

多机器构建方案

webpack的hash虽然给我们带来了极大的方便,但是也存在一些弊端;webpack的三种hash策略都依赖module的_buildHash,而_buildHash值又依赖module的源文件内容和绝对路径,所以同一份源码在不同的机器上构建出来的hash值不一定一样,除非两台机器上的项目路径完全相同;若线上存在多机器构建部署同一个项目时,可能hash值不同而导致访问js或者css时出现404现像。

若想多机器部署hash一样,下面是解决多机器构建生成hash的策略:

  • 项目中的js源文件(排除node_modules)hash生成使用的是非webpack封装后的源码,而是使用js源文件内容,解决封装后源码有绝对路径而导致hash不一致问题。
  • 项目中的css源文件(排除node_modules)hash生成使用的css源码,跟以前一样不存在路径问题。
  • node_modules下面的css的hash生成使用该css文件的相对路径加上该npm包版本号,解决node_modules中的样式文件存在sourceMap而导致路径问题。
  • node_modules下面的js的hash生成使用该js文件的相对路径加上该npm包版本号,解决node_modules中某些npm包生成的hash不一致问题。