这里首先会启动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");}
};
启动本地服务后,会在入口动态新增两个文件入口
并一同打包到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()}
});
监听文件变化主要通过setupDevMiddleware
方法,底层主要是通过webpack-dev-middleware
,调用了compiler.watch
方法监听文件的变化;
当文件变化时,通过memory-fs
库将打包后的文件写入内存,而不是\dist
目录;
其实就是因为
webpack-dev-server
只负责启动服务和前置准备工作
,所有文件相关的操作
都抽离到webpack-dev-middleware
库了,主要是本地文件的编译和输出以及监听;
Compiler
支持可以监控文件系统
的 监听(watching) 机制,并且在文件修改时重新编译
。 当处于监听模式(watch mode)时,compiler
会触发诸如watchRun
,watchClose
和invalid
等额外的事件。
通过webpack-dev-server/lib/Server.js
中的setupHooks()
方法监听webpack编译完成
;主要是通过done
钩子监听到当次compilation
编译完成时,触发done
回调并调用sendStats
发送socket消息ok
和hash
事件
上面讲到,当次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
相关,接下来我们看看其作用
如下所示,我们可以看到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 APIactive: 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 statesbreak;}},// Management APIcheck: 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 calldata: currentModuleData[moduleId]};currentChildModule = undefined;return hot;}
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;
/******/ }
/******/ };
通过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