重学webpack系列(七) -- webpack的HMR的实践与原理

473 阅读5分钟

上一章重学webpack系列(六) -- webpack的sourceMap实践与原理我们讲解了sourceMap的的基础实践与原理,这一章我们来谈一谈热更新

什么是热更新(HotModuleReplace)

热更新自动刷新有区别吗,肯定是有区别的,自动刷新我可以简单的说成window.reload(),这种是能够让用户有体感的刷新的,热更新的使命是不刷新浏览器,自动更新内容的一种方式。换句话说,热更新就是在运行过程中模块的及时变化。

热更新的好处

  • window.reload()之后,整个浏览器需要重新去渲染dom数据状态什么的都会被清空,在中后台大量表单的场景下,这将无疑会带来无数的吐槽甚至愤怒热更新很好的解决了这个问题。
  • 针对上一条,我们可以用额外的操作把数据,状态写入内存,等window.reload()之后,重新读取内存,可以有效的解决这个问题,但是有多余的冗余操作。

HMR的基础实践

老样子我们还是来看一看webpack.config.js文件的配置,我们通过配置hot属性来开启HMR,不过热更新需要用webpack内置HotModuleReplaceMentPlugin来支持。

const webpack = required('webpack');
module.exports = {
    ...
    plugins: [
        ...
        // 支持热更新的插件
        new webpack.HotModuleReplaceMentPlugin()
        ...
    ]
    devServer: {
        // 目录  
        static: {
          directory: path.join(__dirname, 'build'),
        },
        open:'main.html',
        // 开启热更新
        hot: true
        // hotOnly: true
        // 启用gzip压缩
        compress: true,
        // httpServer端口
        port: 9000,
        proxy: {"/api":{..}},
        },
        ...
}

不过webpack5中已经内置了这个插件,不需要去配置了。

image.png

HMR的使用注意事项

HMR支持css模块的热更新,但是不支持js、模块的热更新,因为在HMR中并没有对js模块做热更新的处理,css模块的热更新来自于loader本身的热更新的支持,css变化只是替换掉页面上的样式就可以了。

为什么HMR没有对js模块做处理呢?

因为在不同的js模块当中,模块的导出可能是对象,可能是函数,又可能是字符串,不同的导出产物可能有不同的使用方法,这样的毫无规律可寻的结果导致webpack无从下手去实现一个通用的HMR解决方案,但是在框架中,比如React中要求每个模块的导出必须是一个函数或者类,这样就有了统一的规律,所以实现HMR,也就变得实际可作。

如何去处理HMR对js模块的支持呢?

  • webpackHMR提供了一个hot对象,对象上的accept方法能够在HMR之前,处理模块更新的逻辑。比如我们在编码一个form的时候,改动了formjs逻辑,我们应该这样做。
// 获取到js模块的入口函数
import createForm from './handleForm.js'
// 获取到form
const form = document.querySelector('./handleForm.js')
if(module.hot){
    module.hot.accept('form的路径', ()=>{
        // 保存当前form的现有内容
        const value = form.innerHTML;
        // 从nody上移除form
        document.body.removeChild(form);
        // 更新form位最新的js模块
        form = createForm();
        // 置入老的form的内容
        form.innerHTML = value;
        // 插入到document中
        document.body.appendChild(form)
    })
}

// 处理图片
const img = document.querySelector('img')
import imageUrl from './image.png'

if(module.hot){
    module.hot.accept('./image.png', ()=>{
        img.src = imageUrl
    })
}
  • 运行在HMR中的错误的处理
    • 一旦在HMR中发生了错误,那么HMR的功能就会立即失效,从而导致浏览器自动刷新,所以同学们在开发过程中也可以关注一下这个问题。
    • 如果不想因为这样的问题发生导致浏览器自动刷新,可以配置hotOnly:true,解决这个问题。

HMR的原理

