In the modern front-end development experience, the browser automatically completes the code update while maintaining the current page state after the code change, which has already become the standard configuration in many development tool chains, today we discuss the topic is the use and implementation principle of Webpack DevServer & HMR.
The basic use
Take a look at the following example:
// src/index.css
#app > div {
color:red;
font-size: 20px;
}
// src/app.js
export function setup(initValue = null) {
let appElement = document.getElementById('app');
let nameInputElement = document.createElement('input');
nameInputElement.type = 'text';
nameInputElement.placeholder = 'Please enter your name';
appElement.appendChild(nameInputElement);
let nameDisplayElement = document.createElement('div');
nameDisplayElement.innerHTML = 'Name:';
appElement.appendChild(nameDisplayElement);
nameInputElement.addEventListener('keyup'.(event) = > {
nameDisplayElement.innerHTML = ` name:${event.target.value}`;
});
if (initValue) {
nameInputElement.value = initValue;
nameDisplayElement.innerHTML = ` name:${initValue}`; }}// src/index.js
import './index.css';
import { setup } from './app';
setup();
Copy the code
In the code snippet above, we create a text input field and a div that displays the contents of the input field in real time, as shown below:
Let’s take a quick look at two ways Webpack can enable DevServer & HMR.
Direct configuration
Since webpack-cli comes with webpack-dev-server built-in, we can enable devServer & HMR directly by setting the devServer property for webpack configuration:
// webpack.config.js
module.exports = {
// Other configuration information......
entry: './src/index.js'.devServer: {
static: './dist'.port: 3000.hot: true,}};Copy the code
In the code snippet above, we set devServer to static (static resource root path), port (service port number), hot (HMR enabled), then run NPX webpack serve –open, wait for the browser to open, Try updating SRC /index.css or SRC /app.js and save. You will find that the browser automatically updates the code as follows:
Middleware
We can easily enable DevServer & HMR through direct configuration. Since webpack-CLI uses webpack-dev-server, Webpack-dev-middleware and Webpack-hot-middleware are used by webpack-dev-middleware, so in this section we use them directly to enable DevServer & HMR:
First run the following command to install the dependencies:
yarn add webpack-dev-middleware webpack-hot-middleware express --dev
# or npm install --save-dev webpack-dev-middleware webpack-hot-middleware express
Copy the code
Create./server.js and enter the following:
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler));
app.use(webpackHotMiddleware(compiler));
app.listen(config.devServer.port, function () {
console.log(`Project is running at: http://localhost:${config.devServer.port}\n`);
});
Copy the code
In the code snippet above, we use Express to implement the local server, first instantiating Express and then loading the webpack.config.js configuration to complete the compiler instantiation. We then inject Webpack-dev-middleware and Webpack-hot-middleware into Express Middleware, and finally listen to the development service port in the WebPack configuration to start the service.
Then modify webpack.config.js:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
// Other configuration information......
entry: [
'webpack-hot-middleware/client? path=/__webpack_hmr&timeout=20000'.'./src/index.js',].devServer: {
static: './dist'.port: 3000,},plugins: [
new webpack.HotModuleReplacementPlugin(),
],
};
Copy the code
Comparing the above code snippet with the webpack.config.js content from the previous section, you can see that there are several additional configurations:
- in
entry
addwebpack-hot-middleware/client? path=/__webpack_hmr&timeout=20000
; - delete
devServer
In thehot
Attribute (this attribute is redundant, so it is deleted); - in
plugins
addwebpack.HotModuleReplacementPlugin
.
After everything above is done, run Node. /server.js, then go to http://localhost:3000 and try updating SRC /index.css or SRC /app.js and save it. You’ll see exactly the same effect as in the previous section.
Local refresh
If you run any of the examples above, you’ll see the fact that when you update the JavaScript code, the browser chooses to refresh instead of leaving the page as it was when you updated the CSS. This is because CSS updates are stateless, meaning they just need to be replaced, but JavaScript updates involve maintaining all sorts of states that the Webpack HMR module doesn’t know how to handle, so it takes the most conservative action (refreshing the page) to complete the code update. If we want to implement a partial refresh of JavaScript, we need to manually reset the state. For the example in this article, we can add the following to SRC /index.js:
if (module.hot) {
module.hot.accept('./app.js'.function() {
console.log('Accepting the updated from ./app.js');
let initValue = null;
let appElement = document.getElementById('app');
while (appElement.firstChild) {
let child = appElement.lastChild;
if (child.nodeName.toLocaleLowerCase() === 'input') {
initValue = child.value;
}
appElement.removeChild(child);
}
setup(initValue);
});
}
Copy the code
The above code snippet, we first determine interface module. The hot is available (by HotModuleReplacementPlugin exposure), if available, we will through the module. The hot. Accept method to listen. / app. Js update module, In its callback, we first cache the value of the current input field through initValue, then remove all child nodes with the id app, and finally call the setup function to recreate the relevant nodes.
Run the example again in either of the above ways, enter something in the input box, modify./ SRC /app.js, and save. Now the page has been updated with the JavaScript code in the form of a partial refresh and maintains the page state as follows:
Of course, in addition to using the module. Hot outside, also can use the import meta. WebpackHot (can only be used in strict ESM), relevant details see webpack.docschina.org/api/hot-mod… , will not be elaborated here.
The principle of analysis
We introduced the basic usage of Webpack DevServer & HMR. In this section, we rely on to its webpack – dev – middleware, webpack – hot – middleware and HotModuleReplacementPlugin implementation principle were introduced simply.
webpack-dev-middleware
The main job of Webpack-dev-Middleware is to listen for module changes and respond to requests with the latest module content, and the core code looks like this (the code has been simplified to make it easier to understand) :
const path = require('path');
const memfs = require('memfs');
const mime = require("mime-types");
function getPaths(context) {
const { stats } = context;
return (stats.stats ? stats.stats : [stats]).map(({ compilation }) = > ({
outputPath: compilation.getPath(compilation.outputOptions.path),
publicPath: compilation.getPath(compilation.outputOptions.publicPath),
}));
}
function getFilenameFromRequest(context, req) {
const paths = getPaths(context);
const baseUrl = `${req.protocol}: / /${req.get('host')}`;
const url = new URL(req.url, baseUrl);
for (const { publicPath, outputPath } of paths) {
const publicPathUrl = new URL(publicPath, baseUrl);
if (url.pathname && url.pathname.startsWith(publicPathUrl.pathname)) {
let filename = outputPath;
const pathname = url.pathname.substr(publicPathUrl.pathname.length);
if (pathname) {
filename = path.join(outputPath, pathname);
}
let fileStats;
try {
fileStats = context.outputFileSystem.statSync(filename);
} catch (_) {
continue;
}
if (fileStats.isFile()) {
return filename;
}
if (fileStats.isDirectory()) {
filename = path.join(filename, 'index.html');
try {
fileStats = context.outputFileSystem.statSync(filename);
} catch (_) {
continue;
}
if (fileStats.isFile()) {
returnfilename; }}}}return undefined;
}
function ready(context, req, callback) {
if (context.isReady) {
callback(context.stats);
return;
}
const name = req && req.url || callback.name;
context.logger.info(`Wait until bundle finished${name ? ` :${name}` : ""}`);
context.callbacks.push(callback);
}
function main(compiler) {
/** * Context Settings */
const context = {
isReady: false.stats: null.callbacks: [].outputFileSystem: null.logger: compiler.getInfrastructureLogger('webpack-dev-middleware'),};/** * Memory file system Settings */
const memeoryFileSystem = memfs.createFsFromVolume(new memfs.Volume());
memeoryFileSystem.join = path.join.bind(path);
context.outputFileSystem = memeoryFileSystem;
compiler.outputFileSystem = memeoryFileSystem;
/** * Compiler hook set */
function invalid() {
if (context.isReady) {
context.logger.info('Compilation starting... ');
}
context.isReady = false;
context.stats = undefined;
}
compiler.hooks.watchRun.tap('webpack-dev-middleware', invalid);
compiler.hooks.invalid.tap('webpack-dev-middleware', invalid);
compiler.hooks.done.tap('webpack-dev-middleware'.(stats) = > {
context.stats = stats;
context.isReady = true;
process.nextTick(() = > {
if(! context.isReady) {return;
}
context.logger.info('Compilation finished');
const callbacks = context.callbacks;
context.callbacks = [];
callbacks.forEach(callback= > callback(context.stats))
});
});
/** * Enable listening */
const watchOptions = compiler.options.watchOptions || {};
compiler.watch(watchOptions, (error) = > {
if(error) { context.logger.error(error); }});/** * Express middleware */
return async function(req, res, next) {
const method = req.method;
if (['GET'.'HEAD'].indexOf(method) === -1) {
await next();
return;
}
ready(context, req, async() = > {const filename = getFilenameFromRequest(context, req);
if(! filename) {await next();
return;
}
const contentType = mime.contentType(path.extname(filename));
if (contentType) {
res.setHeader('Content-Type', contentType);
}
res.send(context.outputFileSystem.readFileSync(filename));
});
};
};
module.exports = main;
Copy the code
In the main function, we do the following things:
-
Define the variable context to set the context;
-
In the development environment, general use memory to store the resources after the packaging, so here we create memeoryFileSystem memory file system through memfs module, and assign it to the compiler. OutputFileSystem, In this way, Webpack stores packaged resources in memory.
-
Then listen for the watchRun, invalid, and done hooks of the Compiler.
- in
watchRun
及invalid
In the callback, resetcontext.isReady
及context.stats
; - in
done
Is set firstcontext.isReady
及context.stats
Value, and then callprocess.nextTick
To delay triggeringcontext
Callback, which is delayed because if the resource has changed at this point, thencontext
Resources retrieved in the callback may be invalid and fired in the next taskcontext
Callback to avoid resource invalidation.
- in
-
After setting the compiler’s hook function listening, the Compiler. Watch method is called to start the Webpack process in listening mode.
-
Finally, the middleware that handles the packaged resource request is returned according to the express custom Middleware format.
In a concrete implementation of Express Middleware:
-
If it is not a GET or HEAD request, then call next method directly to hand the request to other middleware for processing, otherwise go to the next step;
-
Call ready and get the requested resource’s path (filename) by calling getFilenameFromRequest in the callback, then set the Content-Type header and send the file contents to the requester. This among them:
- in
ready
In the function, ifcontext.isReady
The value oftrue
, call the callback directly, otherwise add it tocontext.callbacks
In order to incompiler.done
The hook’s callback is triggered. - in
getFilenameFromRequest
Function, we first calculate the path of the requested resource, if the path exists and is a file, directly return; If the path exists and is a directory, matches those under the pathindex.html
If yes, return to the pathindex.html
, otherwise returnsundefined
.
- in
In this section we take a brief look at the core implementation of Webpack-dev-Middleware and summarize the flow as follows:
- Set Webpack’s file system to memory file system;
- Listening to the
watchRun
,invalid
及done
A hook; - Run Webpack’s packaging process in listening mode;
- through
express middleware
Intercepts and responds to resource requests.
webpack-hot-middleware
Webpack-hot-middleware’s main job is to listen for changes in modules and push them to clients, who then replace them. Unlike Webpack-dev-middleware, Webpack-hot-middleware has both server and client components, and the core implementation of webpack-hot-middleware is dissected below (the code has been minimized for ease of understanding).
The service side
function createEventStream() {
let clientId = 0;
let clients = {};
function everyClient(callback) {
Object.keys(clients).forEach(id= > callback(clients[id]));
}
return {
handler: (req, res) = > {
const headers = {
'Access-Control-Allow-Origin': The '*'.'Content-Type': 'text/event-stream; charset=utf-8'.'Cache-Control': 'no-cache, no-transform'.// While behind nginx, event stream should not be buffered:
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
'X-Accel-Buffering': 'no'};constisHttp1 = ! (parseInt(req.httpVersion) >= 2);
if (isHttp1) {
req.socket.setKeepAlive(true);
Object.assign(headers, {
'Connection': 'keep-alive'}); } res.writeHead(200, headers);
res.write('\n');
const id = clientId++;
clients[id] = res;
req.on('close'.function () {
if(! res.finished) { res.end() };delete clients[id];
});
},
publish: (payload) = > {
everyClient(client= > {
client.write('data: ' + JSON.stringify(payload) + '\n\n'); }); }}; };function publishStats(action, context) {
const stats = context.stats.toJson({
all: false.cached: true.children: true.modules: true.timings: true.hash: true}); [stats.children && stats.children.length ? stats.children: [stats]].forEach(() = > {
context.logger.info(`Webpack built ${stats.hash} in ${stats.time} ms`);
context.eventStream.publish({
action,
time: stats.time,
hash: stats.hash,
warnings: stats.warnings || [],
errors: stats.errors || [],
modules: stats.modules.reduce((result, moduleItem) = > ({
...result,
[moduleItem.id]: moduleItem.name,
}), {}),
});
});
}
function main(compiler) {
/** * Context Settings */
const context = {
stats: null.path: '/__webpack_hmr'.eventStream: createEventStream(),
logger: compiler.getInfrastructureLogger('webpack-hot-middleware'),};/** * Compiler hook set */
compiler.hooks.invalid.tap('webpack-hot-middleware'.() = > {
context.stats = null;
context.logger.info('Webpack building... ');
context.eventStream.publish({ action: 'building' });
});
compiler.hooks.done.tap('webpack-hot-middleware'.(stats) = > {
context.stats = stats;
publishStats('built', context);
});
/** * Express middleware */
return function(req, res, next) {
const url = new URL(req.url, `${req.protocol}: / /${req.get('host')}`);
if(url.pathname ! == context.path) {return next();
}
context.eventStream.handler(req, res);
if (context.stats) {
publishStats('sync', context); }}}module.exports = main;
Copy the code
In the main function, we do the following things:
-
Define the variable context to set the context. Note the implementation of createEventStream, which uses the EventSource for server push.
-
Then listen for the invalid and done hooks of the Compiler, where:
- in
invalid
In the callback, resetcontext.stats
And push it to the clientbuilding
Events; - in
done
In the callback, setcontext.stats
And push it to the clientbuilt
Events.
- in
-
In an implementation of Express Middleware:
- If requested
pathname
Don’t for/__webpack_hmr
, then directly callnext
Method to pass the request to other middleware for processing, otherwise proceed to the next step; - through
context.eventStream.handler
The call converts the current request toEventSource
Long links to maintain long-term communication with clients; - Then check whether it is set
context.stats
If the value is met, it will be pushed to the clientsync
Events.
- If requested
The client
By analyzing the implementation of the server, it can be seen that the server needs to push events to the client, and the client naturally needs to listen for related events and process them. The core logic of the client is as follows:
let lastHash;
function upToDate(hash) {
if (hash) {
lastHash = hash;
}
return lastHash == __webpack_hash__;
}
function applyCallback(err) {
if (err) {
console.warn(`[HMR] Update check failed: ${err.stack || err.message}`);
return;
}
if (!upToDate()) {
checkServer();
}
}
const applyOptions = {
ignoreUnaccepted: true.ignoreDeclined: true.ignoreErrored: true.onUnaccepted: (data) = > {
console.warn(`Ignored an update to unaccepted module ${data.chain.join('- >')}`);
},
onDeclined: (data) = > {
console.warn(`Ignored an update to declined module ${data.chain.join('- >')}`);
},
onErrored: (data) = > {
console.error(data.error);
console.warn(`Ignored an error while updating module ${data.moduleId} (${data.type}) `); }};function checkServer() {
const checkCallback = (err) = > {
if (err) {
console.warn(`[HMR] Update check failed: ${err.stack || err.message}`);
return;
}
module.hot.apply(applyOptions, applyCallback)
.then(_= > applyCallback(null))
.catch(applyCallback);
};
module.hot.check(false, checkCallback)
.then(_= > checkCallback(null))
.catch(checkCallback);
}
function parseMessage(message) {
switch (message.action) {
case 'building':
console.log('[HMR] bundle rebuilding');
break;
case 'built':
console.log(`[HMR] bundle ${message.hash} rebuilt in ${message.time} ms`);
case 'sync':
if(! upToDate(message.hash) &&module.hot.status() === 'idle') {
console.log('[HMR] Checking for updates on the server... ');
checkServer();
}
break;
default:
console.error(`[HMR] unknown message action:${message.action}`);
break; }}function connect() {
const source = new window.EventSource('/__webpack_hmr');
source.onopen = () = > {
console.log('[HMR] connected');
};
source.onerror = () = > {
source.close();
};
source.onmessage = (event) = > {
try {
parseMessage(JSON.parse(event.data));
} catch (error) {
console.warn('Invalid HMR message: ' + event.data + '\n'+ error); }}; } connect();Copy the code
- in
connect
Function, we mainly useEventSource
The path to the server is/__webpack_hmr
Request to create a long link and then inonmessage
Is called in a callback to theparseMessage
Function to process the information sent by the server; - in
parseMessage
Function, if the message type issync
In the message,hash
With the current latesthash
Is not consistent andmodule.hot.status
The return value ofidle
, the callcheckServer
Functions; - in
checkServer
Function, we do it by callingmodule.hot.check
And called in its callbackmodule.hot.apply
To complete the module update.
summary
In this section, we briefly analyze the core implementation of Webpack-hot-Middleware. Since the module update requires the cooperation of the server and the client, its implementation is divided into two parts: server and client:
- On the server, use
express middleware
Intercept path is/__webpack_hmr
Request to convert it to a long link so thatinvalid
、done
After the hook is triggered, relevant events can be pushed to the client. - In the client, use
EventSource
Establish a long link with the server and listenonmessage
Event, which parses the message after receiving it to complete the module update operation.
HotModuleReplacementPlugin
As mentioned earlier, in webpack – hot – middleware in the client code, we call the module. The relevant methods in the hot, these methods by HotModuleReplacementPlugin injection, in this section, we to the brief analysis of its implementation.
Check HotModuleReplacementPlugin implementation, in the apply method, HotModuleReplacementPlugin done through listening compiler.hooks.com pilation hooks to support dynamic replace logic module Settings.
Depend on the set
if(compilation.compiler ! == compiler)return;
// Add a dependency to the module.hot.accept interface
compilation.dependencyFactories.set(
ModuleHotAcceptDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
ModuleHotAcceptDependency,
new ModuleHotAcceptDependency.Template()
);
// Omit other module.hot.* interface dependency setup codes here
/ / add the import. Meta. WebpackHot. Rely on the accept interface
compilation.dependencyFactories.set(
ImportMetaHotAcceptDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
ImportMetaHotAcceptDependency,
new ImportMetaHotAcceptDependency.Template()
);
// Omit other import.meta. WebpackHot.* interface dependency setup code
Copy the code
In the code snippet above:
- Judge the present first
compilation
Subordinate to thecompiler
Whether or notapply
methodscompiler
If the arguments are equal, return if they are not, otherwise continue (because this plug-in should not affect the execution of the subcompilation); - Then through
compilation.dependencyTemplates.set
Call separate Settingsmodule.hot.*
及import.meta.webpackHot.*
Interface dependencies (used in theseal
Phase generates relevant code);
compilation.hooks.record
Update some properties in records by listening on the Compilation.hooks. Record hook:
let hotIndex = 0;
const fullHashChunkModuleHashes = {};
const chunkModuleHashes = {};
compilation.hooks.record.tap(
"HotModuleReplacementPlugin".(compilation, records) = > {
if (records.hash === compilation.hash) return;
const chunkGraph = compilation.chunkGraph;
records.hash = compilation.hash;
records.hotIndex = hotIndex;
records.fullHashChunkModuleHashes = fullHashChunkModuleHashes;
records.chunkModuleHashes = chunkModuleHashes;
records.chunkHashes = {};
records.chunkRuntime = {};
for (const chunk of compilation.chunks) {
records.chunkHashes[chunk.id] = chunk.hash;
records.chunkRuntime[chunk.id] = getRuntimeKey(chunk.runtime);
}
records.chunkModuleIds = {};
for (const chunk of compilation.chunks) {
records.chunkModuleIds[chunk.id] = Array.from(
chunkGraph.getOrderedChunkModulesIterable(
chunk,
compareModulesById(chunkGraph)
),
m= >chunkGraph.getModuleId(m) ); }});Copy the code
compilation.hooks.fullHash
By listening to the compilation. Hooks. FullHash hooks (the runtime is added after the trigger) to calculate what the module has changed and store it into updatedModules:
const updatedModules = new TupleSet();
const fullHashModules = new TupleSet();
const nonCodeGeneratedModules = new TupleSet();
compilation.hooks.fullHash.tap("HotModuleReplacementPlugin".hash= > {
const chunkGraph = compilation.chunkGraph;
const records = compilation.records;
for (const chunk of compilation.chunks) {
const getModuleHash = module= > {
if (compilation.codeGenerationResults.has(module, chunk.runtime)) {
return compilation.codeGenerationResults.getHash(
module,
chunk.runtime
);
} else {
nonCodeGeneratedModules.add(module, chunk.runtime);
return chunkGraph.getModuleHash(module, chunk.runtime); }};const fullHashModulesInThisChunk = chunkGraph.getChunkFullHashModulesSet(chunk);
// Set the value of fullHashModules
const modules = chunkGraph.getChunkModulesIterable(chunk);
if(modules ! = =undefined) {
if (records.chunkModuleHashes) {
if(fullHashModulesInThisChunk ! = =undefined) {
for (const module of modules) {
const key = `${chunk.id}|The ${module.identifier()}`;
const hash = getModuleHash(module);
if (fullHashModulesInThisChunk.has(module)) {
if(records.fullHashChunkModuleHashes[key] ! == hash) { updatedModules.add(module, chunk);
}
fullHashChunkModuleHashes[key] = hash;
} else {
if(records.chunkModuleHashes[key] ! == hash) { updatedModules.add(module, chunk); } chunkModuleHashes[key] = hash; }}}else {
// Set chunkModuleHashes}}else {
/ / set the value of fullHashChunkModuleHashes and chunkModuleHashes
}
}
}
hotIndex = records.hotIndex || 0;
if (updatedModules.size > 0) hotIndex++;
hash.update(`${hotIndex}`);
});
Copy the code
Hook compilation. Hooks. There are many interference in fullHash logic, their purpose is to calculate the fullHashModules, fullHashChunkModuleHashes, chunkModuleHashes, such as the value of a variable, With the interference removed, the core logic of the callback is to compare whether the hash value of the module in chunk has changed, and if so, add the related module and chunk to updatedModules.
compilation.hooks.processAssets
By listening to the compilation. Hooks. ProcessAssets hooks to generate [hash]. Hot – update. Js and [hash]. Hot – update. Json file:
compilation.hooks.processAssets.tap(
{
name: "HotModuleReplacementPlugin".stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
},
() = > {
const chunkGraph = compilation.chunkGraph;
const records = compilation.records;
if (records.hash === compilation.hash) return;
if(! records.chunkModuleHashes || ! records.chunkHashes || ! records.chunkModuleIds) {return;
}
// Compare whether the hash of modules in chunk has changed. If so, add related modules and chunks to updatedModules.
// Update chunkModuleHashes value.
const hotUpdateMainContentByRuntime = new Map(a);let allOldRuntime;
// Collect obsolete runtimes by traversing the records.chunkRuntime key and store them in allOldRuntime;
/ / by traversal allOldRuntime forEachRuntime call and set the value of hotUpdateMainContentByRuntime.
if (hotUpdateMainContentByRuntime.size === 0) return;
const allModules = new Map(a);// List of all modules (for later verification of which modules have been removed completely)
// set the value of allModules through compilation.modules.
const completelyRemovedModules = new Set(a);for (const key of Object.keys(records.chunkHashes)) {
const oldRuntime = keyToRuntime(records.chunkRuntime[key]);
const remainingModules = [];
ChunkModuleIds [key] to set the values of remainingModules and completelyRemovedModules.
let chunkId;
let newModules;
let newRuntimeModules;
let newFullHashModules;
let newDependentHashModules;
let newRuntime;
let removedFromRuntime;
const currentChunk = find(
compilation.chunks,
chunk= > `${chunk.id}` === key
);
if (currentChunk) {
// Set the newRuntime value.
if (newRuntime === undefined) continue;
// Set newModules, newRuntimeModules, newFullHashModules, newDependentHashModules based on updatedModules;
// Set the removedFromRuntime value based on oldRuntime and newRuntime.
} else {
// Because chunk has been deleted, set the values of removedFromRuntime and newRuntime to oldRuntime
}
if (removedFromRuntime) {
/ / update according to remainingModules and newRuntime hotUpdateMainContentByRuntime
}
// Generate the [hash].hot-update.js file
if ((newModules && newModules.length > 0) || (newRuntimeModules && newRuntimeModules.length > 0)) {
const hotUpdateChunk = new HotUpdateChunk();
// Check whether backward compatibility of the Webpack 4 API is enabled
if (backCompat)
ChunkGraph.setChunkGraphForChunk(hotUpdateChunk, chunkGraph);
hotUpdateChunk.id = chunkId;
hotUpdateChunk.runtime = newRuntime;
if (currentChunk) {
for (const group of currentChunk.groupsIterable)
hotUpdateChunk.addGroup(group);
}
chunkGraph.attachModules(hotUpdateChunk, newModules || []);
chunkGraph.attachRuntimeModules(
hotUpdateChunk,
newRuntimeModules || []
);
if (newFullHashModules) {
chunkGraph.attachFullHashModules(
hotUpdateChunk,
newFullHashModules
);
}
if (newDependentHashModules) {
chunkGraph.attachDependentHashModules(
hotUpdateChunk,
newDependentHashModules
);
}
const renderManifest = compilation.getRenderManifest({
chunk: hotUpdateChunk,
hash: records.hash,
fullHash: records.hash,
outputOptions: compilation.outputOptions,
moduleTemplates: compilation.moduleTemplates,
dependencyTemplates: compilation.dependencyTemplates,
codeGenerationResults: compilation.codeGenerationResults,
runtimeTemplate: compilation.runtimeTemplate,
moduleGraph: compilation.moduleGraph,
chunkGraph
});
for (const entry of renderManifest) {
let filename;
let assetInfo;
if ("filename" in entry) {
filename = entry.filename;
assetInfo = entry.info;
} else{({path: filename, info: assetInfo } =
compilation.getPathWithInfo(
entry.filenameTemplate,
entry.pathOptions
));
}
const source = entry.render();
compilation.additionalChunkAssets.push(filename);
compilation.emitAsset(filename, source, {
hotModuleReplacement: true. assetInfo });if (currentChunk) {
currentChunk.files.add(filename);
compilation.hooks.chunkAsset.call(currentChunk, filename);
}
}
forEachRuntime(newRuntime, runtime= >{ hotUpdateMainContentByRuntime .get(runtime) .updatedChunkIds.add(chunkId); }); }}const completelyRemovedModulesArray = Array.from(
completelyRemovedModules
);
const hotUpdateMainContentByFilename = new Map(a);/ / set the value of hotUpdateMainContentByFilename, including attributes:
// removedChunkIds, removedModules, updatedChunkIds and assetInfo.
for (const {
removedChunkIds,
removedModules,
updatedChunkIds,
filename,
assetInfo
} of hotUpdateMainContentByRuntime.values()) {
/ / set the value of hotUpdateMainContentByFilename:
// key is the value of filename. The properties are: removedChunkIds, removedModules, updatedChunkIds, and assetInfo.
}
// Generate the [hash].hot-update.json file
for (const [
filename,
{ removedChunkIds, removedModules, updatedChunkIds, assetInfo }
] of hotUpdateMainContentByFilename) {
const hotUpdateMainJson = {
c: Array.from(updatedChunkIds),
r: Array.from(removedChunkIds),
m:
removedModules.size === 0
? completelyRemovedModulesArray
: completelyRemovedModulesArray.concat(
Array.from(removedModules, m= >
chunkGraph.getModuleId(m)
)
)
};
const source = new RawSource(JSON.stringify(hotUpdateMainJson));
compilation.emitAsset(filename, source, {
hotModuleReplacement: true. assetInfo }); }});Copy the code
Hook compilation. Hooks. ProcessAssets logic is more, here is to calculate newModules, hotUpdateMainContentByFilename variable replacement code to comment, view the simplified version of the implementation, The main task of the hook is generated according to newModules, hotUpdateMainContentByFilename variables [hash]. Hot – update. Js and [hash]. Hot – update. Json file. The Webpack-hot-middleware client calls module.hot.check and module.hot.apply internally after receiving a sync message. And completes the module update operation accordingly.
compilation.hooks.additionalTreeRuntimeRequirements
Through listening compilation. Hooks. AdditionalTreeRuntimeRequirements hooks, Settings and instantiate HMR need runtime, This ensures that when Webpack is packaged, the relevant Runtime code is incorporated into the resulting code.
compilation.hooks.additionalTreeRuntimeRequirements.tap(
"HotModuleReplacementPlugin".(chunk, runtimeRequirements) = > {
runtimeRequirements.add(RuntimeGlobals.hmrDownloadManifest);
runtimeRequirements.add(RuntimeGlobals.hmrDownloadUpdateHandlers);
runtimeRequirements.add(RuntimeGlobals.interceptModuleExecution);
runtimeRequirements.add(RuntimeGlobals.moduleCache);
compilation.addRuntimeModule(
chunk,
newHotModuleReplacementRuntimeModule() ); });Copy the code
Code conversion
Running the example above, and looking at the resulting code, we see that our hot update code consists of:
if (module.hot) {
module.hot.accept('./app.js'.function() {
console.log('Accepting the updated from ./app.js');
let initValue = null;
let appElement = document.getElementById('app');
while (appElement.firstChild) {
let child = appElement.lastChild;
if (child.nodeName.toLocaleLowerCase() === 'input') {
initValue = child.value;
}
appElement.removeChild(child);
}
setup(initValue);
});
}
Copy the code
Becomes:
if (true) {
module.hot.accept('./app.js'.function() {
console.log('Accepting the updated from ./app.js');
let initValue = null;
let appElement = document.getElementById('app');
while (appElement.firstChild) {
let child = appElement.lastChild;
if (child.nodeName.toLocaleLowerCase() === 'input') {
initValue = child.value;
}
appElement.removeChild(child);
}
(0, _app__WEBPACK_IMPORTED_MODULE_1__.setup)(initValue);});
}
}
Copy the code
This is because the HotModuleReplacementPlugin by setting the JavaScriptParser on the translation:
const applyModuleHot = parser= > {
parser.hooks.evaluateIdentifier.for("module.hot").tap(
{
name: "HotModuleReplacementPlugin".before: "NodeStuffPlugin"
},
expr= > {
return evaluateToIdentifier(
"module.hot"."module".() = > ["hot"].true)(expr); }); parser.hooks.call .for("module.hot.accept")
.tap(
"HotModuleReplacementPlugin",
createAcceptHandler(parser, ModuleHotAcceptDependency)
);
};
normalModuleFactory.hooks.parser
.for("javascript/auto")
.tap("HotModuleReplacementPlugin".parser= > {
applyModuleHot(parser);
// omit other logic...
});
normalModuleFactory.hooks.parser
.for("javascript/dynamic")
.tap("HotModuleReplacementPlugin".parser= > {
applyModuleHot(parser);
});
Copy the code
The above code, we through monitoring hook normalModuleFactory. Hooks. The parser, and in its applyModuleHot callback handler:
- By listening on hooks
parser.hooks.evaluateIdentifier
matchingmodule.hot
The evaluation expression (here isif (module.hot)
) and then convert it totrue
; - By listening on hooks
parser.hooks.call
matchingmodule.hot.accept
Call, and then set up the necessary dependencies to generate the final code during the code generation phase with a code Generator in conjunction with the dependency template.
summary
This section of HotModuleReplacementPlugin implementation has carried on the simple analysis, here briefly summarizes the main process:
- Add the necessary dependencies;
- through
JavaScriptParser
Transform module update code in our business code; - through
compilation.hooks.record
及compilation.hooks.fullHash
Hooks that calculate and recordmodule
A series of information before and after the update; - According to the previous
module
Update information incompilation.hooks.record
Generated in the hook callback[hash].hot-update.js
和[hash].hot-update.json
Files so that clients can dynamically update modules based on these files.
Here it is important to note that in HarmonyImportDependencyParserPlugin, Will by calling HotModuleReplacementPlugin. GetParserHooks method to get associated with hotAcceptCallback and hotAcceptWithoutCallback JavaScriptParser Hooks, and respectively set HarmonyAcceptImportDependency and HarmonyAcceptDependency depend on:
// Other irrelevant code has been removed......
const HotModuleReplacementPlugin = require(".. /HotModuleReplacementPlugin");
module.exports = class HarmonyImportDependencyParserPlugin {
apply(parser) {
const { hotAcceptCallback, hotAcceptWithoutCallback } = HotModuleReplacementPlugin.getParserHooks(parser);
hotAcceptCallback.tap(
"HarmonyImportDependencyParserPlugin".(expr, requests) = > {
if(! HarmonyExports.isEnabled(parser.state)) {// This is not a harmony module, skip it
return;
}
const dependencies = requests.map(request= > {
const dep = new HarmonyAcceptImportDependency(request);
dep.loc = expr.loc;
parser.state.module.addDependency(dep);
return dep;
});
if (dependencies.length > 0) {
const dep = new HarmonyAcceptDependency(
expr.range,
dependencies,
true); dep.loc = expr.loc; parser.state.module.addDependency(dep); }}); hotAcceptWithoutCallback.tap("HarmonyImportDependencyParserPlugin".(expr, requests) = > {
// Same as hotAcceptCallback, omit...}); }}Copy the code
HMR runtime
By above knowable, HotModuleReplacementPlugin through compilation. Hooks. AdditionalTreeRuntimeRequirements to set the HMR required runtime code, Module.hot. check, module.hot.apply
module.hot.check
Module. Hot. Check is actually called lib/HMR/HotModuleReplacement runtime. HotCheck of js function, its definition is as follows:
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 = [];
blockingPromises = [];
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 () {
returnupdatedModules; }); }}); }); }); }); }Copy the code
Webpack replaces several variables in the above code snippet when it is packaged:
$hmrDownloadManifest$
Replace with__webpack_require__.hmrM
, for loading[hash]-hot-update.json
File;$hmrDownloadUpdateHandlers$
Replace with__webpack_require__.hmrC
, for loading[hash]-hot-update.js
File;
Json and [hash]-hot-update.js files, and then execute the corresponding logic according to the applyOnUpdate value. The process is as follows:
-
If the current state is not Idle, throw an exception. Otherwise, go to the next step.
-
Set the current status to check with setStatus and load the [hash]-hot-update.json file with __webpack_require__.hmrM.
-
After the file is successfully loaded, if the update parameter is null in the callback, set the current state according to the return value of the applyInvalidatedModules call and return null in the callback, otherwise go to the next step;
-
Use setStatus to set the current state to prepare and convert __webpack_require__.hmrC to a Promise array to load the [hash]-hot-update.js file in parallel.
-
After the file loads successfully, call waitForBlockingPromises to wait for all pending requests, and then do the following different things based on the value of the applyOnUpdate argument:
- if
applyOnUpdate
为true
, the implementation ofinternalApply
Dependency replacement; - if
applyOnUpdate
为false
Through thesetStatus
Sets the current state toready
And returns in a callbackupdatedModules
.
- if
module.hot.apply
Module. Hot. Apply actually called lib/HMR/HotModuleReplacement runtime. HotApply of js function, its definition is as follows:
function hotApply(options) {
if(currentStatus ! = ="ready") {
return Promise.resolve().then(function () {
throw new Error("apply() is only allowed in ready status");
});
}
return internalApply(options);
}
function internalApply(options) {
options = options || {};
applyInvalidatedModules();
var results = currentUpdateApplyHandlers.map(function (handler) {
return handler(options);
});
currentUpdateApplyHandlers = undefined;
var errors = results
.map(function (r) {
return r.error;
})
.filter(Boolean);
if (errors.length > 0) {
return setStatus("abort").then(function () {
throw errors[0];
});
}
// Now in "dispose" phase
var disposePromise = setStatus("dispose");
results.forEach(function (result) {
if (result.dispose) result.dispose();
});
// Now in "apply" phase
var applyPromise = setStatus("apply");
var error;
var reportError = function (err) {
if(! error) error = err; };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([disposePromise, applyPromise]).then(function () {
// handle errors in accept handlers and self accepted module load
if (error) {
return setStatus("fail").then(function () {
throw error;
});
}
if (queuedInvalidatedModules) {
return internalApply(options).then(function (list) {
outdatedModules.forEach(function (moduleId) {
if (list.indexOf(moduleId) < 0) list.push(moduleId);
});
return list;
});
}
return setStatus("idle").then(function () {
return outdatedModules;
});
});
}
Copy the code
In hotApply, if the current state is not ready, an exception is thrown; otherwise, internalApply is called. InternalApply runs as follows:
- call
applyInvalidatedModules
Used to perform the client in the callmodule.hot.invalidate
Is the specified callback function. - Then through
currentUpdateApplyHandlers
To collectapplyHandle
The result of the execution of themodule.hot.apply
Is the result of the specified callback function. - through
setStatus
Sets the current state todispose
And remove obsolete modules. - through
setStatus
Sets the current state toapply
And update the corresponding modules.
section
In this section, we briefly analyze the implementation of module.hot.check and module.hot.apply in the HMR runtime. Module. Hot also provides other API (webpack.docschina.org/api/hot-mod…). Due to space constraints, I will not analyze them here.
conclusion
This article gives a comprehensive analysis and introduction to Webpack DevServer & HMR:
- First we introduced the use of Webpack DevServer & HMR introduced that we can pass
webpack-cli
To quickly enable the feature, or borrow itwebpack api
,webpack-dev-middleware
,webpack-hot-middleware
及HotModuleReplacementPlugin
To self-enable; - And then we introduced
webpack-dev-middleware
,webpack-hot-middleware
及HotModuleReplacementPlugin
The realization principle of whichwebpack-hot-middleware
Including client and server two parts, andHotModuleReplacementPlugin
In addition to its own complex logic, it also needsotModuleReplacement.runtime.js
及HarmonyImportDependencyParserPlugin
The auxiliary support.
Webpack system is too large, this paper only gives a brief description of the implementation of core processes and methods, for the missing part, I sincerely hope to study together with you. Finally wish everyone happy code every day! ^_^