1.什么是HMR?网页开发经验。HMR最初是由Webpack设计和实现的,几乎已经成为现代工程工具必备的功能之一。1.1在HMR之前在HMR之前,应用程序的加载和更新是页面级别的原子操作。即使只有一个代码文件发生变化,也需要刷新整个页面才能将最新的代码映射到浏览器,这样就会丢失之前在页面上的执行。所有交互和状态,例如:对于复杂的表单场景,这意味着您可能需要重新填写很多字段。备注会需要重新加载整个页面,影响开发体验。引入HMR后,虽然不能覆盖所有场景,但大部分的小改动都可以实时热更新到页面,从而保证了持续流畅的开发调试体验,大大提高了开发效率。1.2使用HMRWebpack生态,只需要经过简单的配置就可以启动HMR功能,大致分为两步:配置devServer.hot属性为true,如://webpack.config.jsmodule.exports={//...devServer:{//devServer.hot=true必须设置才能启动HMR功能hot:true}};之后还需要调用module.hot.accept接口来声明如何安全的将模块替换为最新的代码,如:importcomponentfrom"./component";letdemoComponent=component();document.body.appendChild(demoComponent);//HMRinterfaceif(module.hot){//Capturehotupdatemodule.hot.accept("./component",()=>{constnextComponent=component();//用hotloadedonedocument.body.replaceChild(nextComponent,demoComponent);替换旧内容demoComponent=nextComponent;});}模块代码的替换逻辑可能非常复杂。幸运的是,我们通常不需要过多关注这一点,因为很多WebpackLoader已经提供了针对不同资源的HMR功能,例如:style-loader内置Css模块热更新vue-loader内置Vue模块热updatereact-hot-reload内置React模块热更新接口因此,从使用的角度来说,只需要针对不同的资源配置对应的支持HMR的Loader,即可轻松上手。2.实现原理WebpackHMRfeature的原理并不复杂,核心流程:使用webpack-dev-server(以下简称WDS)托管静态资源,同时以Runtime方式注入HMR客户端代码。浏览器加载页面后,建立WebSocket与WDS连接Webpack后监听文件变化,增量构建变化的模块,通过WebSocket发送hash事件。浏览器收到hash事件后,请求manifest资源文件确认增量变化范围。浏览器加载变更后的增量模块Webpack运行时触发变更模块的module.hot.accept回调,执行代码变更逻辑。done接下来我会展开HMR的核心源码,详细讲解Webpack5中模块热替换原理的关键部分,内容比较晦涩,不感兴趣的同学可以直接跳到下一章。2.1注入HMRclientruntime执行npxwebpackserve命令后,WDS调用HotModuleReplacementPlugin插件向应用的主Chunk中注入一系列HMRRuntime,包括:用于建立WebSocket连接和处理消息的runtime代码比如hash用于加载热更新资源的RuntimeGlobals.hmrDownloadManifest和RuntimeGlobals.hmrDownloadUpdateHandlers接口用于处理模块更新策略的module.hot.accept接口等。关于WebpackRuntime可以参考Webpack原理系列6:深入了解Webpack运行时。经过HotModuleReplacementPlugin处理后,构建产品包含运行HMR所需的所有客户端运行时和接口。这些HMR运行时会在浏览器中执行一个基于WebSocket消息的计时框架,如图:2.2增量构建HotModuleReplacementPlugin插件除了注入客户端代码外,还会利用Webpack的watch能力在代码文件后执行增量构建改变。量化构建与生成:manifest文件:JSON格式文件,包含所有变化模块列表,命名为[hash].hot-update.json模块变化文件:js格式,包含编译后的模块代码,命名为[hash].hot后-update.js增量构建完成,Webpack会触发compilation.hooks.done钩子,并传递本次构建的统计对象stats。WDS监听donehook,回调中通过WebSocket发送模块更新消息:{"type":"hash","data":"${stats.hash}"}实际效果:2.3加载更新客户端收到hash消息后,先发送manifest请求获取本轮热更新涉及的chunk,如:注意在Webpack4及之前,热更新文件是以模块为单位的,即所有变化的模块会生成相应的热更新文件;在Webpack5之后,热更新文件的单位是chunk,如上例,对mainchunk下的任何文件进行更改,只会生成main.[hash].hot-update.js更新文件。manifest请求完成后,客户端HMRruntime开始下载更改后的chunk文件,并在本地加载最新的模块代码。2.4module.hot.accept回调经过以上步骤,浏览器加载最新模块代码后,HMR运行时会继续触发module.hot.accept回调,将最新代码替换到运行环境中。module.hot.accept是HMR运行时暴露给用户代码的重要接口之一。它在WebpackHMR系统中打开了一个漏洞,允许用户自定义模块热替换的逻辑。module.hot.accept接口签名如下:module.hot.accept(path?:string,callback?:function);它接受两个参数:path:指定需要拦截更改行为的模块路径callback:模块更新后,将最新的模块代码应用到运行环境的功能。例如,对于以下代码://src/bar.jsexportconstbar='bar'//src/index.jsimport{bar}from'./bar';constnode=document.createElement('div')node.innerText=bar;document.body.appendChild(node)module.hot.accept('./bar.js',function(){node.innerText=bar;})例子,module.hot.accept函数监听的变化事件./bar.js模块,一旦代码发生变化就会触发回调,将./bar.js导出的值应用到页面中,实现热更新的效果。module.hot.accept的作用并不复杂,但是在使用过程中还是有一些值得注意的地方,下面会详细介绍。2.4.1故障module.hot.accept函数只接受特定路径的路径参数,也就是说我们不能通过glob之类的方式批量注册热更新回调。一旦某个模块没有注册对应的module.hot.accept函数,HMRruntime就会执行一个来回策略,通常会刷新页面,以保证页面上始终运行最新的代码。2.4.2更新事件冒泡在WebpackHMR框架中,module.hot.accept函数只能捕获后代模块对应的当前模块的更新事件。例如,对于下面的模块依赖树:例子中update事件会按照模块依赖树自下而上的传递,从foo到index,从bar-1到bar再到index,但是不支持reverseor跨子树传递,即:bar.js及其子树无法在foo.js中捕获module的change事件无法在bar-1.js中捕获。bar.js的change事件很像DOM事件规范中的冒泡过程。如果不确定模块的依赖关系,建议直接在入口文件中添加Writethehotupdatefunction。2.4.3无参调用除了上述调用方式外,module.hot.accept函数还支持无参调用方式。它的作用是捕获当前文件的变化事件,从模块第一行开始重新运行该模块的代码,例如://src/bar.jsconsole.log('bar');module.hot.accept();example模块变化后,会从头开始重复执行console.log语句。2.5小结回顾整个HMR流程,所有的状态转换都是由WebSocket消息驱动的,而这部分逻辑是由HMR运行时控制的,开发者感知不大。开发者唯一需要关心的就是为每个需要热更新处理的文件注册module.hot.accept回调。幸运的是,这部分需求已经被很多成熟的Loader处理过了。作为例子,下一节我们将深入挖掘vue-loader源码,学习如何灵活使用module.hot.accept函数来处理文件更新。3、vue-loader如何实现HMRvue-loader是一个用于处理VueSingleFileComponent的Webpack加载器。它可以将如下格式的内容翻译成等效的可以在浏览器中运行的代码:除了常规的代码翻译,在HMR模式下,vue-loader还会在每个Vue文件中注入一个模块替换逻辑,比如:"./src/a.vue":/*!*********************!*\!***./src/a.vue***!\********************//***/((module,__webpack_exports__,__webpack_require__)=>{//模块代码//.../*hotreload*/if(true){varapi=__webpack_require__(/*!../node_modules/vue-hot-reload-api/dist/index.js*/"../node_modules/vue-hot-reload-api/dist/index.js")api.install(__webpack_require__(/*!vue*/"../node_modules/vue/dist/vue.runtime.esm.js"))if(api.compatible){module.hot.accept()if(!api.isRecorded('45c6ab58')){api.createRecord('45c6ab58',component.options)}else{api.reload('45c6ab58',component.options)}module.hot.accept(/*!./a.vue?vue&type=template&id=45c6ab58&*/"./src/a.vue?vue&type=template&id=45c6ab58&",__WEBPACK_OUTDATED_DEPENDENCIES__=>{/*harmonyimport*/_a_vue_vue_t是:_a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__.render,staticRenderFns:_a_vue_vue_type_template_id_45c6ab58___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns})})(__WEBPACK_OUTDATED_DEPENDENCIES__);})}}//.../***/}),这段被注入用于处理模块热替换的代码,Themain步骤是:第一次执行时,调用api.createRecord记录组件配置,api对vue-hot-reload-api库暴露的接口执行module.hot.accept()语句,监听当前模块change事件,当模块发生变化时调用api.reload执行module.hot.accept("xxx.vue?vue&type=template&xxxx",fn)来监听vue文件模板代码的变化事件。当模板模块发生变化时,为什么调用api.rerender时需要调用两次模块。热接受?这是因为vue-loader在翻译的时候会把SFC的不同段拆成多个模块,比如:template对应generatexxx.vue?vue&type=template;script对应generatexxx.vue?vue&type=script所以vue-loader必须为这些不同的模块调用accept接口来处理不同代码块的变化事件。可以看出vue-loader对HMR的支持基本都是围绕vue-hot-reload-api展开的。当代码文件变化触发module.hot.accept回调时,会根据情况执行vue-hot-reload-api暴露的组件的reload和rerender函数,这两个函数最终都会触发组件实例的$forceUpdate强制重新渲染的功能。4.总结最后,我们回顾一下Webpack的HMR特性有两个关键点。一种是监听文件变化,通过WebSocket发送变化消息;另一种是需要客户端的配合,通过module.hot.accept接口明确告知Webpack如何执行代码。代替。总的来说,并没有想象中那么难。本文转载自微信公众号「Tecvan」
