本文内容只适用于webpack v1版本,webpack v2已经修复了hash计算规则。javascript
以前讨论了webpack的hash与chunkhash的区别以及各自的应用场景,若是是常规单页面应用的话,上篇文章提供的方案是没有问题的。可是前端项目复杂多变,应对复杂多页面项目时,咱们不得不继续踩webpack的hash坑。css
在进入正文以前先解释一下所谓的常规单页面和复杂多页面是什么意思。html
这两个并不是专业术语,而是笔者实在想不出更恰当的说法了,见谅。前端
常规单页面符合如下条件:java
与主文件不关联的懒加载文件指的是逻辑与主文件彻底无关的js文件,这类文件不参与主文件打包。好比主文件main.js
中有如下代码:webpack
window.onload = function(){ var script = document.createElement('script'); script.src = '//static.daojia.com/bi.js'; document.head.appendChild(script); }
其中bi.js
的内部逻辑与main.js
没有任何关联,它对于main.js
来讲就是一个字符串而已。git
与之相对应的是与主文件有逻辑关系的模块文件,好比如下代码:github
window.onload = function(){ require.ensure([],function(require){ require('./part.a.js'); },'a'); }
其中part.a.js
是懒加载模块,以上源码经编译会生成独立的part文件,由main.js
按需加载。web
复杂多页面项目符合如下条件:算法
那么这种类型的项目复杂度在哪呢?如何应用webpack去解决hash问题?
上篇文章webpack的hash与chunkhash的区别以及各自的应用场景提到应该使用chunkhash
结合webpack-md5-plugin做为js文件hash解决方案。这种方案在应对全部模块均同步编译的场景是没有问题的,可是请你们首先考虑下文的场景。
入口文件main.app.js
的代码以下:
import '../style/main.app.scss'; import fn_d from './part.d.js'; console.log('main'); window.onload = function(){ require.ensure([],(require)=>{ require('./part.a.js'); }); }
异步模块part.a.js
代码以下:
import fn_d from './part.d.js'; console.log('part a'); setTimeout(()=>{ require.ensure([],(require)=>{ require('./part.b.js'); }); },10000);
异步模块part.b.js
代码以下:
import fn_c from './part.c.js'; import fn_d from './part.d.js'; console.log('part b');
使用webpack将以上源代码进行编译,输出如下文件:
main.app.[chunkhash].js
:主文件;part.a.[chunkhash].js
:异步模块a;part.b.[chunkhash].js
:异步模块b;main.app.[chunkhash].css
:样式文件。截止到目前是没有问题的,如今,请你们想象一下:若是咱们修改了part.a.js
源码,编译的结果文件哪些文件的hash改变了?
首先能够确定的是part.a.[chunkhash].js
的hash值会改变,那么其余文件呢?
答案是:只有part.a.[chunkhash].js
的hash改变了,其他文件的hash都与修改前一致。
那么这种结果是否合理呢?
在回答这个问题以前,咱们首先了解一下webpack runtime是如何加载异步模块的。请看如下代码:
var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.charset = 'utf-8'; script.async = true; script.src = __webpack_require__.p + "js/part/part." + ({ "1": "a", "2": "b" }[chunkId] || chunkId) + "." + { "1": "f5ea7d95", "2": "b93662b0" }[chunkId] + ".js"; head.appendChild(script);
上述代码是编译生成的main.app.[chunkhash].js
中实现懒加载的逻辑,原理就是你们熟知的动态生成<script>
标签。可是在对script.src
赋值时,webpack有如下三个概念须要知晓:
chunkId
,对应上述代码中的"1"
和"2"
;chunkName
,对应上述代码中的"a"
和"b"
;chunkHash
,对应上述代码中的"f5ea7d95"
和"b93662b0"
。
chunkId
和chunkName
暂时不用关心,咱们只须要关注chunkHash
的变更。
也就是说,part.a.[chunkhash].js
和part.b.[chunkhash].js
的hash值是写死在main.app.[chunkhash].js
中的。按照以前的编译结果,part.a.[chunkhash].js
的hash变了,可是main.app.[chunkhash].js
的hash没变,那么用户的浏览器仍然缓存着旧版本的main.app.[chunkhash].js
,此时异步加载的part.a.[chunkhash].js
仍然是旧版本的文件。这显然是不符合需求的。
总结以上所述,懒加载模块的改动经编译,主文件的hash值没有变化,影响了版本发布。
笔者在初次遇到上述问题时,第一个出如今脑海里的念头是:主文件计算hash值时没有把异步模块的内容计算在内。
结合上篇文章webpack的hash与chunkhash的区别以及各自的应用场景,webpack-md5-plugin是在chunk-hash
钩子函数中替换了chunkhash
,那么webpack在执行chunk-hash
钩子函数以前对源代码的编译进行到了哪一步?
咱们在chunk-hash
钩子函数内将各模块的信息打印出来:
compilation.plugin("chunk-hash", function(chunk, chunkHash) { console.log(chunk); });
因为打印信息太多,就不贴出来了。此时一共有5个chunk:
main.app
;part.a
;part.b
。其中html和style都是由插件导出,因此这两个chunk是不会被分配chunkId
和chunkName
的,不会影响js的编译。
而后打印一下各模块对应此时的代码。main.app.js
此时的代码以下:
require('../styles/main.app.scss'); var _partD = require('./part.d.js'); var _partD2 = _interopRequireDefault(_partD); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } console.log('main'); window.onload = function () { require.ensure(['./part.a.js'], function (require) { require('./part.a.js'); }, 'a'); };"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = function (msg) { console.log(msg); };
能够看出,main.app.js
相关的同步模块part.d.js
的内容已经被编译进了主文件(最后三行),只是url仍然未改变。而异步模块part.a.js
不只url仍然是原始的本地相对地址,并且内容也并无编译进主文件。
可是请注意,上文提到的5个chunk中包含了part.a
,也就是说part.a.js
此时已经被编译了,而且已经计算了hash值。
详细的log信息你们能够自行打印出来研究。
此时main.app.js
的chunkhash仍然是使用webpack自身计算所得,webpack默认的chunkhash计算方法是将与当前模块全部相关的所有内容做为算法参数,包括style文件。而webpack-md5-hash插件对chunk-hash
钩子进行捕获并从新计算chunkhash,它的计算方法是只计算模块自己的当前内容(包括同步模块),也就是上文的代码。这种计算方式把异步模块的内容忽略掉了,形成了本文面对的问题:异步模块的修改并未影响主文件的hash值。
既然找到了引发问题的缘由,那么相应的解决方案相信你们内心多少有点数了。
可能会有人说:我不使用webpack-md5-hash插件不就好了吗?
你们还记得上篇文章webpack的hash与chunkhash的区别以及各自的应用场景提到的webpack计算chunkhash的方法,style文件也会被计算在内,因此使用webpack自身的chunkhash计算方案确定是不可行的。
若是有研究webpack稍微深刻的同窗可能会发现:主文件使用[hash]
而不是[chunkhash]
,异步模块使用chunkhash
,同时搭配webpack-md5-hash插件使用。这种方案下,style的修改并不会影响主文件的[hash]
值。这种方案是否可行呢?
首先咱们分析一下这种方案的原理。[hash]
是compilation实例的hash值,webpack是在全部的chunkhash基础上进行计算此hash值。默认状况下,main.app.js
的chunkhash会包括style文件的内容,而webpack-md5-hash插件将style文件内容剔除,只计算js部分。因此,style文件的修改不影响最后的[hash]
值。
乍看起来,以上方案是能够解决咱们的问题的。可是你们请考虑这种场景:若是项目中存在不止一个主js文件呢?修改任意js代码会不会影响最终主文件的[hash]
值?
答案是确定的!webpack将全部js文件的内容做为计算[hash]
的参数,任何js文件的修改都会影响最终的结果。也就是说,假如我修改了主文件main.app_a.js
或者main.app_a.js
的任意(同步/异步)模块,那么main.app.js
的hash值也会改变。这显然是不符合需求的。
既然上面的两种方案都不行,到底什么才是可行的方案呢?
其实,解决问题的关键在前文中都提到了,只要打印出chunk-hash
钩子函数的chunk信息,解决方案就浮出水面了。关键点有两个:
chunk-hash
时异步模块已经被编译了,而且生成了hash值;chunks
属性,value是异步模块chunk的集合数组。咱们主文件中获取到各异步模块的hash值,而后将这些hash值与主文件的代码内容一同做为计算hash的参数,这样就能保证主文件的hash值会跟随异步模块的修改而修改。
基于以上方案,笔者站在巨人肩上,在webpack-md5-hash插件的基础上进行了简单地修改。代码以下:
compilation.plugin("chunk-hash", function(chunk, chunkHash) { var source = chunk.modules.sort(compareModules).map(getModuleSource).reduce(concatenateSource, ''); // get children chunks hashes so that child chunk impact main file's hash var child_hashes = ''; if (chunk.entry && chunk.name && chunk.chunks && chunk.chunks.length > 0) { child_hashes = getHashes(chunk.chunks); } var chunk_hash = child_hashes === '' ? md5(source) : md5(source + child_hashes); chunkHash.digest = function() { return chunk_hash; }; });
以上插件已发布webpack-split-hash
webpack的不少理念和解决方案是针对SPA项目的,多页面应用的一些问题须要一些复杂的方案去解决。hash是前端静态资源增量发布的通用手段,而webpack针对hash的解决方案是没法应对多页面项目的。本篇文章以笔者真实遇到的场景为例,记录了懒加载场景下各模块的hash解决方案。
最后打个广告,58到家前端工程集成解决方案boi已经开源。boi是对webpack的深度使用,它不是最好的前端工程解决方案,咱们在不断踩坑的路上尽可能分享webpack以及前端工程化的心得,但愿可以帮助你们少踩点坑。