本文内容只适用于webpack v1版本,webpack v2已经修复了hash计算规则。

之前讨论了webpack的hash与chunkhash的区别以及各自的应用场景,如果是常规单页面应用的话,上篇文章提供的方案是没有问题的。但是前端项目复杂多变,应对复杂多页面项目时,我们不得不继续踩webpack的hash坑。

在进入正文之前先解释一下所谓的常规单页面复杂多页面是什么意思。

这两个并非专业术语,而是笔者实在想不出更恰当的说法了,见谅。

1. 项目类型

1.1 常规单页面项目

常规单页面符合以下条件:

  • 可以存在多个主js文件和css文件;
  • 每个js文件都是同步打包的,也就是说不存在与主文件相关联的懒加载文件

与主文件不关联的懒加载文件指的是逻辑与主文件完全无关的js文件,这类文件不参与主文件打包。比如主文件main.js中有以下代码:

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来说就是一个字符串而已。

与之相对应的是与主文件有逻辑关系的模块文件,比如以下代码:

window.onload = function(){
require.ensure([],function(require){
require('./part.a.js');
},'a');
}

其中part.a.js是懒加载模块,以上源码经编译会生成独立的part文件,由main.js按需加载。

1.2 复杂多页面项目

复杂多页面项目符合以下条件:

  • 存在与主文件相关联的懒加载模块文件;
  • 存在多个主js文件。

那么这种类型的项目复杂度在哪呢?如何应用webpack去解决hash问题?

2. 懒加载的hash解决方案

上篇文章webpack的hash与chunkhash的区别以及各自的应用场景提到应该使用chunkhash结合webpack-md5-plugin作为js文件hash解决方案。这种方案在应对所有模块均同步编译的场景是没有问题的,但是请大家首先考虑下文的场景。

2.1 应用场景

入口文件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"

chunkIdchunkName暂时不用关心,我们只需要关注chunkHash的变动。

也就是说,part.a.[chunkhash].jspart.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值没有变化,影响了版本发布

2.2 引起问题的原因

笔者在初次遇到上述问题时,第一个出现在脑海里的念头是:主文件计算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:

  • css;
  • html;
  • main.app
  • part.a
  • part.b

其中html和style都是由插件导出,所以这两个chunk是不会被分配chunkIdchunkName的,不会影响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值。

2.3 解决方案

既然找到了引起问题的原因,那么相应的解决方案相信大家心里多少有点数了。

可能会有人说:我不使用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信息,解决方案就浮出水面了。关键点有两个:

  1. chunk-hash时异步模块已经被编译了,并且生成了hash值;
  2. 主文件有个数组类型的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

3. 总结

webpack的很多理念和解决方案是针对SPA项目的,多页面应用的一些问题需要一些复杂的方案去解决。hash是前端静态资源增量发布的通用手段,而webpack针对hash的解决方案是无法应对多页面项目的。本篇文章以笔者真实遇到的场景为例,记录了懒加载场景下各模块的hash解决方案。

最后打个广告,58到家前端工程集成解决方案boi已经开源。boi是对webpack的深度使用,它不是最好的前端工程解决方案,我们在不断踩坑的路上尽量分享webpack以及前端工程化的心得,希望能够帮助大家少踩点坑。