重学webpack系列(五) -- webpack的devServer实践与原理中我们已已经讲到了,webpack监听文件构建,创建httpServer,与client建立socket联系,检测hot && allowToHotliveReload && allowToLiveReload(),执行热更新或者直接本地更新,本地浏览器刷新我们不谈,我们看一看热更新是怎么做的。

// node_modules/webpack/hot/dev-server.js
/* globals __webpack_hash__ */
if (module.hot) {
	var lastHash; // 每一次传入的hash是一个全局对象,此用来记录上一次的hash,以作比较
        
        // 根据文件hash来判定是不是需要更新
	var upToDate = function upToDate() {
		return lastHash.indexOf(__webpack_hash__) >= 0;
	};
	var log = require("./log");
        
        // 
	var check = function check() {
		module.hot
			.check(true) // 断点走向webpack\lib\hmr\HotModuleReplacement.runtime.js
			.then(function (updatedModules) {
                                // HMR失效,自动走浏览器刷新
				if (!updatedModules) {
					log(...)
					window.location.reload();
					return;
				}
                                
                                // 不需要更新文件,走一下个文件的check
				if (!upToDate()) {
					check();
				}
                                
                                // ***引入更新函数,执行更新***
				require("./log-apply-result")(updatedModules, updatedModules);

				if (upToDate()) {
					log("info", "[HMR] App is up to date.");
				} // 更新完成
			})
			.catch(function (err) {
                                // 异常处理,直接刷新浏览器
				...
                                window.location.reload();
			});
	};
	
	var hotEmitter = require("./emitter");
        
        // 添加webpackHotUpdate事件,更新中执行。
	hotEmitter.on("webpackHotUpdate", function (currentHash) {
		lastHash = currentHash;
                // 检查server的hash与client的_webpack_hash_是否一致
                // 检查是否处于idle(空闲)阶段。才能触发更新
		if (!upToDate() && module.hot.status() === "idle") {
			log("info", "[HMR] Checking for updates on the server...");
			check();
		}
	});
	log("info", "[HMR] Waiting for update signal from WDS...");
} else {
	throw new Error("[HMR] Hot Module Replacement is disabled.");
}

上述代码都是devServer的前戏,我们来看一看具体核心check函数是怎么做的更新是怎么做的。

// node_modules/webpack/lib/hmr/HotModuleReplacement.runtime.js
...
check: hotCheck,
...

// hotCheck
function hotCheck(applyOnUpdate) {
  // 处理状态
  if (currentStatus !== "idle") {
    throw new Error("check() is only allowed in idle status");
  }
  // 只有check(true)才会走到这里
  return setStatus("check")
    // 传入hmrDownloadManifest函数去下载mainfist文件
    .then($hmrDownloadManifest$)
    .then(function (update) {  
      ...
      }

      return setStatus("prepare").then(function () {
        // 创建更新模块数组  
        var updatedModules = [];
        currentUpdateApplyHandlers = [];

        return Promise.all(
          Object.keys($hmrDownloadUpdateHandlers$).reduce(function (
            promises,
            key
          ) {
            // 执行hmrDownloadUpdateHandlers,读取chunk文件,比较文件并完成热更新。
            $hmrDownloadUpdateHandlers$[key](
              update.c,
              update.r,
              update.m,
              promises,
              currentUpdateApplyHandlers,
              updatedModules
            );
            return promises;
          },
          [])
        ).then(function () {
          return waitForBlockingPromises(function () {
            if (applyOnUpdate) {
              return internalApply(applyOnUpdate);
            } else {
              return setStatus("ready").then(function () {
                return updatedModules;
              });
            }
          });
        });
      });
    });
}

总结

接着重学webpack系列(五) -- webpack的devServer实践与原理来讲,HMR的底层原理大致可以用下图表示。

image.png 下一章我们将会讲一讲webpack的执行机制,顺便看一下loaderpluginHMRwebpack流程中,是怎么执行的, 直通车 >>> 重学webpack系列(八) -- webpack的运行机制与工作原理