- 模块:项目中使用的每个文件
- 通过互相引用,这些模块会形成一个图(ModuleGraph)数据结构
- 在打包过程中,模块被合并成chunk,chunk合并成chunk组
- 一个chunk组中可能有多个chunk。例如,splitChunksPlugin会将一个chunk组拆分为一个或多个chunk
模块热替换(HRM)
模块热替换(HMR-hot module replacement)功能会在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面。
- HRM Server是服务端,用来将变化的js模块通过websocket的消息通知给浏览器端。
- HRM Runtime是浏览器端,用于接收HMR Server传递的模块数据,浏览器可以看到.hot-update.json的文件过来。
HotModuleReplacementPlugin是做什么用的
- webpack构建出来的bundle.js本身是不具备热更新的能力的,HotModuleReplacementPlugin的作用就是将HMR runtime注入到bundle.js,使得bundle.js可以和HMR server建立websocket的通信连接。
- webpack.HotModuleReplacementPlugin没有必要加,配置了hot:true会自动引入这个plugin
webpack-dev-server和hot-module-replacement-plugin之间的关系
- webpack-dev-server(WDS)的功能提供bundle server的能力,就是生成的bundle.js文件可以通过
localhost://***
的方式去访问,另外WDS也提供livereload(浏览器的自动刷新)。 - hot-module-replacement-plugin的作用是提供HMR的runtime,并且将runtime注入到bundle.js代码里面去。一旦磁盘里面的文件修改,那么HMR server会将有修改的js module信息放送给HMR runtime,然后HMR runtime去局部更新页面的代码。因此这种方式可以不用刷新浏览器。
- 单独写两个包也是出于功能的解耦来考虑的。简单来说:hot-module-replacement-plugin包给webpack-dev-server提供了热更新的能力。
源码阅读
-
源码版本
webpack-dev-server 5.0.4
webpack 5.91.0
-
启动 HMR Server
这个工作主要是在webpack-dev-server中完成
看webpack-dev-server/lib/Server.js文件,下面的express服务实际上对应的是Bundle Server
js
const getExpress = memoize(() => require("express"));
/**
* @private
* @returns {void}
*/
setupApp() {
/** @type {import("express").Application | undefined}*/
this.app = new /** @type {any} */ (getExpress())();
}
debug调用栈: 启动服务结束之后就通过createSocketServer创建websocket服务
js
createWebSocketServer() {
/** @type {WebSocketServerImplementation | undefined | null} */
this.webSocketServer = new /** @type {any} */ (this.getServerTransport())(
this,
);
// ...
}
js
getServerTransport() {
let implementation;
let implementationFound = true;
switch (
typeof (
/** @type {WebSocketServerConfiguration} */
(this.options.webSocketServer).type
)
) {
case "string":
// Could be 'sockjs', in the future 'ws', or a path that should be required
if (
/** @type {WebSocketServerConfiguration} */ (
this.options.webSocketServer
).type === "sockjs"
) {
implementation = require("./servers/SockJSServer");
} else if (
/** @type {WebSocketServerConfiguration} */ (
this.options.webSocketServer
).type === "ws"
) {
implementation = require("./servers/WebsocketServer");
} else {
try {
// eslint-disable-next-line import/no-dynamic-require
implementation = require(
/** @type {WebSocketServerConfiguration} */ (
this.options.webSocketServer
).type,
);
} catch (error) {
implementationFound = false;
}
}
break;
case "function":
implementation = /** @type {WebSocketServerConfiguration} */ (
this.options.webSocketServer
).type;
break;
default:
implementationFound = false;
}
if (!implementationFound) {
throw new Error(
"webSocketServer (webSocketServer.type) must be a string denoting a default implementation (e.g. 'ws', 'sockjs'), a full path to " +
"a JS file which exports a class extending BaseServer (webpack-dev-server/lib/servers/BaseServer.js) " +
"via require.resolve(...), or the class itself which extends BaseServer",
);
}
return implementation;
}
debug调用栈:
在webpack-dev-server/client/index.js中,接收到hash消息后,更新hash;接收ok消息,执行reloadApp
js
var onSocketMessage = {
// ....
/**
* @param {string} hash
*/
hash: function hash(_hash) {
status.previousHash = status.currentHash;
status.currentHash = _hash;
},
// ....
ok: function ok() {
sendMessage("Ok");
if (options.overlay) {
overlay.send({
type: "DISMISS"
});
}
reloadApp(options, status);
},
// ...
}
webpack-dev-server/client/utils/reloadApp.js中的reloadApp,这里利用node.js的EventEmitter,发出webpackHotUpdate消息。这里又将更新的事情给回了webpack(为了更好的维护代码,以及职责划分的更明确)
js
import hotEmitter from "webpack/hot/emitter.js";
import { log } from "./log.js";
/** @typedef {import("../index").Options} Options
/** @typedef {import("../index").Status} Status
/**
* @param {Options} options
* @param {Status} status
*/
function reloadApp(_ref, status) {
// ...
if (hot && allowToHot) {
log.info("App hot update...");
hotEmitter.emit("webpackHotUpdate", status.currentHash);
if (typeof self !== "undefined" && self.window) {
// broadcast update to window
self.postMessage("webpackHotUpdate".concat(status.currentHash), "*");
}
}
// allow refreshing the page only if liveReload isn't disabled
else if (liveReload && allowToLiveReload) {
// ...
}
}
export default reloadApp;
在webpack的webpack/hot/dev-server.js中,监听webpackUpdate事件,并执行check方法。并在check方法中调用module.hot.check方法进行热更新。
js
// 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();
}
});
log("info", "[HMR] Waiting for update signal from WDS...");
js
var check = function check() {
module.hot
.check(true)
.then(function (updatedModules) {
// ....
})
.catch(function (err) {
// ...
});
};
module.hot.check,通过HotModuleReplacementPlugin已经注入到我们chunk中了(也就是上面说的HMR Runtime),所以后面就是它如何更新bundle.js
见下面这段打包后的代码: // webpack/hot/dev-server.js中的module.hot.check(true)中的module就是通过参数传递过来的
js
/***/
"./node_modules/.pnpm/webpack@5.91.0_webpack-cli@5.1.4/node_modules/webpack/hot/dev-server.js": /*!****************************************************************************************************!*\
!*** ./node_modules/.pnpm/webpack@5.91.0_webpack-cli@5.1.4/node_modules/webpack/hot/dev-server.js ***!
\****************************************************************************************************/
/***/
((module,__unused_webpack_exports,__webpack_require__)=>{
eval(" ... 这部分是webpack/hot/dev-server.js文件源码 //# sourceURL=webpack://ts-animates-webpack-demo/./node_modules/.pnpm/webpack@5.91.0_webpack-cli@5.1.4/node_modules/webpack/hot/dev-server.js?");
/***/
}
),
debug调用栈:
HMR Runtime中更新bundle.js
开启热更新后,打包生成的代码会比不开启多出很多的东西。
打包后的代码中新增了一个createModuleHotObject
js
module.hot = createModuleHotObject(options.id, module);
这个函数就是用来返回一个hot对象,所以调用module.hot.check的时候,实际就是执行hotCheck函数
js
function createModuleHotObject(moduleId, me) {
var _main = currentChildModule !== moduleId;
var hot = {
// ...
// Module API
active: true,
accept: function (dep, callback, errorHandler)
// ...
},
decline: function (dep) {
// ..
},
dispose: function (callback) {
hot._disposeHandlers.push(callback);
},
addDisposeHandler: function (callback) {
hot._disposeHandlers.push(callback);
},
removeDisposeHandler: function (callback) {
// ...
},
invalidate: function () {
// ...
},
// Management API
check: hotCheck,
apply: hotApply,
// ...
return hot;
}
hotCheck中调用了__webpack_require__.hmrM
js
function hotCheck(applyOnUpdate) {
// ...
return setStatus("check")
.then(__webpack_require__.hmrM)
.then(function (update) {
if (!update) {
// ...
}
return setStatus("prepare").then(function () {
var updatedModules = [];
currentUpdateApplyHandlers = [];
return Promise.all(
// 会调用__webpack_require__.hmrC.jsonp
// __webpack_require__.hmrC.jsonp 函数中会执行loadUpdateChunk
Object.keys(__webpack_require__.hmrC).reduce(function (
promises,
key
) {
__webpack_require__.hmrC[key](
update.c,
update.r,
update.m,
promises,
currentUpdateApplyHandlers,
updatedModules
);
return promises;
}, [])
).then(function () {
// ...
});
});
});
}
__webpack_require__.hmrM
加载.hot-update.json
webpack_reuqire .p指的是本地服务的域名,类似http://0.0.0:300 webpack_require.hmrF去获取.hot-update.json文件的地址
js
__webpack_require__.hmrM = () => {
if (typeof fetch === "undefined") throw new Error("No browser support: need fetch API");
return fetch(__webpack_require__.p + __webpack_require__.hmrF()).then((response) => {
if(response.status === 404) return; // no update available
if(!response.ok) throw new Error("Failed to fetch update manifest " + response.statusText);
return response.json();
});
};
js
/* webpack/runtime/get update manifest filename */
(() => {
__webpack_require__.hmrF = () => ("runtime." + __webpack_require__.h() + ".hot-update.json");
})();
加载要更新的模块
js
function loadUpdateChunk(chunkId, updatedModulesList) {
currentUpdatedModulesList = updatedModulesList;
return new Promise((resolve, reject) => {
waitingUpdateResolves[chunkId] = resolve;
// start update chunk loading
var url = __webpack_require__.p + __webpack_require__.hu(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
var loadingEnded = (event) => {
// ...
};
__webpack_require__.l(url, loadingEnded);
});
}
用上一次的hash请求json和js文件。
__webpack_require__.l
类似JSONP的方式进行,因为JSONP获取的代码可以直接执行
js
__webpack_require__.l = (url, done, key, chunkId) => {
// ...
if(!script) {
needAttach = true;
script = document.createElement('script');
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.setAttribute("data-webpack", dataWebpackPrefix + key);
script.src = url;
}
inProgress[url] = [done];
var onScriptComplete = (prev, event) => {
// ...
}
var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
needAttach && document.head.appendChild(script);
};
请求http://***/runtime.9e7296ebc5ffdc0597f0.hot-update.json
的返回内容
js
{
"c": [
"main",
"runtime"
],
"r": [],
"m": []
}
请求 http://***/main.9e7296ebc5ffdc0597f0.hot-update.js
返回的内容。
js
self["webpackHotUpdatets_animates_webpack_demo"]("main", {
/***/
"./node_modules/.pnpm/vue-loader@17.4.2_vue@3.4.25_webpack@5.91.0/node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[2]!./node_modules/.pnpm/vue-loader@17.4.2_vue@3.4.25_webpack@5.91.0/node_modules/vue-loader/dist/index.js??ruleSet[1].rules[9].use[0]!./src/pages/CssCenter.vue?vue&type=template&id=6b351e44&scoped=true":
/***/
((__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{
eval(" ... 这里是更新后的文件内容...//# sourceURL=webpack://ts-animates-webpack-demo/./src/pages/CssCenter.vue?./node_modules/.pnpm/vue-loader@17.4.2_vue@3.4.25_webpack@5.91.0/node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B2%5D!./node_modules/.pnpm/vue-loader@17.4.2_vue@3.4.25_webpack@5.91.0/node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B9%5D.use%5B0%5D");
/***/
}
)
});
下面是webpackHotUpdatets_animates_webpack_demo函数的定义: 第一个参数是模块名,第二个是对象模块路径和内容,第三个是下次更新的hahs值。
js
self["webpackHotUpdatets_animates_webpack_demo"] = (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;
/******/ }
/******/ };
webpackHotUpdatets_animates_webpack_demo方法主要是设置要更新的模块路径和更新内容,随后执行loadUpdateChunk的resolve方法,表示资源获取完成。
执行完该方法后再次回到hotCheck的setStatus("prepare")方法的第二个then回调
第二个then回调中的代码如下:
js
return waitForBlockingPromises(function () {
/******/ if (applyOnUpdate) {
/******/ return internalApply(applyOnUpdate);
/******/ } else {
/******/ return setStatus("ready").then(function () {
/******/ return updatedModules;
/******/ });
/******/ }
/******/ });
紧接着执行internalApply方法
再执行applyHandler方法
首先主要是执行getAffectedModuleEffects方法
回调internalApply方法,执行result.apply方法。
apply方法用新拉取的模块替换__webpack_require__.m模块里面的旧模块。 并调用currentUpdateRuntime方法设置__webpack_require__.h的下一次更新的hash。再执行module.hot.accept
js
results.forEach(function (result) {
/******/ if (result.apply) {
/******/ var modules = result.apply(reportError);
/******/ if (modules) {
/******/ for (var i = 0; i < modules.length; i++) {
/******/ outdatedModules.push(modules[i]);
/******/ }
/******/ }
/******/ }
/******/ });
执行`module.hot.accept`
下面是vue中type=template部分的`module.hot.accept`实现,主要是重新执行render函数来实现的
```js
/* hot reload */
if (true) {
__exports__.__hmrId = "6b351e44"
const api = __VUE_HMR_RUNTIME__
module.hot.accept()
if (!api.createRecord('6b351e44', __exports__)) {
api.reload('6b351e44', __exports__)
}
module.hot.accept(/*! ./CssCenter.vue?vue&type=template&id=6b351e44&scoped=true */ "./src/pages/CssCenter.vue?vue&type=template&id=6b351e44&scoped=true", __WEBPACK_OUTDATED_DEPENDENCIES__ => { /* harmony import */ _CssCenter_vue_vue_type_template_id_6b351e44_scoped_true__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./CssCenter.vue?vue&type=template&id=6b351e44&scoped=true */ "./src/pages/CssCenter.vue?vue&type=template&id=6b351e44&scoped=true");
(() => {
api.rerender('6b351e44', _CssCenter_vue_vue_type_template_id_6b351e44_scoped_true__WEBPACK_IMPORTED_MODULE_0__.render)
})(__WEBPACK_OUTDATED_DEPENDENCIES__); })
}
执行完module.hot.accept
后,页面就渲染成最新的内容了
css的module.hot.accept
是style-loader实现的
源码地址:style-loader/dist/runtime/injectStylesIntoStyleTag.js
最终实现源码地址:style-loader/dist/runtime/styleTagTransform.js
js
/* istanbul ignore next */
function insertStyleElement(options) {
var element = document.createElement("style");
options.setAttributes(element, options.attributes);
options.insert(element, options.options);
return element;
}
module.exports = insertStyleElement;
js
function styleTagTransform(css, styleElement) {
if (styleElement.styleSheet) {
styleElement.styleSheet.cssText = css;
} else {
while (styleElement.firstChild) {
styleElement.removeChild(styleElement.firstChild);
}
styleElement.appendChild(document.createTextNode(css));
}
}
module.exports = styleTagTransform;
实现原理也就是创建一个style标签,再将该标签的cssText赋值新的css。