野风技术札记 2019-06-30
在web开发中,webpack的hot module replacement(HMR)功能是可以向运行状态的应用程序定向的注入并更新已经改变的modules。它的出现可以避免像LiveReload那样,任意文件的改变而刷新整个页面。
这个特性可以极大的提升开发拥有运行状态,以及资源文件普遍过多的前端应用型网站的效率。完整介绍可以看官网文档
本文是先从使用者的角度去介绍这个功能,然后从设计者的角度去分析并拆分需要实现的功能和实现的一些细节。
对于使用者来说,体验到这个功能需要以下的配置。
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
entry: {
app: './src/index.js'
},
devServer: {
contentBase: './dist',
hot: true,
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin()
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};代码: index.js 依赖print.js,使用module.hot.accept接受print.js的更新:
import './print';
if (module.hot) {
module.hot.accept('./print', function () {
console.log('i am updated');
})
}改变print.js代码:
console.log('print2')
console.log('i am change');此时服务端向浏览器发送socket信息,浏览器收到消息后,开始下载以hash为名字的下载的json,jsonp文件,如下图:

浏览器会下载对应的hot-update.js,并注入运行时的应用中:
webpackHotUpdate(0,{
/***/ 30:
/***/ (function(module, exports) {
console.log('print2')
console.log('i am change');
/***/ })
})0 代表着所属的chunkid,30代表着所属的moduleid。
替换完之后,执行module.hot.accept的回调函数,如下图:

简单来讲,开启了hmr功能之后,处于accepted状态的module的改动将会以jsonp的形式定向的注入到应用程序中。
一张图来表示HMR的整体流程:

当翻开bundle.js的时候,你会发现Runtime代码多了许多以下的代码:
/******/ function hotDownloadUpdateChunk(chunkId) {
/******/ ...
/******/ }
/******/ function hotDownloadManifest(requestTimeout) {
/******/ ...
/******/ }
/******
/******/ function hotSetStatus(newStatus) {
/******/ ...
/******/ }
/******/打包的时候,明明只引用了4个文件,但是整个打包文件却有30个modules之多:

/* 30 */
/***/ (function(module, exports) {
console.log('print3')
console.log('i am change');
/***/ })到现在你可能会有以下几个疑问:
以上问题,可以从三个不同的角度去解决。server,webpack,brower。
entry:{app:'./src/index.js'},转换为
entry:{app:['/Users/zhujian/Documents/workspace/webpack/webpack-demos-master/node_modules/[email protected]@webpack-dev-server/client/index.js?http://localhost:8082'],'webpack/hot/dev-server','./src/index.js'}构建业务代码时,附带上socketjs,hot代码。
Server.js
if (this.hot) this.sockWrite([conn], 'hot');
浏览器
hot: function hot() {
_hot = true;
log.info('[WDS] Hot Module Replacement enabled.');
}监听编译器的生命周期模块。
socket
compiler.plugin('compile', invalidPlugin);
compiler.plugin('invalid', invalidPlugin);
compiler.plugin('done', (stats) => {
this._sendStats(this.sockets, stats.toJson(clientStats));
this._stats = stats;
});资源文件锁定
context.compiler.plugin("done", share.compilerDone);
context.compiler.plugin("invalid", share.compilerInvalid);
context.compiler.plugin("watch-run", share.compilerInvalid);
context.compiler.plugin("run", share.compilerInvalid);MainTemplate增加module-obj,module-require事件
module-obj事件负责生成以下代码
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {},
/******/ hot: hotCreateModule(moduleId),
/******/ parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
/******/ children: []
/******/ };
/******/module-require事件负责生成以下代码
/******/ modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
新增Watching类支持watch模式,并结合watchpack监听文件变化。
class Watching {
....
}新增updateHash实现
updateHash(hash) {
this.updateHashWithSource(hash);
this.updateHashWithMeta(hash);
super.updateHash(hash);
}新增updateHash实现
updateHash(hash) {
hash.update(`${this.id} `);
hash.update(this.ids ? this.ids.join(",") : "");
hash.update(`${this.name || ""} `);
this._modules.forEach(m => m.updateHash(hash));
}增加createHash方法,默认调用md5计算compilation hash。调用依赖树module,chunk的updateHash方法。
createHash() {
....
}如:
if(module.hot){}编译后
if(true){}entry:{app:['/Users/zhujian/Documents/workspace/webpack/webpack-demos-master/node_modules/[email protected]@webpack-dev-server/client/index.js?http://localhost:8082'],'webpack/hot/dev-server','./src/index.js'}打包后
/* 5 */
/***/ (function(module, exports, __webpack_require__) {
// webpack-dev-server/client/index.js
__webpack_require__(6);
//webpack/hot/dev-server
__webpack_require__(26);
// .src/index.js
module.exports = __webpack_require__(28);
/***/ })compilation.plugin("record", function(compilation, records) {
if(records.hash === this.hash) return;
records.hash = compilation.hash;
records.moduleHashs = {};
this.modules.forEach(module => {
const identifier = module.identifier();
const hash = require("crypto").createHash("md5");
module.updateHash(hash);
records.moduleHashs[identifier] = hash.digest("hex");
});
records.chunkHashs = {};
this.chunks.forEach(chunk => {
records.chunkHashs[chunk.id] = chunk.hash;
});
records.chunkModuleIds = {};
this.chunks.forEach(chunk => {
records.chunkModuleIds[chunk.id] = chunk.mapModules(m => m.id);
});
});compilation.plugin("additional-chunk-assets", function() {
....
this.assets[filename] = source;
});初始化runtime,将所有附加的模块代码统一增加parents,children等属性。并提供check,以及apply方法去管理hmr的生命周期。
module.hot.check(true).then(function(updatedModules) {
....
})本人的简易版webpack实现simple-webpack
(完)