使用热更新小例子
// dev-server.js
const HtmlWebpackPlugin = require("html-webpack-plugin")
const webpack = require("webpack")
const webpackDevServer = require('webpack-dev-server')
const path = require('path')
const config = {
mode: 'development',
entry: [
'./src/index.js',
'webpack/hot/dev-server.js',
'webpack-dev-server/client/index.js?hot=true'
],
devtool: 'inline-source-map',
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
title: 'hot module replacement'
})
],
output: {
filename: '[name].[hash:4].[contenthash:8].js',
path: path.resolve(__dirname, 'dist'),
clean: true
}
};
const compiler = webpack(config);
const server = new webpackDevServer({ hot: false, client: false }, compiler);
(async () => {
await server.start()
console.log('start...')
})();
// ./src/index.js
import print from "./print.js"
function component() {
const btn = document.createElement('button')
btn.innerText = 'click'
btn.onclick = print
return btn
}
const elem = component()
document.body.appendChild(elem)
if (module.hot) {
module.hot.accept('./print.js', function () {
console.log('update print.js...')
})
}
// ./src/print.js
export default function print() {
console.log('print')
}
本例子摘取webpack官网,稍加改动
node dev-server.js就可以启动了
简略版webpack-dev-server内部
const getExpress = memoize(() => require("express"));
class Server {
constructor(options = {}, compiler) {
this.compiler = compiler;
this.options = options;
this.currentHash = undefined;
}
async start() {
await this.initialize();
}
async initialize() {
if (this.options.webSocketServer) {
const compilers = this.compiler.compilers || [this.compiler];
compilers.forEach((compiler) => {
this.addAdditionalEntries(compiler);
const webpack = compiler.webpack || require("webpack");
if (this.options.hot) {
const HMRPluginExists = compiler.options.plugins.find(
(p) => p && p.constructor === webpack.HotModuleReplacementPlugin,
);
if (HMRPluginExists) {
this.logger.warn(
`"hot: true" automatically applies HMR plugin, you don't have to add it manually to your webpack configuration.`,
);
} else {
// Apply the HMR plugin
const plugin = new webpack.HotModuleReplacementPlugin();
plugin.apply(compiler);
}
}
});
}
this.setupHooks();
this.setupApp();
this.createServer();
if (this.options.webSocketServer) {
this.createWebSocketServer()
}
}
addAdditionalEntries(compiler) {
const additionalEntries = [];
// ...
if (this.options.client && ...) {
additionalEntries.push(`${require.resolve("../client/index.js")}?${webSocketURLStr}`);
}
if (this.options.hot === "only") {
additionalEntries.push(require.resolve("webpack/hot/only-dev-server"));
} else if (this.options.hot) {
additionalEntries.push(require.resolve("webpack/hot/dev-server"));
}
const webpack = compiler.webpack || require("webpack");
for (const additionalEntry of additionalEntries) {
new webpack.EntryPlugin(compiler.context, additionalEntry, {
name: undefined,
}).apply(compiler);
}
}
setupHooks() {
this.compiler.hooks.done.tap("webpack-dev-server", (stats) => {
if (this.webSocketServer) {
this.sendStats(this.webSocketServer.clients, this.getStats(stats));
}
this.stats = stats;
}
);
}
setupApp() {
this.app = (new getExpress())();
}
createServer() {
const { type, options } = this.options.server
this.server = require((type)).createServer(
options,
this.app,
);
(this.server).on("connection", (socket) => {
this.sockets.push(socket)
socket.once("close", () => {
this.sockets.splice(this.sockets.indexOf(socket), 1)
})
}
);
}
createWebSocketServer() {
this.webSocketServer = new (this.getServerTransport())(this);
(this.webSocketServer).implementation.on("connection", (client) => {
if (!this.stats) {
return;
}
this.sendStats([client], this.getStats(this.stats), true);
}
);
}
sendStats(clients, stats, force) {
this.currentHash = stats.hash;
this.sendMessage(clients, "hash", stats.hash);
this.sendMessage(clients, "ok");
}
sendMessage(clients, type, data, params) {
for (const client of clients) {
if (client.readyState === 1) {
client.send(JSON.stringify({ type, data, params }));
}
}
}
getStats(statsObj) {
const stats = Server.DEFAULT_STATS;
return statsObj.toJson(stats);
}
}
总结上述代码
Server的参数可以在上个例子中得知
首先调用start方法,开始初始化,且调用HotModuleReplacementPlugin插件
在初始化过程中调用hooks,webpack在每次增量构建完成触发done并传递本次的统计信息stats,如果有webSocketServer,则向当前连接的客户端集合发送本次统计信息对象stats
接着构建app、server、websocketServer搭建服务器,用于向客户端发送信息,比如热更新{ type: "hash", data: "abcs..." },发送方法位于sendMessage
简略版webpack-dev-server下的client
var client = null;
var socket = function initSocket(url, handlers, reconnect) {
client = new WebSocketClient(url);
client.onOpen(function () {
retries = 0;
if (typeof reconnect !== "undefined") {
maxRetries = reconnect;
}
});
client.onClose(function () {
if (retries === 0) {
handlers.close();
}
client = null;
if (retries < maxRetries) {
var retryInMs = 1000 * Math.pow(2, retries) + Math.random() * 100;
retries += 1;
setTimeout(function () {
socket(url, handlers, reconnect);
}, retryInMs);
}
});
client.onMessage(function (data) {
var message = JSON.parse(data);
if (handlers[message.type]) {
handlers[message.type](message.data, message.params);
}
});
};
var status = {
isUnloading: false,
currentHash: typeof __webpack_hash__ !== "undefined" ? __webpack_hash__ : ""
};
var onSocketMessage = {
ok: function ok() {
sendMessage("Ok");
reloadApp(options, status);
},
hash: function hash(_hash) {
status.previousHash = status.currentHash;
status.currentHash = _hash;
},
// ...
};
var socketURL = createSocketURL(parsedResourceQuery);
socket(socketURL, onSocketMessage, options.reconnect);
在Server.js中sendStats有一行代码 t h i s . s e n d M e s s a g e ( c l i e n t s , " o k " ) ; this.sendMessage(clients, "ok"); this.sendMessage(clients,"ok");这样会触发onSocketMessage的ok方法,由此触发reloadApp方法
当每次构建完触发done钩子函数,服务端向客户端发送hash事件
简略版reloadApp
function reloadApp(_ref, status) {
var hot = _ref.hot, liveReload = _ref.liveReload;
function applyReload(rootWindow, intervalId) {
clearInterval(intervalId);
rootWindow.location.reload();
}
var search = self.location.search.toLowerCase();
var allowToHot = search.indexOf("webpack-dev-server-hot=false") === -1;
var allowToLiveReload = search.indexOf("webpack-dev-server-live-reload=false") === -1;
if (hot && allowToHot) {
hotEmitter.emit("webpackHotUpdate", status.currentHash);
} else if (liveReload && allowToLiveReload) {
var rootWindow = self;
var intervalId = self.setInterval(function () {
if (rootWindow.location.protocol !== "about:") {
applyReload(rootWindow, intervalId);
} else {
rootWindow = rootWindow.parent;
if (rootWindow.parent === rootWindow) {
applyReload(rootWindow, intervalId);
}
}
});
}
}
允许热更新则触发webpackHotUpdate事件,该事件在 a d d i t i o n a l E n t r i e s . p u s h ( r e q u i r e . r e s o l v e ( " w e b p a c k / h o t / d e v − s e r v e r " ) ) additionalEntries.push(require.resolve("webpack/hot/dev-server")) additionalEntries.push(require.resolve("webpack/hot/dev−server"))引入的模块中注册过
webpack的hot文件下dev-server
if (module.hot) {
var lastHash;
var upToDate = function upToDate() {
return lastHash.indexOf(__webpack_hash__) >= 0;
};
var check = function check() {
module.hot
.check(true)
.then(function (updatedModules) {
if (!updateModules) {
log("warning","[HMR] Cannot find update. " + (typeof window !== "undefined" ? "Need to do a full reload!" : "Please reload manually!"));
return;
}
if (!upToDate()) {
check();
}
})
};
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function (currentHash) {
lastHash = currentHash;
if (!upToDate() && module.hot.status() === "idle") check();
});
}
status 为 'idle'表示该进程正在等待调用 check
module.hot.check 会触发检查当前模块及其依赖模块是否有更新。如果有更新,webpack 将尝试进行热替换,将新的模块代码应用到运行中的应用程序,从而实现无需刷新页面即可更新代码
接收到客户端发来的webpackHotUpdate信息,更新哈希值,并判断是否更新并且当前状态在等待check
HotModuleReplacementPlugin
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;
});
}
});
});
});
});
}
function accept (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;
}
}
function internalApply(options) {
options = options || {};
var results = currentUpdateApplyHandlers.map(function (handler) {
return handler(options);
});
currentUpdateApplyHandlers = undefined;
results.forEach(function (result) {
if (result.dispose) result.dispose();
});
var outdatedModules = [];
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]);
}
}
}
});
return Promise.all([...]).then(function () {
return setStatus("idle").then(function () {
return outdatedModules;
});
});
}
$hmrDownloadManifest$在其他函数中替换为RuntimeGlobals.hmrDownloadManifest
// RuntimeGlobals.js
// function downloading the update manifest
exports.hmrDownloadManifest = "__webpack_require__.hmrM";
// array with handler functions to download chunk updates
exports.hmrDownloadUpdateHandlers = "__webpack_require__.hmrC";
// object with all hmr module data for all modules
exports.hmrModuleData = "__webpack_require__.hmrD";
Template.asString([`${RuntimeGlobals.hmrDownloadManifest} = ${runtimeTemplate.basicFunction("", [
'if (typeof fetch === "undefined") throw new Error("No browser support: need fetch API");',
`return fetch(${RuntimeGlobals.publicPath} + ${
RuntimeGlobals.getUpdateManifestFilename
}()).then(${runtimeTemplate.basicFunction("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();"
])});`
])};`
])
总结
首先内部向入口额外添加HotModuleReplacementPlugin、创建客户端webSocket和注册webpackHotUpdate监听事件
接着使用done钩子在每次构建完向客户端发送hash和ok事件
HotModuleReplacementPlugin会注入一段代码:hmrDownloadManifest、hmrDownloadUpdateHandlers、check等
接着创建服务端,并在connection事件中记录与之连接的客户端,然后发送hash和ok消息,客户端接受到后调用reloadApp方法,并触发webpackHotUpdate事件,执行该事件的回调函数,执行check方法,其方法包括module.hot.check
module.hot.check方法去请求增量文件,然后下载更新的chunk,执行module.hot.accept代码实现无刷新更新
对于后续文件更新,首先对文件进行监听,有改动则触发webpack重新编译,之后触发done钩子