webpack多页面开发与懒加载hash解决方案的更多相关文章

  1. Android中ViewPager+Fragment取消(禁止)预加载延迟加载(懒加载)问题解决方案

    转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/53205878本文出自[DylanAndroid的博客] Android中Vie ...

  2. Webpack探索【16】--- 懒加载构建原理详解(模块如何被组建&如何加载)&源码解读

    本文主要说明Webpack懒加载构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack懒加载构建原理. 本文使用的 ...

  3. iOS开发中懒加载的使用和限制

    1.在开发过程中很多时候,很多控件和对象需要alloc为了,提高开发效率使得懒加载得以产生. 2.下边用代码解释: - (NSMutableArray *)newsArr{ if (!_newsArr ...

  4. wow.js让css3动画变动更有趣(滚动页面动画模拟懒加载特效)

    CSS3的出现给网站页面增加了活力,网站增色不少,有这么小小的一款插件就能做出很多动画效果. 最重要的是它:简单易用.轻量级.无需 jQuery......他就是wow.js 地址:https://d ...

  5. [js开源组件开发]图片懒加载lazyload

    图片懒加载lazyload 前端对请求的一种优化方式,为什么叫懒加载,无从查起,反正我当初一直认为它是滚动加载的一种类型.它主要是以图片或背景在可视区域内时才显示真正的图片,减少src带来的负荷.所以 ...

  6. Webpack探索【10】--- 懒加载详解

    本文主要讲懒加载方面相关内容.

  7. Spring Boot JPA Entity Jackson序列化触发懒加载的解决方案

    Spring Jpa这项技术在Spring 开发中经常用到. 今天在做项目用到了Entity的关联懒加载,但是在返回Json的时候,不管关联数据有没有被加载,都会触发数据序列化,而如果关联关系没有被加 ...

  8. Hibernate @OneToOne懒加载实现解决方案

    在hibernate注解(三)中,我提高过一对一(@OneToOne)懒加载失效的问题.虽然给出了解决方法,但并没有给出完整的解决方案.今天我专门针对该问题进行讨论.至于懒加载失效的原因,在之前的文章 ...

  9. webpack散记---代码分割 和 懒加载

    webpack methods ES 2015 Loader spec (1)webpack methods方法 require.ensure //可以动态加载依赖 []:dependencies / ...

随机推荐

  1. 多层嵌套对象无法使用gson反序列化的问题The JsonDeserializer com.google.gson.DefaultTypeAdapters$CollectionTypeAdapter@3bf13cde failed to de

    http://www.oschina.net/question/1256646_123691 http://bbs.csdn.net/topics/360005140 http://stackover ...

  2. 转:Directshow开发的一些例子

    DirectShow Filter 开发典型例子分析 --字幕叠加 (FilterTitleOverlay)1 本文分析一下<DirectShow开发指南>中的一个典型的Transform ...

  3. Google Interview University - 坚持完成这套学习手册,你就可以去 Google 面试了

    作者:Glowin链接:https://zhuanlan.zhihu.com/p/22881223来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 原文地址:Google ...

  4. 文件系统管理 之 Linux 创建文件系统及挂载文件系统流程详解

    阅读此文,必须具备知识点:<Linux 查看磁盘分区.文件系统.使用情况的命令和相关工具介绍><实例解说 fdisk 使用方法><合理规划您的硬盘分区><Fe ...

  5. wordpress图片水印插件DX-Watermark

    DX-Watermark是一款功能齐全的wordpress图片水印插件,可以自动给上传的图片添加文本或者图片水印. 后台截图: 文本水印: 图片水印: 选项说明: 类型:可选择文本或图片水印两种类型, ...

  6. Fixed error when submitting assignments in Machine Learning on Coursera

    Environment: OS: OSX 10.8.5 Matlab: R2013a(8.1.0.604) 64bit   How to fix: In file submit.m, line 129 ...

  7. Servlet3.0学习总结——基于Servlet3.0的文件上传

    Servlet3.0学习总结(三)——基于Servlet3.0的文件上传 在Servlet2.5中,我们要实现文件上传功能时,一般都需要借助第三方开源组件,例如Apache的commons-fileu ...

  8. SQL语句调优 - 索引上的数据检索方法

    如果一张表上没有聚集索引,数据将会随机的顺序存放在表里.以dbo.SalesOrderDetail_TEST为例子.它的上面没有聚集索引,只有一个在SalesOrderID上的非聚集索引.所以表格的每 ...

  9. PHP操作MongoDB学习(转)

    1  mongodb启动时,设置启动项 C:\>mongodb\bin\mongod --config C:\mongodb.conf 其中mongodb.conf为:    dbpath = ...

  10. UDP"打洞"原理

    1. NAT分类 根据Stun协议(RFC3489),NAT大致分为下面四类 1) Full Cone 这种NAT内部的机器A连接过外网机器C后,NAT会打开一个端口.然后外网的任何发到这个打开的端口 ...