使用webpack打包,不免会使用Hot Module Replacement功能,该功能可以实现修改、添加或删除前端页面中的模块代码,并且是在页面不刷新的前提下。它到底是怎么运做的呢?本文主要从调试工具、配置文件、官方文档三个方面进行解析。javascript
首先从chrome的调试工具network中看看,代码改变的时候,页面与后端之间发生了什么?html
咱们看到除了加载页面所依赖的文件外,多了一个链接,这是一个Server-sent Events,相关的介绍能够参考这篇文章,并且每隔一段时间都会向发送一次数据。数据内容主要是前端
action:sync操做;vue
hash:f397e485c539fd7a10fb,是bundle的hash,由于和产出文件collections.f397e485c539fd7a10fbjs的内容hash值相同;java
modules:产出bundle中的module id和对应的文件地址。webpack
而后修改一处代码,webpack自动编译后,发现network中发生了几处变化,首先是客户端收到后端发出的事件git
action:built操做,通知浏览器webpack从新发起了编译;github
hash:最新产出bundle的内容hash值为debc36315df6764f157c;web
modules:bundle中的模块id和对应模块的文件地址。chrome
另外前端对后端发起了两个请求,请求了f397e485c539fd7a10fb.hot-update.json和0.f397e485c539fd7a10fb.hot-update.js两个文件,文件的hash值正好是未发生修改以前后端发送前端的bundle hash值。
咱们查看一下两个文件的内容。
json文件的内容:
h:debc36315df6764f157c,bundle内容的最新hash值;
c:"0": true, 表示bundle id为0的文件被修改了;
js文件的内容:
内容是一个函数,相似jsonp的返回形式,也就是页面收到请求后执行了webpackHotUpdate函数,对bundle id为0的文件中的moudle id为50的模块进行修改。
跟进到这里,咱们能够推测出这个交互过程:
(0)webpack首次编译时将如何更新更新模块(update-method)和接收后端推进事件(event-source)的代码打包到bundle之中;
(1)webpack进入watch 模式,在项目代码发生变化的时候从新编译;
(2)将编译产出存放在dev-server,此处的编译只针对变更的模块,产出应该包含上文中提到的oldbundlehash.hot-update.json和oldbundlehash.hot-update.js文件;
(3)dev-server中使用hot-middleware中间件向前端发送built事件;
(4)前端收到通知后,向后端请求最新的变更文件,请求到的js文件经过script标签加载后执行,其实就是执行已经预埋到bundle中的函数(update-method),从而修改bundle文件。
接下来咱们从项目的配置文件来验证一下,配置文件主要参考vue-cli中的webapck项目。
涉及到Hot Module Replacement的地方主要有两处:
entry的配置:在每一个入口bundle开头引入了event-source,即在页面中接收后端发送的事件
/*********./build/webpack.dev.conf.js********/
// 将event-source相关代码,添加到每一个入口chunk中,做为HRM Runtime的一部分。 // 后端相应的配置见dev-server的hotMiddleware部分 Object.keys(baseWebpackConfig.entry).forEach(function (name) { baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) })
/*********./build/dev-client.js********/ // Event-Source对象用于接收服务器端推送事件 // eventsource-polyfill用于扩展Event-Source对象在IE浏览器下的兼容性 require('eventsource-polyfill') var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') // 主要用于接受后端hotMiddleware的通知,执行reload操做 hotClient.subscribe(function (event) { if (event.action === 'reload') { window.location.reload() } })
插件的配置:引入HotModuleReplacementPlugin插件,将update-method的代码打入bundle
plugins: [ ... // HMR插件将HMR Runtime代码嵌入到bundle中,可以操做APP代码,完成代码替换 new webpack.HotModuleReplacementPlugin(), // 报错提示插件:报错不阻塞,可是编译后给出提示 new webpack.NoEmitOnErrorsPlugin(), new FriendlyErrorsPlugin() ]
涉及到Hot Module Replacement的地方主要有两处:
将compiler挂载在devMiddleware上:对编译产出提供静态文件服务
// 将compiler挂载在dev-server上,监听本地代码变化,变化则启动编译并将编译后的文件暂存到内存中 var devMiddleware = require('webpack-dev-middleware')(compiler, { publicPath: config.dev.assetsPublicPath === './' ? '' : config.dev.assetsPublicPath, quiet: true })
将compiler挂载在hotMiddleware上:通知前端event-source对象发生了rebuilt
// 编译后发送通知到HRM Runtime,HRM Runtime收到update通知后,下载更新的模块,通知APP更新,APP收到通知,而后要求HRM Runtime执行模块替换 var hotMiddleware = require('webpack-hot-middleware')(compiler, { log: () => {} })
由配置文件能够基本验证以前经过network debug获得的推论,接下来去看一下官方文档验证一下。
官方文档先是整体介绍了一下 Hot Module Replacement的基本原理,而后将原理中涉及到几个知识点进行了介绍。
webapck在编译的过程当中,将HMR Runtime嵌入到bundle中;编译结束后,webpack对项目代码文件进行监视,发现文件变更从新编译变更的模块,同时通知HMR Runtime,而后HMR Runtime加载变更的模块文件,尝试执行热更新操做。更新的逻辑是:先检查模块是否能支持accept方法,不支持的话,则冒泡查找模块树的父节点,直到入口模块,accept方法也就是模块hot-replace的handler。
(1)compiler
这里的compiler也就是指webapck,主要提供update的信息,也就是update menifest(json文件格式)和update chunks(js文件格式);
(2)app
app也就是指前端页面,app中的代码主要调用HMR Runtime下载最新的模块代码,而后调用HMR Runtime执行update操做;
(3)HMR Runtime
HMR Runtime是webapck内嵌到前端页面的代码,主要提供来能给个职能check和apply。check用来下载最新模块代码,runtime可以接收后端发送的事件和发送请求;apply用于更新模块,主要将要更新的模块打上tag,而后调用模块的(也有多是父模块)的更新handler执行更新。
(4)module
HRM是一个可插拔的工具,只能影响包含HMR code的模块。一般状况下,没有必要为每一个模块写入HMR code,更新的时候会进行冒泡检查HMR code的是否存在。
根据官方文档的介绍,基本和咱们的推论吻合,区别在于官方文档引入了HMR Runtime的概念,这个能够看做是推论中的event-source和update-method的结合体。
如今你们应该清楚了 webpack的Hot Module Replacement的基本原理了,官方文档中提到了如何根据最新的模块替换旧模块的方法,这个会放到下一篇文章中进行介绍。