热更新,主要就是把前端工程 文件变更,即时编译,然后通知到浏览器端,刷新代码。
服务单与客户端通信方式有:ajax 轮询,EventSource、websockt。
客户端刷新一般分为两种:
Hot Module Replacement,简称HMR,无需完全刷新整个页面的同时,更新模块。HMR的好处,在日常开发工作中体会颇深:节省宝贵的开发时间、提升开发体验。
HMR作为一个Webpack内置的功能,可以通过HotModuleReplacementPlugin或--hot开启。
模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。
当代码文件修改并保存之后,webapck通过watch监听到文件发生变化,会对代码文件重新打包生成两个模块补丁文件manifest(js)和一个(或多个)updated chunk(js),将结果存储在内存文件系统中,通过websocket通信机制将重新打包的模块发送到浏览器端,浏览器动态的获取新的模块补丁替换旧的模块,浏览器不需要刷新页面就可以实现应用的更新。
Webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。tapable的核心思路有点类似于node.js中的events,最基本的发布/订阅模式。回顾grunt gulp 任务队列,省去一般的操作。
具体可阅读《webpack4.0源码分析之Tapable》
内置了webpack-dev-middleware和express服务器,利用webpack-dev-middleware提供文件的监听和编译,利用express提供http服务,底层利用websocket代替EventSource实现了webpack-hot-middleware提供的客户端和服务器之间的通信机制。
// webpack.config.server.js
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const config = require('./webpack.config');
config.plugins.push(
new webpack.HotModuleReplacementPlugin(),//热替换
new webpack.NoEmitOnErrorsPlugin(),//去除系统抛出的错误消息
);
// node_modules/webpack-dev-server/lib/Server.js
// 绑定监听事件
setupHooks() {
const {done} = compiler.hooks;
// 监听webpack的done钩子,tapable提供的监听方法
done.tap('webpack-dev-server', (stats) => {
this._sendStats(this.sockets, this.getStats(stats));
this._stats = stats;
});
};
// 通过websoket给客户端发消息
_sendStats() {
this.sockWrite(sockets, 'hash', stats.hash);
this.sockWrite(sockets, 'ok');
}
复制
每次修改代码保存后(gulp grunt 通过watch 监听),控制台都会出现 Compiling…字样,触发新的编译中...可以在控制台中观察到:
每次修改代码,就会触发编译。说明我们还需要监听本地代码的变化,主要是通过setupDevMiddleware方法实现的。这个方法主要执行了webpack-dev-middleware库。
webpack-dev-server只负责启动服务和前置准备工作,所有文件相关的操作都抽离到webpack-dev-middleware库了,主要是本地文件的编译和输出以及监听,无非就是职责的划分更清晰了。
webpack-dev-middleware 是一个 express 中间件,核心实现两个功能:
webpack-dev-middleware源码里做了什么事:
// node_modules/webpack-dev-middleware/index.js
compiler.watch(options.watchOptions, (err) => {
if (err) { /*错误处理*/ }
});
// 通过“memory-fs”库将打包后的文件写入内存
setFs(context, compiler);
复制
为什么代码的改动保存会自动编译,重新打包?这一系列的重新检测编译就归功于compiler.watch这个方法了。监听本地文件的变化主要是通过文件的生成时间是否有变化,这里就不细讲了。
webpack --watch
webpack --watch 启动监听模式之后,webpack第一次编译项目,并将结果存储在内存文件系统,相比较磁盘文件读写方式内存文件管理速度更快,内存webpack服务器通知浏览器加载资源,浏览器获取的静态资源除了JS code内容之外,还有一部分通过 webpack-dev-server 注入的的 HMR runtime代码,作为浏览器和webpack服务器通信的客户端( webpack-hot-middleware 提供类似的功能)。
当监听到一次webpack编译结束,_sendStats方法就通过websoket给浏览器发送通知,检查下是否需要热更新。下面重点讲的就是_sendStats方法中的ok和hash事件都做了什么。
那浏览器是如何接收到websocket的消息呢?
也就是websocket客户端代码。
// webpack-dev-server/client/index.js
var socket = require('./socket');
var onSocketMessage = {
hash: function hash(_hash) {
// 更新currentHash值
status.currentHash = _hash;
},
ok: function ok() {
sendMessage('Ok');
// 进行更新检查等操作
reloadApp(options, status);
},
};
// 连接服务地址socketUrl,?http://localhost:8080,本地服务地址
socket(socketUrl, onSocketMessage);
function reloadApp() {
if (hot) {
log.info('[WDS] App hot update...');
// hotEmitter其实就是EventEmitter的实例
var hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
}
}
复制
socket方法建立了websocket和服务端的连接,并注册了 2 个监听事件。
热更新检查事件是调用reloadApp方法。比较奇怪的是,这个方法又利用node.js的EventEmitter,发出webpackHotUpdate消息。这是为什么?为什么不直接进行检查更新呢?
个人理解就是为了更好的维护代码,以及职责划分的更明确。websocket仅仅用于客户端(浏览器)和服务端进行通信。而真正做事情的活还是交回给了webpack。
webpack如何刷新资源
// node_modules/webpack/hot/dev-server.js
var check = function check() {
module.hot.check(true)
.then(function(updatedModules) {
// 容错,直接刷新页面
if (!updatedModules) {
window.location.reload();
return;
}
// 热更新结束,打印信息
if (upToDate()) {
log("info", "[HMR] App is up to date.");
}
})
.catch(function(err) {
window.location.reload();
});
};
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function(currentHash) {
lastHash = currentHash;
check();
});
复制
这里webpack监听到了webpackHotUpdate事件,并获取最新了最新的hash值,然后终于进行检查更新了。检查更新呢调用的是module.hot.check方法。那么问题又来了,module.hot.check又是哪里冒出来了的!答案是HotModuleReplacementPlugin搞得鬼。
moudle.hot.check 之后的源码都是HotModuleReplacementPlugin塞入到bundle.js中
利用上一次保存的hash值,调用hotDownloadManifest发送xxx/hash.hot-update.json的ajax请求;
请求结果获取热更新模块,以及下次热更新的Hash 标识,并进入热更新准备阶段。
hotAvailableFilesMap = update.c; // 需要更新的文件
hotUpdateNewHash = update.h; // 更新下次热更新hash值
hotSetStatus("prepare"); // 进入热更新准备状态
复制
调用hotDownloadUpdateChunk发送xxx/hash.hot-update.js 请求,通过JSONP方式。
function hotDownloadUpdateChunk(chunkId) {
var script = document.createElement("script");
script.charset = "utf-8";
script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
if (null) script.crossOrigin = null;
document.head.appendChild(script);
}
复制
再看下webpackHotUpdate这个方法。
window["webpackHotUpdate"] = function (chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules);
} ;
复制
function hotAddUpdateChunk(chunkId, moreModules) {
// 更新的模块moreModules赋值给全局全量hotUpdate
for (var moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
hotUpdate[moduleId] = moreModules[moduleId];
}
}
// 调用hotApply进行模块的替换
hotUpdateDownloaded();
}
复制
热更新的核心逻辑就在hotApply方法了。
删除过期的模块,就是需要替换的模块,通过hotUpdate可以找到旧模块
var queue = outdatedModules.slice();
while (queue.length > 0) {
moduleId = queue.pop();
// 从缓存中删除过期的模块
module = installedModules[moduleId];
// 删除过期的依赖
delete outdatedDependencies[moduleId];
// 存储了被删掉的模块id,便于更新代码
outdatedSelfAcceptedModules.push({
module: moduleId
});
}
复制
将新的模块添加到 modules 中
appliedUpdate[moduleId] = hotUpdate[moduleId];
for (moduleId in appliedUpdate) {
if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
复制
通过__webpack_require__执行相关模块的代码
for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
var item = outdatedSelfAcceptedModules[i];
moduleId = item.module;
try {
// 执行最新的代码
__webpack_require__(moduleId);
} catch (err) {
// ...容错处理
}
}
复制
hotApply的确比较复杂,知道大概流程就好了,这一小节,要求你对webpack打包后的文件如何执行的有一些了解,大家可以自去看下。
WebSocket是基于TCP的全双工通讯的协议,它与EventSource有着本质上的不同.(前者基于TCP,后者依然基于HTTP)。
可以通过scockt.io 库来实现更新
webpack-hot-middleware中间件是webpack的一个plugin,通常结合webpack-dev-middleware一起使用。借助它可以实现浏览器的无刷新更新(热更新),即webpack里的HMR(Hot Module Replacement)。
核心是给webpack提高服务端和客户端之间的通信机制,内部使用windoe.EventSocurce实现。
如何配置请参考 webpack-hot-middleware,如何理解其相关插件请参考 手把手深入理解 webpack dev middleware 原理与相关 plugins。
HotModuleReplacementPlugin
EventSource 不是一个新鲜的技术,它早就随着H5规范提出了,正式一点应该叫Server-sent events,即SSE。鉴于传统的通过ajax轮训获取服务器信息的技术方案已经过时,我们迫切需要一个高效的节省资源的方式去获取服务器信息,一旦服务器资源有更新,能够及时地通知到客户端,从而实时地反馈到用户界面上。EventSource就是这样的技术,它本质上还是HTTP,通过response流实时推送服务器信息到客户端。
const es = new EventSource('/message');// /message是服务端支持EventSource的接口
es.onmessage = function(e){
console.log(e.data); // 打印服务器推送的信息
}
复制
使用EventSource技术实时更新网页信息十分高效。实际使用中,我们几乎不用担心兼容性问题,主流浏览器都了支持EventSource,当然,除了掉队的IE系。对于不支持的浏览器,其PolyFill方案请参考HTML5 Cross Browser Polyfills
服务端实现/message接口,需要返回类型为 text/event-stream的响应头。
var http = require('http');
http.createServer(function(req,res){
if(req.url === '/message'){
res.writeHead(200,{
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
setInterval(function(){
res.write('data: ' + +new Date() + '\n\n');
}, 1000);
}
}).listen(8888);
复制
为了避免缓存,Cache-Control 特别设置成了 no-cache,为了能够发送多个response, Connection被设置成了keep-alive.。发送数据时,请务必保证服务器推送的数据以 data:开始,以\n\n结束,否则推送将会失败(原因就不说了,这是约定的)。
参看文章:
webpack 热更新(HMR)实现原理 https://segmentfault.com/a/1190000022485386
轻松理解webpack热更新原理 轻松理解webpack热更新原理 - 掘金
Webpack 热更新实现原理分析 Webpack 热更新实现原理分析 - 知乎
Webpack插件机制之Tapable-源码解析 https://juejin.im/post/5dc169b0f265da4d542092c6
转载本站文章《webpack原理(1):Webpack热更新实现原理代码分析 》, 请注明出处:webpack原理(1):Webpack热更新实现原理代码分析 - webpack原理分析与源码解读 - 周陆军的个人网站