likes
comments
collection
share

HMR理解

作者站长头像
站长
· 阅读数 59

简介

HMR是指当你对代码进行修改并保存后,webpack对代码重新打包,并将新的模块发送到浏览器端,浏览器通过替换旧的模块,在不刷新浏览器的前提下,就能够对应用进行更新。

实现大致原理

webpack构建的bundle时候,加入一段服务沟通的js。文件修改会触发webpack重新构建,服务器通过向浏览器发送更新消息,浏览器通过jsonp拉取更新的模块,jsonp回调触发模块替换逻辑。

实现过程

1: watch 文件变化,触发编译,devServer推送更新消息到浏览器 2: 浏览器接收到服务端消息,然后做出响应 3: 对模块进行热更新,或者 热更新发生错误回退成刷新整个页面

1-> watch 文件变化,触发编译,devServer推送更新消息到浏览器

通过开启webpacl的watch模式编译,watch模式底层是调用系统fs.watch来监听文件变化。

当文件发生变化时候,重新编译输出bundle.js。devServer下,是没有文件会输出到output.path目录下单,这是因为webpacl把文件输出到了内存中。使用了操作内存的库是memory-fs,它是NodeJS原生fs模块内存版(in-memory)的完整功能实现,会将你请求的url映射到对应的内存中,因此读写快。源码:

// webpack-dev-middleware/lib/fs.js
 const isMemoryFs =
 !isConfiguredFs &&
 !compiler.compilers &&
 compiler.outputFileSystem instanceof MemoryFileSystem;
 ...
 compiler.outputFileSystem = fs;
 fileSystem = fs;
 } else if (isMemoryFs) {
     fileSystem = compiler.outputFileSystem;
 } else {
     fileSystem = new MemoryFileSystem();
     compiler.outputFileSystem = fileSystem;
 }

devServer通知浏览器端文件发生改变,在启动devServer时候,sockjs在服务端和浏览器短建立了一个webSocket长连接,webpack-dev-server调用webpack api 监听compile的done事件,将新模块hash值发送到浏览器端。监听部分源码

 // webpack-dev-server/lib/Server.js
 const addHooks = (compiler) => {
     ...
     done.tap('webpack-dev-server', (stats) => {
         this._sendStats(this.sockets, this.getStats(stats));
         this._stats = stats;
     });
 };
 ...
 _sendStats(sockets, stats, force) {
     ...
     this.sockWrite(sockets, 'hash', stats.hash);
     if (stats.errors.length > 0) {
         this.sockWrite(sockets, 'errors', stats.errors);
     } else if (stats.warnings.length > 0) {
         this.sockWrite(sockets, 'warnings', stats.warnings);
     } else {
         this.sockWrite(sockets, 'ok');
     }
 }
2->浏览器接收到服务端消息,然后做出响应

webpack-dev-server修改了webpack配置中的entry属性,在entry里面增加了webpack-dev-client的代码,这样在最后的bundle.js文件中,就会有接收websocket消息的代码了。并且新增了HotModuleReplacementPlugin插件。 webpack-dev-client代码,监听服务端发来的消息,只有type和hash两个内容。当收到type为hash的消息,会将hash值暂存起来,当收到ok消息,执行reload操作。reload具体操作是:

 // webpack-dev-server/client-src/default/reloadApp.js
 if (hot) {
     ...
     const hotEmitter = require('webpack/hot/emitter');
         hotEmitter.emit('webpackHotUpdate', currentHash);
     if (typeof self !== 'undefined' && self.window) {
         self.postMessage(`webpackHotUpdate${currentHash}`, '*');
     }
 }
 else if (liveReload) {
     ...
 }

可以看出,如果配置了模块热更新,就调用hotEmitter将最新hash值发送给webpackHotUpdate事件,如果没有配置热更新,就进行liveReload逻辑。webpackHotUpdate事件监听到hash值事件后,会调用webpack/lib/HotModuleReplacement.runtime 中的 check 方法,检测是否有新的更新:

 // webpack/lib/HotModuleReplacement.runtime
     function hotCheck(apply) {
     ...
         return hotDownloadManifest(hotRequestTimeout).then(function(update) {
             ...
                 hotEnsureUpdateChunk(chunkId);
             ...
             return promise;
         });
     }
     function hotEnsureUpdateChunk(chunkId) {
         if (!hotAvailableFilesMap[chunkId]) {
             hotWaitingFilesMap[chunkId] = true;
         } else {
             hotRequestedFilesMap[chunkId] = true;
             hotWaitingFiles++;
             hotDownloadUpdateChunk(chunkId);
         }
     }

可以看出,主要调用了 hotDownloadManifest 和 hotDownloadUpdateChunk, hotDownloadUpdateChunk是通过JSONP的方式,请求最新的代码模块。拿到的最新代码是这样子的: HMR理解 此时浏览器已经拿到了最新的代码,可以进行热更新,或者刷新页面了

3->对模块进行更新或者刷新页面

更新的逻辑在插件HotModuleReplacement中。大致的工作是:先找出过期的模块及其依赖;从缓存中删除过期的模块和依赖;将新的模块添加到modules中,当下次调用webpack_require (webpack 重写的 require 方法)方法的时候,就是获取到了新的模块代码了。

 // insert new code
 for (moduleId in appliedUpdate) {
 	if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
 		modules[moduleId] = appliedUpdate[moduleId];
 	}
 }

然后执行父模块中的accept回调:

 newModule.parents && newModule.parents.forEach(parentID => {
            let parentModule = __webpack_require__.c[parentID];
            parentModule.hot._acceptedDependencies[moduleID] && parentModule.hot._acceptedDependencies[moduleID]()
        });

webpack 开启HMR配置

1: 使用webpack-dev-server,设置hot为true。 2:写模块时候,增加以下示例:

if(module.hot){ // 判断是否有热加载
    module.hot.accept('./test.js', function(){ // 需要热加载的模块路径
        console.log('Accepting the updated printMe module!'); // 发生热加载,执行的操作callback
    })
}

缺点是,更新逻辑需要自己写,比如要使页面显示的内容生效,就需要在回调中写类似:document.append(xxx)

实际开发中,基本没有写过这样的逻辑,是因为大多数的loader已经处理了这部分的逻辑。

转载自:https://juejin.cn/post/7043674442005741581
评论
请登录