本文转载自微信公众号《微一大学前端技术》,作者朱海华。转载本文请联系微一大学前端技术公众号。HMR的后台让我们在使用WebpackDevServer后可以在开发项目中专注于Coding,因为它可以监听代码变化实现打包更新,最后通过自动刷新同步到浏览器,以便我们及时查看效果.但是DevServer从监听打包到整体通知浏览器刷新页面会带来一个比较麻烦的问题,就是无法保存应用状态。因此,Webpack为这个问题提供了一种新的解决方案。HotModuleReplacementHMRHotModuleReplacement的简单概念就是当我们修改并保存代码时,Webpack会重新打包代码并将新模块发送给浏览器,浏览器用新模块替换旧模块,从而实现该过程在不刷新浏览器的前提下更新页面。最明显的优势在于,与传统的livereload相比,HMR不会丢失应用的状态,提高开发效率。在我们开始深入理解WebpackHMR之前,我们可以先简单过一下下面的流程图HRM流程概述1597240262452-5ecbaec0-6245-4ed5-9195-59c7a38e8b24.pngWebpackCompile:watchPacklocalfilesandwritethemintomemoryBoundleServer:启动一个本地服务,提供浏览器端访问的文件HMRServer:输出热更新文件到HMRRuntimeHRMRuntime:生成文件,注入浏览器内存Bundle:构建输出文件HMR入门体验打开HMR其实非常容易,因为HMR本身就是它已经集成到Webpack中。有两种方法可以打开它。在运行webpack-dev-server命令时添加--hot参数直接开启HMR。编写配置文件的代码如下//./webpack.config.jsconstwebpack=require('webpack')module.exports={//...devServer:{//启用HMR特性。如果不支持MMR,会回退到livereloadhot:true,},plugins:[//...//HMR-dependentpluginnewwebpack.HotModuleReplacementPlugin()]}HMR的Server和ClientdevServer通过翻页的方式通知浏览器文件变化webpack-dev-server的源码在这个过程中,依赖于sockjs提供的服务端和浏览器之间的桥梁,在devServer启动时,建立一个webSocket长链接,通知浏览器webpack编译打包下的各个状态,以及同时监听compile下的done事件。当编译完成后,新打包模块的哈希值会通过发送给浏览器的sendStats方法重新编译。//webpack-dev-server/blob/master/lib/Server.jssendStats(sockets,stats,force){constshouldEmit=!force&&stats&&(!stats.errors||stats.errors.length===0)&&(!stats.warnings||stats.warnings.length===0)&&stats.assets&&stats.assets.every((asset)=>!asset.emitted);if(shouldEmit){this.sockWrite(sockets,'still-ok');return;}this.sockWrite(sockets,'hash',stats.hash);if(stats.errors.length>0){this.sockWrite(sockets,'errors',stats.errors);}elseif(stats.warnings.length>0){this.sockWrite(sockets,'warnings',stats.warnings);}else{this.sockWrite(sockets,'ok');}}客户端接收服务器消息并响应webpack-dev-server/client接收到typehash消息后暂时缓存hash值,同时在接收到typeok时在浏览器上执行reload操作。reload策略选择functionreloadApp({hotReload,hot,liveReload},{isUnloading,currentHash}){if(isUnloading||!hotReload){return;}if(hot){log.info('Apphotupdate...');consthotEmitter=require('webpack/hot/emitter');hotEmitter.emit('webpackHotUpdate',currentHash);if(typeofself!=='undefined'&&self.window){//broadcastupdatetowindowsself.postMessage(`webpackHotUpdate${currentHash}`,'*');}}//allowrefreshingthepageonlyifliveReloadis'tdisabledelseif(liveReload){letrootWindow=self;//useparentwindowforreload(incasewe'reinaniframewithnovalidsrc)constintervalId=self.setInterval(()=>{if(rootWindow.location.protocol!=='about:'){//reloadimmediatelyifprotocolisvalidapplyReload(rootWindow,intervalId);}else{rootWindow=rootWindow.parent;if(rootWindow.parent===rootWindow){//ifparentequalscurrentwindowwe'vereachedtherootwhichwouldcontinueforever,sotriggerareloadanywaysapplyReload(rootWindow,intervalId);}}});}functionapplyReload(rootWindow,intervalId){clearInterval(intervalId);log.info('Appupdated.Reloading...');rootWindow.location.reload();}通过浏览webpack-dev-server/client的源码可以看出,首先决定使用hotconfiguration哪种更新策略,刷新浏览器或代码进行热更新(HMR),如果配置了HMR,调用webpack/hot/emitter将最新的hash值发送给webpack,如果没有配置模块热更新,直接调用applyReloadlocation.reload方法刷新页面。webpack根据hash请求最新的模块代码。这一步其实是webpack中三个模块(三个文件,后面是文件路径对应的英文名)相互配合的结果。首先是webpack/hot/dev-server(以下简称dev-server)监听第三步webpack-dev-server/client发送的webpackHotUpdate消息,调用webpack/lib/HotModuleReplacement中的check方法.runtime(简称HMR运行时)检测是否有新的更新。检查过程中会用到webpack/lib/JsonpMainTemplate.runtime(简称jsonpruntime)中的hotDownloadUpdateChunk和hotDownloadManifest两个方法。第二种方法是调用AJAX向服务器请求是否有更新的文件。如果有更新的文件,会发送列表返回给浏览器,第一种方法是通过jsonp请求最新的模块代码,然后将代码返回给HMRruntime,HMRruntime会进一步处理它根据返回的新模块代码,可能是刷新页面或修改模块热更新。在这个过程中,其实就是webpack的三个模块一起执行后得到的结果。webpack/hot/dev-server监听客户端发送的webpackHotUpdate消息。("webpackHotUpdate",function(currentHash){lastHash=currentHash;if(!upToDate()&&module.hot.status()==="idle"){log("info","[HMR]Checkingforupdatesontheserver...");check();}});log("info","[HMR]WaitingforupdatesignalfromWDS...");}else{thrownewError("[HMR]HotModuleReplacementisdisabled.");[HMRruntime/check()](https://github.com/webpack/webpack/blob/v4.41.5/lib/HotModuleReplacement.runtime.js)检测是否有新的更新,检测过程会使用webpack/lib/web/JsonpMainTemplate.runtime.js中的hotDownloadUpdateChunk(通过jsonp请求新的模块代码,返回给HMRRuntime)和hotDownloadManifest(向Server发送AJAx请求,请求是否有更新文件,如果有新文件,则返回新文件给浏览器)获取更新文件列表,获取模块更新后,最新代码HMRRuntime对模块进行热更新。这是整个HMR最关键的一步,最关键的一步无非就是hotApply这个方法。由于代码太多,这里直接进入流程分析(关键代码),有兴趣的同学可以阅读源码。查找outdatedModules和outdatedDependencies,并删除过时的模块和对应的依赖//removemodulefromcachedeleteinstalledModules[moduleId];//whendisposingthereisnoneedtocalldisposehandlerdeleteoutdatedDependencies[moduleId];添加新模块modulesfor(moduleIdinappliedUpdate,ProwappliedUpdate){if(IdinappliedOmoduleUpdate,Haperlid)){modules[moduleId]=appliedUpdate[moduleId];}}至此整个模块替换流程结束,可以获取到最新的模块代码.接下来轮到业务代码知道模块发生变化了。~HMR中的大热成员HotModuleReplaceMentPlugin由于我们写的JavaScript代码是一个完全没有任何规则的模块,所以可以导出的是一个模块,一个函数,甚至只是一个字符串。对于这些完全没有规则的模块,Webpack不可能提供通用的模块替换方案来应对。因此,在这种情况下,我们要想体验完整的HMR开发流程,就需要自己手动处理。当JS模块更新时,如何将更新后的JS模块替换到页面中,其中HotModuleReplacementPlugin为我们提供了一系列关于HMR的API,其中最关键的就是hot.accept。接下来我们尝试手动处理JS模块更新,通知浏览器实现相应的局部刷新:::info目前主流的开发框架Vue和React都提供了统一的模块替换功能,所以Vue和React项目不需要targetHMR做的是人工代码处理,css文件也是由style-loader统一处理的,所以不需要额外处理,所以下面的代码处理逻辑都是基于纯原生开发的:::返回代码中来假设当前main.js文件如下//./src/main.jsimportcreateChildfrom'./child'constchild=createChild()document.body.appendChild(child)main.js是webpack打包的入口文件,在文件Child中引入module因此,当Child模块中的业务代码发生变化时,webpack必然会重新打包复用这些更新的模块。因此,我们需要在main.js中实现,来处理这些模块的更新热替换,它依赖于逻辑当HMR开启时,我们可以访问全局模块对象的热成员,它提供了一个accept方法,用于注册更新后如何处理模块。它接受两个参数,一个是需要监控的模块的路径(相对路径)。第二个参数是模块更新时如何处理。其实就是一个回调函数//main.js//监听子模块的变化module.hot.accept('./child',()=>{console.log('你好老大,子模块已经更新了~')})完成这些后,重新运行npmrunserve,同时修改子模块,你会发现控制台会输出以上控制台内容,同时,浏览器会不会自动更新,所以我们可以得出结论,当你手动更新某个模块时,自动刷新机制不会被激活。接下来我们看一下HMR中JS模块替换逻辑module.hot.accept的原理和How实现原理为什么我们只能通过调用module.hot.accept来实现热更新。查看源码可以发现实现如下//部分源码accept:function(dep,回调,errorHandler){if(dep===undefined)hot._selfAccepted=true;elseif(typeofdep==="function")hot._selfAccepted=dep;elseif(typeofdep===="object"&&dep!==null){for(vari=0;i
