HMR理解
简介
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的方式,请求最新的代码模块。拿到的最新代码是这样子的:
此时浏览器已经拿到了最新的代码,可以进行热更新,或者刷新页面了
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