從源碼看webpack的hash策略

近期發現webpack在多臺機器上打包同一份代碼生成的hash不同,查看社區沒有文章深刻說明hash生成策略 ,因此把webpack源碼擼了一遍,定位到是hash生成時包含有項目的絕對路徑致使,最後編寫一個webpack插件解決了該問題,本文主要講解hash的用法和原理以及如何解決多機器hash不一致等坑。javascript

webpack的hash策略

前端同窗衆所周知靜態資源首次被加載後瀏覽器會進行緩存,同一個資源在緩存未過時狀況下通常不會再去請求,那麼當資源有更新時如何通知瀏覽器資源有變化呢?資源文件命名hash化就是解決該問題而生;webpack是如今前端的主流構建工具,因此本文主要是講述webpack構建後文件名的hash策略;webpack分爲hash、chunkhash、contenthash這三種hash,下面咱們依次講述一下三種hash的使用和原理。css

hash

使用webpack構建時hash是使用最多的一種,webpack構建後整個項目的js和css輸出文件的hash都相同;例如一個項目有6個組件,須要把組件一、二、3做爲代碼塊(chunk)輸出一組js和css文件,組件四、5做爲代碼塊(chunk)輸出一組js和css文件,webpack以下配置:html

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

經過webpack構建完後輸出的第一組js、css文件的hash相同,而且第二組和第一組的hash也相同,下圖是hash在項目中的效果:前端

hash2

因此只要某一個文件被修改,全部輸出文件的hash都會跟着變化;所以它有一個弊端,一旦修改了某一個文件,整個項目的文件緩存都會失效。java

chunkhash

chunkhash相對hash影響範圍比較小,使用chunkhash時,每個代碼塊(chunk)輸出文件對應一個hash,某源文件被修改後,只有該源文件所在代碼塊(chunk)的輸出文件的hash會變化;例如一個項目有6個組件,須要把組件一、二、3做爲代碼塊(chunk)輸出一組js和css文件,組件四、5做爲代碼塊(chunk)輸出一組js和css文件,webpack以下配置:node

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

經過webpack打包構建完後輸出的兩組hash不一樣,可是每一組內部js和css的hash相同,下圖是chunkhash在項目中的效果: webpack

hash3

contenthash

當使用mini-css-extract-plugin插件時還可使用contenthash來獲取文件的hash,contenthash相對於chunkhash影響範圍更小;每個代碼塊(chunk)中的js和css輸出文件都會獨立生成一個hash,當某一個代碼塊(chunk)中的js源文件被修改時,只有該代碼塊(chunk)輸出的js文件的hash會發生變化;例如一個項目有6個組件,須要把組件一、二、3做爲代碼塊(chunk)輸出一組js和css文件,組件四、5做爲代碼塊(chunk)輸出一組js和css文件,webpack以下配置:web

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生成策略。npm

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不一致問題。
相關文章
相關標籤/搜索