preface
When I first learned VUE and contacted VUE-CLI, I was deeply attracted by its hot loading function in development. It’s interesting to see how it can update the Web application without refreshing the code we have modified, while maintaining some state of the application, which is very useful in our daily development. Since it turns out that the zhejiang function is provided by hotModuleReplace, a plug-in module of WebPack, this article takes you through how it is implemented.
Throw doubt
For things we are not familiar with or have no contact with, we can improve our ability by asking questions and solving these questions by ourselves, which is a good opportunity for growth.
-
Where do I store the files packed by Webpack? When there is no writeToDisk: True configuration item.
-
How does the packer interact with the client at work?
-
How the newly generated module works with the browser application.
At the beginning, the granularity of the questions we threw was relatively coarse, which didn’t matter. As we explored step by step, we would encounter some details and sort out the details, thus having a deeper understanding of the whole problem.
Diagram of HMR process
The figure above depicts a complete HMR process, so let’s see how it works.
-
In WebPack listening mode, after the file we wrote is saved, a recompilation is triggered, and the finished bundle is stored in memory. At compile packaging time, the ProgressPlugin plug-in for WebPack pushes compilation progress to the browser through webSocket.
-
Webpack-dev-server interacts with Webpack, and webpack-dev-middleware does two things: First, webpack-dev-middleware calls the API of the Compiler generated by Webpack and tells it whether to write packaged files to memory or to hard disk, depending on the writeToDisk configuration item. Second, webpack-dev-Middleware calls return an Express-standard middleware that registers a route to the app, intercepts HTTP requests, and responds to the corresponding file content according to the request path.
-
In this step, the browser mainly establishes long connection communication with the websocket service on the server through the sockJS library, and pushes the various stages of webpack compilation and packaging to the browser. There will be different operations according to this, and the most core is when the server pushes the new hash. The browser side uses this hash to perform subsequent hot update logic.
-
When the message type pushed by the server is OK, webpack-dev-server/client executes the reloadApp method, mainly to trigger the webpackHotUpdate event. The webpack/hot/dev-server determines whether to refresh the page or to heat update the page according to the relevant configuration and information transmitted. The link between the two is Webpack /hot/ Emitter, which depends on the Events module of Node, and can also take effect in the browser, indicating that it is platform-independent.
-
In webpack/hot/dev – server by calling the module. The hot. The check method of mapping to HotModuleReplacement. HotCheck method in the runtime, These methods by HotModuleReplacementPlugin into bendle.
-
HotCheck JsonpMainTemplate. Then call the runtime hotDownloadManifest and hotDownloadUpdateChunk method.
-
HotDownloadManifest carries the hash pushed on it to make an Ajax request to get the latest list of files.
-
HotDownloadUpdateChunk initiates a JSONP request for the latest code block based on the file list, continuing the logic of subsequent code updates.
-
When the code is updated, the old and new modules are compared to determine whether to update. If updates are made, the dependencies between modules are sorted out, and then the modules and their associated dependencies are updated. If the update fails, the bottom is flushed.
Run the Demo
Let’s use a simple example to illustrate. To illustrate some of these details, take the most familiar vUE as an example.
Let’s initialize a basic Vue project with the following directories and core configuration:
├─ public │ ├─ ├─ ├.vue ├─ webPack.config.jsCopy the code
The configuration of webpack.config.js is as follows,
{
devtool: 'source-map'.mode: 'development'.entry: path.resolve(__dirname, './main.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
devServer: process.env.NODE_ENV == 'production'? {}, {contentBase: path.join(__dirname, 'dist'),
hot: true.writeToDisk: true,},module: {
rules: [{test: /\.vue$/,
use: ['vue-loader']},]},plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html'}})]Copy the code
Package. json is configured as follows,
"scripts": {
"dev": "rm -rf ./dist && NODE_ENV=development webpack-dev-server --config webpack.config.js --open --inline --progress"
}
Copy the code
Main. js is the normal vue startup portal, so I won’t go into details here.
When we modify test.vue,
Step 1: WebPack puts the packed files into memory
Webpack-dev-middleware puts packaged files into memory by calling the API of Webpack’s Compiler. The core code is as follows:
// webpack-dev-middleware/index.js
if(! options.lazy) { context.watching = compiler.watch(options.watchOptions,(err) = > {
// ...})}Copy the code
When writeToDisk is configured, packaged files are written to the corresponding folder. But without this configuration, where are the files stored? We mentioned a memory-fs dependency in Webpack-dev-Middleware, which gives us a good idea of what it might be. Compiler in webpack outputFileSystem will be hanging on the memory – instances of fs. The core code is as follows:
// webpack-dev-middleware/lib/fs.js
constisMemoryFs = ! isConfiguredFs && ! compiler.compilers && compiler.outputFileSysteminstanceof MemoryFileSystem;
if (isConfiguredFs) {
const { fs } = context.options;
compiler.outputFileSystem = fs;
fileSystem = fs;
} else if (isMemoryFs) {
fileSystem = compiler.outputFileSystem;
} else {
fileSystem = new MemoryFileSystem();
compiler.outputFileSystem = fileSystem;
}
Copy the code
If the current compiler outputFileSystem not MemoryFileSystem, then use MemoryFileSystem replacement, such packaging file is stored in MemoryFileSystem build by the file system, Speed up access to read.
Step 2: devServer notifies the browser side of file compilation
The Compiler in Webpack will add the ProgressPlugin plug-in, count the compilation progress of the package, and then communicate with the server websocket through SOckJS to push each state of the package to the client. At this point, you also need to know the status of the packing completion, listen for the done event in Compiler. hooks, and push the packed stats.hash to the browser by calling _sendStats. The core code is as follows:
// webpack-dev-server/lib/Server.js
new webpack.ProgressPlugin((percent, msg, addInfo) = > {
/ /... Use the ProgressPlugin plug-in
this.sockWrite(this.sockets, 'progress-update', { percent, msg });
// ...
}).apply(this.compiler);
setupHooks() {
// ...
const addHooks = (compiler) = > {
const { compile, invalid, done } = compiler.hooks;
// ...
// Add the done event listener
done.tap('webpack-dev-server'.(stats) = > {
this._sendStats(this.sockets, this.getStats(stats));
this._stats = stats;
});
};
if (this.compiler.compilers) {
this.compiler.compilers.forEach(addHooks);
} else {
addHooks(this.compiler); }}_sendStats(sockets, stats, force) {
// ...
// Push the new hash to the browser
this.sockWrite(sockets, 'hash', stats.hash);
// ...
}
Copy the code
Step 3: webpack-dev-server/client receives and processes the information pushed by the server
At the beginning of this logic, I was a little curious about how the processing code for the webpack-dev-server/client part of the bundle was transferred to the bundle. In fact, when Webpack-dev-server started, I went back to update the Webpack compiler and injected this code into the client through the addEntries plugin. The core code is as follows:
// webpack-dev-server/lib/Server.js
updateCompiler(this.compiler, this.options)
// webpack-dev-server/lib/utils/updateCompiler.js
addEntries(webpackConfig, options);
compilers.forEach((compiler) = > {
// ...
const providePlugin = new webpack.ProvidePlugin({
__webpack_dev_server_client__: getSocketClientPath(options),
});
providePlugin.apply(compiler);
});
Copy the code
Webpack-dev-server /client processes websocket messages according to different types. ReloadApp is called when the hash value is received and the message type is OK. As shown below:
In reloadApp, webpack/ Hot/Emitter fires The webpackHotUpdate event and passes the latest hash as a parameter. If the hot parameter is not configured, page refresh will be performed in the macro task. The core code is as follows:
// webpack-dev-server/client-src/default/index.js
const onSocketMessage = {
hash(hash) {
status.currentHash = hash;
},
ok(){ reloadApp(options, status); }}function reloadApp({ hotReload, hot, liveReload }, { isUnloading, currentHash }) {
if (hot) {
const hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
} else if (liveReload) {
// ...
self.setInterval(() = > {
rootWindow.location.reload()
})
}
}
Copy the code
Step 4: Webpack accepts the latest hash validation and updates the module
In the previous step we distributed the webpackHotUpdate event, depending on where the event was consumed. The main callback consumed in webpack/hot/dev-server is the module.hot.check method,
Module. This object is hot will HotModuleReplacement HotModuleReplacementPlugin plug-in. The runtime code injection. The core code is as follows:
// webpack/hot/dev-server.js
hotEmitter.on("webpackHotUpdate".function(currentHash) {
/ /...
module.hot.check(true).then(() = > {
/ /...})});// webpack/lib/HotModuleReplacementPlugin.js
const Template = require("./Template")
const hotInitCode = Template.getFunctionContent(
require("./HotModuleReplacement.runtime")
)
mainTemplate.hooks.moduleObj.tap(
"HotModuleReplacementPlugin".(source, chunk, hash, varModuleId) = > {
return Template.asString([
`${source}, `.`hot: hotCreateModule(${varModuleId}), `.// Inject the HMr. Runtime API into module
"parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),"."children: []"]); });// webpack/lib/HotModuleReplacement.runtime.js
function hotCreateModule(moduleId) {
var hot = {
check: hotCheck,
// other api
};
return hot;
}
Copy the code
HotCheck method will be called JsonpMainTemplate. Two methods of runtime hotDownloadManifest and hotDownloadUpdateChunk, The former uses the hash passed in the previous step to initiate an Ajax request for the server to get the file update list, as shown in the figure. The latter initiates a JSONP request for the corresponding latest code block based on the list of files, continuing the logic for subsequent code updates.
The hotDownloadManifest method gets the list of updated files
HotDownloadUpdateChunk gets the updated new module code.
I have a question here, and I have been thinking about it for some time, but I have not thought about the general question, is why to update the above way. Technically, when a server pushes a message of type OK, it can push the updated code block as well, but why not? Module decoupling, dev-server/client is only responsible for message delivery, HotModuleReplacement is responsible for code module updates, which compound the principle of a single design, and WebPack does not use dev-server. Webpack-hot-middleware can be used with webpack-hot-middleware to implement hot updates. Webpack-hot-middleware uses the SSE protocol, not Websocket, which is also used in Flutter devtools. It can well decouple the functions of modules. : : :
Step 5: HotModuleReplacement. Update the runtime module
In the above jSONP request we see that it calls the webpackHotUpdate method, Through the packaging of the bundle. Js can see it again call JsonpMainTemplate. WebpackHotUpdateCallback in the runtime, It mainly call HotModuleReplacement. Runtime hotAddUpdateChunk, behind a series of calls not to dwell on one by one, the way we look at the most core hotApply. Since this method is implemented with a lot of code, we can trace hotStatus changes to tease out the update logic.
- “Idle” => “check”, this phase
module.hot.check
Methods. - “Check” => “prepare”, this phase passes above
hotDownloadManifest
Get the latest file list. - “Prepare” => “ready”, the new module is stored
hotUpdate
. - “Ready” => “Dispose”, this stage goes through two cycles, in the first cycle, pass first
getAffectedStuff
Method to find all relevant information about the current new moduleModule outdatedModulesandRely on outdatedDependencies.result
The return value is as follows, if not found, willresult.type
Represented as adisposed
. Return to normalresult.type
foraccepted
.
let result = {
"type":"accepted"."moduleId":"./lib/vue-loader/index.js? ! ./test.vue? vue&type=script&lang=js&"."outdatedModules": ["./lib/vue-loader/index.js? ! ./test.vue? vue&type=script&lang=js&"."./test.vue? vue&type=script&lang=js&"."./test.vue"]."outdatedDependencies":{}
}
Copy the code
In the second loop, find outoutdatedModulesIn, and is already in usehot._selfAccepted
fortrue
These modules are called by themselvesThis method, forvue
Component for this part of the code is invue-loader
The core code is as follows:
if (
installedModules[moduleId] &&
installedModules[moduleId].hot._selfAccepted &&
Removed self-accepted modules should not be requiredappliedUpdate[moduleId] ! == warnUnexpectedRequire ) { outdatedSelfAcceptedModules.push({module: moduleId,
errorHandler: installedModules[moduleId].hot._selfAccepted
});
}
Copy the code
- Dispose “=> “apply”, at this stage, iterate over outdatedModules, remove expired modules and submodules referenced by installedModules from installedModules, and remove dependencies, the core code is as follows:
var queue = outdatedModules.slice();
while (queue.length > 0) {
moduleId = queue.pop();
module = installedModules[moduleId];
if (!module) continue;
// remove module from cache
delete installedModules[moduleId];
// when disposing there is no need to call dispose handler
delete outdatedDependencies[moduleId];
// remove "parents" references from all children
for (j = 0; j < module.children.length; j++) {
var child = installedModules[module.children[j]];
if(! child)continue;
idx = child.parents.indexOf(moduleId);
if (idx >= 0) {
child.parents.splice(idx, 1); }}}Copy the code
- “Apply” => “fail” or “idle”. In this phase, modules are mainly updated. Modules that can be updated are first updated
appliedUpdate
Mounted to the globalmodules
, foroutdatedSelfAcceptedModules
Is called by the module in$require$(moduleId)
Here,$require$
It’s just packed up__webpack_require__
Function, mainly used to load modules.
Step 6: How to customize vUE components
Test. Vue this module in the vue – loader parsing, will through the vue – loader/codegen/hotReload genHotReloadCode of js method to add the following code, The core logic for updating vUE components without refreshing the page in VUe2 is provided by the vuE-hot-reload API library.
// vue-loader/codegen/hotReload.js genHotReloadCode
if (module.hot) {
var api = require("xxx/node_modules/vue-hot-reload-api/dist/index.js")
api.install(require('vue'))
if (api.compatible) {
module.hot.accept()
if(! api.isRecorded('13429420')) {
api.createRecord('13429420', component.options)
} else {
api.reload('13429420', component.options)
}
module.hot.accept("./test.vue? vue&type=template&id=13429420&scoped=true&".function () {
api.rerender('13429420', {
render: render,
staticRenderFns: staticRenderFns
})
})
}
}
Copy the code
Here vue-hot-reload-API in order to support more hot new, its core implementation logic is, in the component lifecycle hook function, insert the function of the object saving the context of the component, the first rendering is to save the current context of the component; On the second hot update, replace the options of the component and then call the component’s $forceUpdate method to re-render.
conclusion
- The general understanding of this article
webpack hmr
The whole process of the work, there are many details that are not captured properly, such aswebpack-dev-server/client
Insertion of code,hotapply
All the twists and turns inside, and the wholewebpack
The realization of module mechanism, welcome friends to actively discuss. - This article refers to the principle of Webpack HMR analysis, thanks to the big guy pay, feel that such writing ideas are very worthy of reference.
reference
- Webpack HMR principle analysis
- Hot Module Replacement