热更新原理
1. webpack-dev-server启动本地服务
这里首先会启动webpack并生成compiler
实例(compiler实例通过各种事件钩子可以实现监听编译无效、编译结束等功能);
然后会通过express
启动一个本地服务,用于服务浏览器对打包资源的请求;
同时,server启动后会启动一个websocket
服务,用于服务端与浏览器之间的全双工通信(比如本地资源更新并打包结束后通知客户端请求新的资源);
webpack-dev-server/client/index.js目录下onSocketMessage函数如下:
var onSocketMessage = {
hot: function hot() {
if (parsedResourceQuery.hot === "false") {
return;
}
options.hot = true;
},
liveReload: function liveReload() {
if (parsedResourceQuery["live-reload"] === "false") {
return;
}
options.liveReload = true;
},
/**
* @param {string} hash
*/
hash: function hash(_hash) {
status.previousHash = status.currentHash;
status.currentHash = _hash;
},
logging: setAllLogLevel,
/**
* @param {boolean} value
*/
overlay: function overlay(value) {
if (typeof document === "undefined") {
return;
}
options.overlay = value;
},
/**
* @param {number} value
*/
reconnect: function reconnect(value) {
if (parsedResourceQuery.reconnect === "false") {
return;
}
options.reconnect = value;
},
"still-ok": function stillOk() {
log.info("Nothing changed.");
if (options.overlay) {
hide();
}
sendMessage("StillOk");
},
ok: function ok() {
sendMessage("Ok");
if (options.overlay) {
hide();
}
reloadApp(options, status);
},
close: function close() {
log.info("Disconnected!");
if (options.overlay) {
hide();
}
sendMessage("Close");
}
};
2. 修改webpack.config.js的entry配置
启动本地服务后,会在入口动态新增两个文件入口
并一同打包到bundle文件中,如下:
// 修改后的entry入口
{ entry:
{ index:
[
// socket客户端代码,onSocketMessge,处理ok/hash等消息
'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080',
// 监听'webpackHotUpdate热更新检查
'xxx/node_modules/webpack/hot/dev-server.js',
// 开发配置的入口
'./src/index.js'
],
},
}
这里需要说明下两个文件的作用:
webpack/hot/dev-server.js
:该函数主要用于处理检测更新,将其注入到客户端代码中,然后当接收到服务端发送的webpackHotUpdate
消息后调用module.hot.check()
方法检测更新;有更新时通过module.hot.apply()
方法应用更新webpack-dev-server/client/index.js
:动态注入socket客户端代码,通过onSocketMessage
函数处理socket服务端的消息;用于更新hash及热模块检测和替换;
// webpack/hot/dev-server.js核心代码
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function (currentHash) {
lastHash = currentHash;
if (!upToDate() && module.hot.status() === "idle") {
log("info", "[HMR] Checking for updates on the server...");
check(); // module.hot.check()
}
});
3. webpack监听文件变化
监听文件变化主要通过setupDevMiddleware
方法,底层主要是通过webpack-dev-middleware
,调用了compiler.watch
方法监听文件的变化;
当文件变化时,通过memory-fs
库将打包后的文件写入内存,而不是\dist
目录;
其实就是因为
webpack-dev-server
只负责启动服务和前置准备工作
,所有文件相关的操作
都抽离到webpack-dev-middleware
库了,主要是本地文件的编译和输出以及监听;
Compiler
支持可以监控文件系统
的 监听(watching) 机制,并且在文件修改时重新编译
。 当处于监听模式(watch mode)时,compiler
会触发诸如watchRun
,watchClose
和invalid
等额外的事件。
4. 监听webpack编译结束
通过webpack-dev-server/lib/Server.js
中的setupHooks()
方法监听webpack编译完成
;主要是通过done
钩子监听到当次compilation
编译完成时,触发done
回调并调用sendStats
发送socket消息ok
和hash
事件
5. 浏览器接收到热更新的通知
上面讲到,当次compilation
结束后会通过websocket
发送消息通知到客户端,客户端检测是否需要热更新;客户端根据消息类型(ok
/hash
/hot
/ovelay
/invalid
等)做对应的处理
客户端接收websocket的代码在启动webpack服务后会动态加入到entry入口中并打包到
bundle.js
中,因此可以正常接收socket服务端消息
以下是部分socketMessge的处理函数,这里hash
可以看到用于更新previousHash
及currentHash
,ok
事件主要用于进行热更新检查,主要通过reloadApp
实现,其内部则是通过node的EventEmitter发送了webpackHotUpdate
事件触发热更新检查;而真正的热更新检查是由HotModuleReplacementPlugin
的module.hot.check()
实现的;
/**
* @param {string} hash
*/
hash: function hash(_hash) {
status.previousHash = status.currentHash;
status.currentHash = _hash;
},
ok: function ok() {
sendMessage("Ok");
if (options.overlay) {
hide();
}
reloadApp(options, status);
},
/**
* @param {boolean} value
*/
overlay: function overlay(value) {
if (typeof document === "undefined") {
return;
}
options.overlay = value;
},
invalid: function invalid() {
log.info("App updated. Recompiling..."); // Fixes #1042. overlay doesn't clear if errors are fixed but warnings remain.
if (options.overlay) {
hide();
}
sendMessage("Invalid");
},
module.hot.check
与module.hot.apply
方法与HotModuleReplacementPlugin
相关,接下来我们看看其作用
6. HotModuleReplacementPlugin
如下所示,我们可以看到module.hot
的定义由createModuleHotObject
决定,内部的hot
对象中定义了check: hotChek
、apply: hotApply
等;具体实现需要借助setStatus
函数及对应status
由于这些代码需要在HMR中使用,也是运行时代码,所以同样会被开始就注入到入口文件中
// *\node_modules\webpack\lib\hmr\HotModuleReplacement.runtime.js
$interceptModuleExecution$.push(function (options) {
var module = options.module;
var require = createRequire(options.require, options.id);
module.hot = createModuleHotObject(options.id, module);
module.parents = currentParents;
module.children = [];
currentParents = [];
options.require = require;
});
createModuleHotObject
的实现如下:
function createModuleHotObject(moduleId, me) {
var _main = currentChildModule !== moduleId;
var hot = {
// private stuff
_acceptedDependencies: {},
_acceptedErrorHandlers: {},
_declinedDependencies: {},
_selfAccepted: false,
_selfDeclined: false,
_selfInvalidated: false,
_disposeHandlers: [],
_main: _main,
_requireSelf: function () {
currentParents = me.parents.slice();
currentChildModule = _main ? undefined : moduleId;
__webpack_require__(moduleId);
},
// Module API
active: true,
accept: function (dep, callback, errorHandler) {
if (dep === undefined) hot._selfAccepted = true;
else if (typeof dep === "function") hot._selfAccepted = dep;
else if (typeof dep === "object" && dep !== null) {
for (var i = 0; i < dep.length; i++) {
hot._acceptedDependencies[dep[i]] = callback || function () {};
hot._acceptedErrorHandlers[dep[i]] = errorHandler;
}
} else {
hot._acceptedDependencies[dep] = callback || function () {};
hot._acceptedErrorHandlers[dep] = errorHandler;
}
},
decline: function (dep) {
if (dep === undefined) hot._selfDeclined = true;
else if (typeof dep === "object" && dep !== null)
for (var i = 0; i < dep.length; i++)
hot._declinedDependencies[dep[i]] = true;
else hot._declinedDependencies[dep] = true;
},
dispose: function (callback) {
hot._disposeHandlers.push(callback);
},
addDisposeHandler: function (callback) {
hot._disposeHandlers.push(callback);
},
removeDisposeHandler: function (callback) {
var idx = hot._disposeHandlers.indexOf(callback);
if (idx >= 0) hot._disposeHandlers.splice(idx, 1);
},
invalidate: function () {
this._selfInvalidated = true;
switch (currentStatus) {
case "idle":
currentUpdateApplyHandlers = [];
Object.keys($hmrInvalidateModuleHandlers$).forEach(function (key) {
$hmrInvalidateModuleHandlers$[key](
moduleId,
currentUpdateApplyHandlers
);
});
setStatus("ready");
break;
case "ready":
Object.keys($hmrInvalidateModuleHandlers$).forEach(function (key) {
$hmrInvalidateModuleHandlers$[key](
moduleId,
currentUpdateApplyHandlers
);
});
break;
case "prepare":
case "check":
case "dispose":
case "apply":
(queuedInvalidatedModules = queuedInvalidatedModules || []).push(
moduleId
);
break;
default:
// ignore requests in error states
break;
}
},
// Management API
check: hotCheck,
apply: hotApply,
status: function (l) {
if (!l) return currentStatus;
registeredStatusHandlers.push(l);
},
addStatusHandler: function (l) {
registeredStatusHandlers.push(l);
},
removeStatusHandler: function (l) {
var idx = registeredStatusHandlers.indexOf(l);
if (idx >= 0) registeredStatusHandlers.splice(idx, 1);
},
//inherit from previous dispose call
data: currentModuleData[moduleId]
};
currentChildModule = undefined;
return hot;
}
7. module.hot.check 开始热更新
hotCheck的实现如下:
function hotCheck(applyOnUpdate) {
if (currentStatus !== "idle") {
throw new Error("check() is only allowed in idle status");
}
return setStatus("check")
.then($hmrDownloadManifest$)
.then(function (update) {
if (!update) {
return setStatus(applyInvalidatedModules() ? "ready" : "idle").then(
function () {
return null;
}
);
}
return setStatus("prepare").then(function () {
var updatedModules = [];
currentUpdateApplyHandlers = [];
return Promise.all(
Object.keys($hmrDownloadUpdateHandlers$).reduce(function (
promises,
key
) {
$hmrDownloadUpdateHandlers$[key](
update.c,
update.r,
update.m,
promises,
currentUpdateApplyHandlers,
updatedModules
);
return promises;
},
[])
).then(function () {
return waitForBlockingPromises(function () {
if (applyOnUpdate) {
return internalApply(applyOnUpdate);
} else {
return setStatus("ready").then(function () {
return updatedModules;
});
}
});
});
});
});
}
可以看到check
状态成功后会进入prepare
状态,成功后会返回一个promise对象;
$hmrDownloadUpdateHandlers$.$key$ = function (
chunkIds,
removedChunks,
removedModules,
promises,
applyHandlers,
updatedModulesList
) {
applyHandlers.push(applyHandler);
currentUpdateChunks = {};
currentUpdateRemovedChunks = removedChunks;
currentUpdate = removedModules.reduce(function (obj, key) {
obj[key] = false;
return obj;
}, {});
currentUpdateRuntime = [];
chunkIds.forEach(function (chunkId) {
if (
$hasOwnProperty$($installedChunks$, chunkId) &&
$installedChunks$[chunkId] !== undefined
) {
promises.push($loadUpdateChunk$(chunkId, updatedModulesList));
currentUpdateChunks[chunkId] = true;
} else {
currentUpdateChunks[chunkId] = false;
}
});
if ($ensureChunkHandlers$) {
$ensureChunkHandlers$.$key$Hmr = function (chunkId, promises) {
if (
currentUpdateChunks &&
$hasOwnProperty$(currentUpdateChunks, chunkId) &&
!currentUpdateChunks[chunkId]
) {
promises.push($loadUpdateChunk$(chunkId));
currentUpdateChunks[chunkId] = true;
}
};
}
};
以下面代码作为例子:
// src/index.js
import { addByBit, add } from './add';
export default function () {
console.log('rm console loader test==', addByBit(1,2));
return 'hello index file.......';
}
// src/foo.js
export default () => {
console.log('hello webpack demos!')
return 'hello webpack'
}
// src/add.js
// add function
export function add(a, b) {
console.log('a + b===', a + b);
return a + b;
}
// add by bit-operation
export function addByBit(a, b) {
if (b === 0) return a;
let c = a ^ b,
d = (a & b) << 1;
return addByBit(c, d);
}
- 我们更改
src/index.js
,将return 'hello index file.......';
更改为return 'hello index file.';
触发更新时首先会发送ajax
请求http://localhost:8081/index.3b102885936c5d7de6d5.hot-update.json
,3b102885936c5d7de6d5
为oldhash,对应的返回为:
// 20221205151344
// http://localhost:8080/index.455e1dda0f8e3cbf9ba0.hot-update.json
{
"c": [
"index"
],
"r": [],
"m": []
}
c
: chunkIds,
r
: removedChunks,
m
: removedModules
-
我们更改
src/index.js
,移除add.js
文件引用export default function () { console.log('rm console loader test=='); return 'hello index file.......'; }
本次更新后得到的
[id]-[hash].hot-update.json
为:// 20221205152831 // http://localhost:8080/index.a75a200ec8e959f6ed40.hot-update.json { "c": [ "index" ], "r": [ ], "m": [ "./src/add.js" ] }
另外,webpack还会通过JSONP
方式请求http://localhost:8080/index.455e1dda0f8e3cbf9ba0.hot-update.js
,3b102885936c5d7de6d5
为oldhash,对应的返回为待更新模块的更新后的chunk代码;
self["webpackHotUpdatedemo"]("index",{
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ ((__unused_webpack_module, __unused_webpack___webpack_exports__, __webpack_require__) => {
eval("/* harmony import */ var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./add */ \"./src/add.js\");\n\r\n\r\n/* harmony default export */ function __WEBPACK_DEFAULT_EXPORT__() {\r\n console.log('rm console loader test==', (0,_add__WEBPACK_IMPORTED_MODULE_0__.addByBit)(1,2));\r\n return 'hello index file.';\r\n}\n\n//# sourceURL=webpack://demo/./src/index.js?");
/***/ })
},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("8be9bd00cb51bd4d68f1")
/******/ })();
/******/
/******/ }
);
可以看到其内部调用了函数self["webpackHotUpdatedemo"]
,其定义如下,入参分别为chunkId
、moreModules
、runtime
,runtime
用于更新最新的文件hash,
webpack
的输出产物除了业务代码外,还有包括支持webpack模块化、异步模块加载、热更新等特性的支撑性代码,这些代码称为runtime
。
self["webpackHotUpdatedemo"] = (chunkId, moreModules, runtime) => {
/******/ for(var moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ currentUpdate[moduleId] = moreModules[moduleId];
/******/ if(currentUpdatedModulesList) currentUpdatedModulesList.push(moduleId);
/******/ }
/******/ }
/******/ if(runtime) currentUpdateRuntime.push(runtime);
/******/ if(waitingUpdateResolves[chunkId]) {
/******/ waitingUpdateResolves[chunkId]();
/******/ waitingUpdateResolves[chunkId] = undefined;
/******/ }
/******/ };
8. module.hot.apply
通过webpack/lib/hmr/JavascriptHotModuleReplacement.runtime.js
中的相关逻辑做热更新;
-
首先需要删除过期的模块
具体通过webpack/lib/hmr/JavascriptHotModuleReplacement.runtime.js
中的dispose
方法实现 -
将新的模块添加到modules中,并更新模块代码
具体通过
webpack/lib/hmr/JavascriptHotModuleReplacement.runtime.js
中的apply
方法实现。insert new code
run new runtime modules
call accept handlers
Load self accepted modules
参考文献
- 轻松理解webpack热更新原理