Webpack的HMR原理分析
module.exports = {
entry : {
main : './src/main.js',
home : './src/home.js',
common : ['jquery'],
common2 : ['react']
},
output : {
path: path.join(__dirname, 'build'),
filename: '[name].js'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new CommonsChunkPlugin({
name: "chunk",
minChunks: function(module, count) {
return module.resource && (/common/).test(module.resource) && count === 2;
},
}),
new SetVersion()
],
devServer: {
contentBase: './build',
hot: true
//支持 HMR
}
}
在 Webpack 的 devServer 配置中设置了 hot 为 true,而且在 Webpack 的 plugin 中添加了 new webpack.HotModuleReplacementPlugin() 这个插件。
Webpack 的 HMR 的实现原理
compiler.plugin("done", function(stats) {
//clientStats 表示需要保存 stats 中的那些属性,可以允许配置,参见 Webpack 官网
this._sendStats(this.sockets, stats.toJson(clientStats));
this._stats = stats;
}.bind(this));
每次 compiler 的 'done' 钩子函数被调用的时候就会要求客户端去检查模块更新,如果客户端不支持 HMR,那么就会全局加载。
Server.prototype._sendStats = function(sockets, stats, force) {
if(!force &&
stats &&
(!stats.errors || stats.errors.length === 0) &&
stats.assets &&
stats.assets.every(function(asset) {
return !asset.emitted;
//(1)每一个 asset 都是没有 emitted 属性,表示没有发生变化。如果发生变化那么这个 assets 肯定有 emitted 属性
})
)
return this.sockWrite(sockets, "still-ok");
//(1)将 stats 的 hash 写给 socket 客户端
this.sockWrite(sockets, "hash", stats.hash);
//设置 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");
}
通过 webpack-dev-server 提供的 websocket 服务端代码通知 websocket 客户端)发送的 ok 和 warning 信息的时候会要求更新。如果支持 HMR 的情况下就会要求检查更新,同时发送过来的还有服务器端本次编译的 compilation 的 hash 值。如果不支持 HMR,那么要求刷新页面。
ok: function() {
sendMsg("Ok");
if(useWarningOverlay || useErrorOverlay) overlay.clear();
if(initial) return initial = false;
reloadApp();
},
warnings: function(warnings) {
log("info", "[WDS] Warnings while compiling.");
var strippedWarnings = warnings.map(function(warning) {
return stripAnsi(warning);
});
sendMsg("Warnings", strippedWarnings);
for(var i = 0; i < strippedWarnings.length; i++)
console.warn(strippedWarnings[i]);
if(useWarningOverlay) overlay.showMessage(warnings);
if(initial) return initial = false;
reloadApp();
},
function reloadApp() {
//(1)如果开启了 HMR 模式
if(hot) {
log("info", "[WDS] App hot update...");
var hotEmitter = require("webpack/hot/emitter");
hotEmitter.emit("webpackHotUpdate", currentHash);
//重新启动 webpack/hot/emitter,同时设置当前 hash,通知上面的 webpack-dev-server 的 webpackHotUpdate 事件,告诉它打印哪些模块的更新信息
if(typeof self !== "undefined" && self.window) {
// broadcast update to window
self.postMessage("webpackHotUpdate" + currentHash, "*");
}
} else {
//(2)如果不是 Hotupdate 那么直接 reload 我们的 window 就可以了
log("info", "[WDS] App updated. Reloading...");
self.location.reload();
}
}
如果 ok 则调用reloadApp方法,而 reloadApp 方法 判断如果开启了 HMR 模式, 通过hotEmitter 执行webpackHotUpdate方法,如果不是 Hotupdate 那么直接 reload刷新网页。
if(module.hot) {
var lastHash;
var upToDate = function upToDate() {
return lastHash.indexOf(__webpack_hash__) >= 0;
//(1)如果两个 hash 相同那么表示没有更新,其中 lastHash 表示上一次编译的 hash,记住是 compilation 的 hash
//只有在 HotModuleReplacementPlugin 开启的时候存在。任意文件变化后 compilation 都会发生变化
};
//(2)下面是检查更新的模块
var check = function check() {
module.hot.check().then(function(updatedModules) {
//(2.1)没有更新的模块直接返回,通知用户无需 HMR
if(!updatedModules) {
console.warn("[HMR] Cannot find update. Need to do a full reload!");
console.warn("[HMR] (Probably because of restarting the webpack-dev-server)");
return;
}
//(2.2)开始更新
return module.hot.apply({
ignoreUnaccepted: true,
//和 accept 函数指定热加载那些模块
ignoreDeclined: true,
//decline 表示不支持这个模块热加载
ignoreErrored: true,
//error 表示出错的模块
onUnaccepted: function(data) {
console.warn("Ignored an update to unaccepted module " + data.chain.join(" -> "));
},
onDeclined: function(data) {
console.warn("Ignored an update to declined module " + data.chain.join(" -> "));
},
onErrored: function(data) {
console.warn("Ignored an error while updating module " + data.moduleId + " (" + data.type + ")");
}
//(2.2.1)renewedModules 表示哪些模块已经更新了
}).then(function(renewedModules) {
//(2.2.2)如果有模块没有更新完成,那么继续检查
if(!upToDate()) {
check();
}
//(2.2.3)更新的模块 updatedModules,renewedModules 表示哪些模块已经更新了
require("./log-apply-result")(updatedModules, renewedModules);
//通知已经热加载完成
if(upToDate()) {
console.log("[HMR] App is up to date.");
}
});
}).catch(function(err) {
//(2.3)更新异常,输出 HMR 信息
var status = module.hot.status();
if(["abort", "fail"].indexOf(status) >= 0) {
console.warn("[HMR] Cannot check for update. Need to do a full reload!");
console.warn("[HMR] " + err.stack || err.message);
} else {
console.warn("[HMR] Update check failed: " + err.stack || err.message);
}
});
};
var hotEmitter = require("./emitter");
//(3)emitter 模块内容,也就是导出一个 events 实例
/*
var EventEmitter = require("events");
module.exports = new EventEmitter();
*/
hotEmitter.on("webpackHotUpdate", function(currentHash) {
lastHash = currentHash;
//(3.1)表示本次更新后得到的 hash 值
if(!upToDate()) {
//(3.1.1)有更新
var status = module.hot.status();
if(status === "idle") {
console.log("[HMR] Checking for updates on the server...");
check();
} else if(["abort", "fail"].indexOf(status) >= 0) {
console.warn("[HMR] Cannot apply update as a previous update " + status + "ed. Need to do a full reload!");
}
}
});
console.log("[HMR] Waiting for update signal from WDS...");
} else {
throw new Error("[HMR] Hot Module Replacement is disabled.");
}
上面看到了 log-apply-result 模块,该模块是在所有的内容已经更新完成后调用的,下面继续看一下它到底做了什么事情:
module.exports = function(updatedModules, renewedModules) {
//(1)renewedModules 表示哪些模块需要更新,剩余的模块 unacceptedModules 表示,哪些模块由于 ignoreDeclined,ignoreUnaccepted 配置没有更新
var unacceptedModules = updatedModules.filter(function(moduleId) {
return renewedModules && renewedModules.indexOf(moduleId) < 0;
});
//(2)unacceptedModules 表示该模块无法 HMR,打印 log
if(unacceptedModules.length > 0) {
console.warn("[HMR] The following modules couldn't be hot updated: (They would need a full reload!)");
unacceptedModules.forEach(function(moduleId) {
console.warn("[HMR] - " + moduleId);
});
}
//(2)没有模块更新,表示模块是最新的
if(!renewedModules || renewedModules.length === 0) {
console.log("[HMR] Nothing hot updated.");
} else {
console.log("[HMR] Updated modules:");
//(3)打印那些模块被热更新。每一个 moduleId 都是数字,那么建议使用 NamedModulesPlugin(webpack 2 建议)
renewedModules.forEach(function(moduleId) {
console.log("[HMR] - " + moduleId);
});
var numberIds = renewedModules.every(function(moduleId) {
return typeof moduleId === "number";
});
if(numberIds)
console.log("[HMR] Consider using the NamedModulesPlugin for module names.");
}
};
所以"webpack/hot/only-dev-server"的文件内容就是检查哪些模块更新了(通过 webpackHotUpdate 事件完成,而该事件依赖于compilation的 hash 值),其中哪些模块更新成功,而哪些模块由于某种原因没有更新成功。
接下来看看 "webpack/hot/dev-server":
f(module.hot) {
var lastHash;
//__webpack_hash__ 是每次编译的 hash 值是全局的
var upToDate = function upToDate() {
return lastHash.indexOf(__webpack_hash__) >= 0;
};
var check = function check() {
module.hot.check(true).then(function(updatedModules) {
//检查所有要更新的模块,如果没有模块要更新那么回调函数就是 null
if(!updatedModules) {
console.warn("[HMR] Cannot find update. Need to do a full reload!");
console.warn("[HMR] (Probably because of restarting the webpack-dev-server)");
window.location.reload();
return;
}
//如果还有更新
if(!upToDate()) {
check();
}
require("./log-apply-result")(updatedModules, updatedModules);
//已经被更新的模块都是 updatedModules
if(upToDate()) {
console.log("[HMR] App is up to date.");
}
}).catch(function(err) {
var status = module.hot.status();
//如果报错直接全局 reload
if(["abort", "fail"].indexOf(status) >= 0) {
console.warn("[HMR] Cannot apply update. Need to do a full reload!");
console.warn("[HMR] " + err.stack || err.message);
window.location.reload();
} else {
console.warn("[HMR] Update failed: " + err.stack || err.message);
}
});
};
var hotEmitter = require("./emitter");
//获取 MyEmitter 对象
hotEmitter.on("webpackHotUpdate", function(currentHash) {
lastHash = currentHash;
if(!upToDate() && module.hot.status() === "idle") {
//调用 module.hot.status 方法获取状态
console.log("[HMR] Checking for updates on the server...");
check();
}
});
console.log("[HMR] Waiting for update signal from WDS...");
} else {
throw new Error("[HMR] Hot Module Replacement is disabled.");
}
两者的主要代码区别在于 check() 函数的调用方式:
如果 autoApply 设置为 true,那么回调函数传入的就是所有被自己 dispose 处理 过的模块,同时 apply 方法也会自动调用,如果 auApply 设置为 false,那么所有的模块更新都会通过手动调用 apply 来完成。而所说的被自己 dispose 处理就是通过如下的方式来完成的:
if (module.hot) {
module.hot.accept();
//支持热更新
//当前模块代码更新后的回调,常用于移除持久化资源或者清除定时器等操作,如果想传递数据到更新后的模块,可以通过传入 data 参数,后续参数可以通过 module.hot.data 获取
module.hot.dispose(() => {
window.clearInterval(intervalId);
});
}
而一般调用 webpack-dev-server 只会添加 --hot 而已,即内部不需要调用 apply,而传入的都是被 dispose 处理过的模块:
f(devServerOptions.hotOnly)
devClient.push("webpack/hot/only-dev-server");
else if(devServerOptions.hot)
devClient.push("webpack/hot/dev-server");