HMR
Front knowledge
To understand HRM in Webpack, you should first have some knowledge of the following
- Learn the basics of Webpack and the implementation of require in Webpack juejin.cn/post/690377…
- Tapable (Core library for WebPack)
- Basic Node knowledge (Express or KOA)
- Understand the websocket
The refresh
- Browser-based refresh, without preserving page state, is simple and straightforward
window.location.reload()
. - The other is based on
WDS (Webpack-dev-server)
Module hot replacement, only need to partially refresh the changed module on the page, while retaining the current page state, such as check box selected state, input box input, etc.
HMR as a Webpack built-in function, can pass HotModuleReplacementPlugin or opened – hot. So how does HMR implement hot update? Let’s take a look at it.
What is the HMR
Hot Module Replacement (HMR) is one of the most exciting features webPack has introduced so far. When you make changes to your code and save it, WebPack repackages it and sends the new Module to the browser. Browsers replace old modules with new ones so that applications can be updated without refreshing the browser. (Note not browser refresh).
HMR is used in Webpack
Just add the following two lines of code to webpack.config.js to implement hot updates
- devServer: {hot:true}
- plugins:[new webpack.HotModuleReplacementPlugin()]
module.exports = {
mode: 'development'.entry: './src/index.js'.devtool: 'source-map'.output: {
filename: 'main.js'.path: path.join(__dirname,'dist')},devServer: {
hot:true
},
plugins: [new HtmlWebpackPlugin(),
new webpack.HotModuleReplacementPlugin()
]
}
Copy the code
When a hot update happens we look in the browser to see what’s changed and when the first Webpack is packaged, it generates a hash and sends it to the client. When the module changes. Webpack returns to generate a hash value. Send to the client. The client will retrieve the old hash value when hot module replacement occurs. Go download the file. To achieve hot updateThe browser gets a JSON file based on the hash. To know which modules have changed
Download the changed module based on the hash. Notify the code to update
Implementation of HMR in Webpack
In Webpack, the implementation principle of HRM is shown in the figure below, which may seem complicated now. We will implement it step by step according to the steps in the figure
- First we create an instance of WebPack (the compiler object has a list of webpack methods and properties)
- Create a server server.
- After the server is created, modify the entry configuration in config. Dynamically add two files to entry (webpack-dev-server/client.index.js and webpack/hot/index.js. The purpose of these two files will be covered later.)
- Listen for webapck’s package completion event. (When subsequent files are changed. Can trigger module updates.
- Use the module of Watch to monitor the changes of files. When the files change, it will trigger the packaging of Webpack. Output to the file system. This is when step four is triggered
- A static file server hosts the files so that we can access them in a browser
- Create a WebSocket server. When the fourth step is complete, after the Webpack is wrapped, the corresponding hash will be generated. The Websocket pushes the hash to the client
- The client receives the hash through the Websocket. I’m going to check to see if the file is up to date
- When the file is updated. The corresponding JSON file will be downloaded. Json files record which files are updated and the latest hash
- Download the changed file, then the browser deletes the cached file and replaces it with the latest file contents. Finally, execute Accept to invoke the file. That completes our hot update
Next, we will implement HRM step by step according to the above steps
Create webPack instance and create server
In package.json file. Add a command
"scripts": {
"dev": "node webpack-dev-server"
},
Copy the code
webpack-dev-server/index.js
const webpack = require('webpack')
const config = require('.. /webpack.config.js')
const complier = webpack(config) // Create an instance of Webpack
const Server = require('./lib/server/index')
const server = new Server(complier) // Create a server
server.listen(9090.'localhost'.() = >{
console.log('Service started')})Copy the code
webpack-dev-server/lib/server/index.js
const express = require('express')
const http = require('http')
const fs = require('fs-extra')
const path = require('path')
class Server {
constructor(compiler) {
this.compiler = compiler // Get an instance of webpack
this.setupApp()
this.createServer()
}
setupApp() {
this.app =new express() // Create an Express service (mainly using the middleware capabilities of Express)
}
createServer() {
this.server = http.createServer(this.app) // Create an HTTP server.
}
listen(port,host,callback){
this.server.listen(port,host,callback)
}
}
module.exports = Server
Copy the code
So we can execute NPM run dev and access port 9090, even though we can’t access any files right now.
Modify the configuration of webPack entry files
When creating the server, modify the webPack configuration file
class Server {
constructor(compiler) {
this.compiler = compiler // Get an instance of webpack
updateCompiler(compiler)
}
}
Copy the code
Lib/utils/updateCompiler. Js file
const path = require('path')
function updateCompiler(compiler) {
const config = compiler.options
config.entry = {
main:[
path.resolve(__dirname,'.. /.. /client/index.js'),
path.resolve(__dirname,'.. /.. /.. /webpack/hot/devServer.js'),
config.entry
]
}
}
module.exports = updateCompiler
Copy the code
The main reason is to inject two files into the entry. Used for hot updates. Both files will be explained in more detail later
The listening Webpack is packaged
The main purpose is to listen for events that WebPack has completed. Each time the Webpack is wrapped, this event is triggered to retrieve the latest hash value and send it to the client
class Server {
constructor(compiler) {
this.compiler = compiler
updateCompiler(compiler)
this.setupHooks()
}
setupApp() {
this.app =new express()
}
createServer() {
this.server = http.createServer(this.app)
}
setupHooks() {
let { compiler } = this
// Use open listener webpack. Get the corresponding hash. To emit the corresponding hash to the client
compiler.hooks.done.tap('webpack-dev-server'.(stats) = >{
this.currentHash = stats.hash
this.clientSocketList.forEach(socket= >{
socket.emit('hash'.this.currentHash)
socket.emit('ok')})})}Copy the code
webpack-dev-middleWare
The main logic of this part is to package our files in Watch mode. Output to our memory file system. Implement a static file server that allows us to access packaged files through a browser.
class Server {
constructor(compiler) {
this.compiler = compiler
updateCompiler(compiler)
this.setupApp()
this.currentHash;
this.clientSocketList = []
this.setupHooks()
this.setupDevMiddleWare()
this.routes()
this.createServer()
}
setupApp() {
this.app =new express()
}
createServer() {
this.server = http.createServer(this.app)
}
setupHooks() {
let { compiler } = this
compiler.hooks.done.tap('webpack-dev-server'.(stats) = >{
this.currentHash = stats.hash
this.clientSocketList.forEach(socket= >{
socket.emit('hash'.this.currentHash)
socket.emit('ok')})})}setupDevMiddleWare() {
this.middleWare = this.webpackDevMiddleware()
}
routes() {
let { compiler } = this
let config = compiler.options
// Express middleware.
this.app.use(this.middleWare(config.output.path))
}
webpackDevMiddleware() {
let { compiler } = this
// When a file changes, WebPackjiu recompiles
compiler.watch({},() = >{
console.log("Listening mode")})this.fs = compiler.outputFileSystem = fs
// This is a static file server. It is mainly used to access the files that we have generated after packaging
return (staticDir) = > {
return (req,res,next) = > {
let {url} = req
if(url==='/favicon.ico') {
return res.sendStatus(404)
}
url==='/'? url='/index.html':null
let filePath = path.join(staticDir,url)
console.log(filePath)
try {
let stateObj = this.fs.statSync(filePath)
if(stateObj.isFile()) {
// Read the file
let content = this.fs.readFileSync(filePath)
// Get the suffix of the file. Returns the corresponding file type
res.setHeader('Content-Type',mime.getType(filePath))
// Return the file to the client
res.send(content)
}
} catch (error) {
return res.sendStatus(404)}}}}listen(port,host,callback){
this.server.listen(port,host,callback)
}
}
module.exports = Server
Copy the code
Create the WebSocket service
The webSocket service is created to send the newly generated hash value to the client when WebPack is recompiled. The client hashes the corresponding latest code
class Server {
constructor(compiler) {
this.createServer()
}
createSocketServer() {
const io = socketIo(this.server) // Instantiate the socket service
io.on('connection'.(socket) = >{
console.log('New client linked up')
this.clientSocketList.push(socket) / / maintain the socket
socket.emit('hash'.this.currentHash) // Sends the latest hash value to the client
socket.emit('ok')
socket.on('disconnect'.() = >{
// After the connection is disconnected, the corresponding socket is deleted
let index = this.clientSocketList.indexOf(socket)
this.clientSocketList.splice(index,1)})})}}Copy the code
Getting to this point on the server side of the code is almost complete. Next, let’s implement the client-side code
Client-related logic
Here, we first create two files./ SRC /index.js
let input = document.createElement('input')
document.body.appendChild(input)
let div = document.createElement('div')
document.body.appendChild(div)
let render = () = > {
let title = require('./title.js')
div.innerHTML = title
}
render()
if(module.hot){
module.hot.accept(['./title.js'],render)
}
Copy the code
./src/title.js
module.exports = 'title'
Copy the code
After webPack packing, simplify the code after Webpack packing. We can get the code. Want to know how to implement it in detail. Check out this article at juejin.cn/post/690377…
(function (modules) {
let installedModules = {}
function __webpack_require__(modulesId) {
if (installedModules[modulesId]) {
return installedModules[modulesId]
}
let module = installedModules[modulesId] = {
i: modulesId,
l: false.exports: {},
}
modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);
module.l = true
return module.exports
}
return __webpack_require__('./src/index.js') ({})"./src/index.js": function (module.exports, __webpack_require__) {
__webpack_require__("webpack/hot/devServer.js")
__webpack_require__("webpack-dev-server/client/index.js")
let input = document.createElement('input')
document.body.appendChild(input)
let div = document.createElement('div')
document.body.appendChild(div)
let render = () = > {
let title = __webpack_require__(/ *! ./title.js */ "./src/title.js")
div.innerHTML = title
}
render()
if (true) {
module.hot.accept([/ *! ./title.js */ "./src/title.js"], render)
}
},
"./src/title.js": function (module.exports) {
module.exports = 'title'
},
"webpack-dev-server/client/index.js": function (module.exports) {},"webpack/hot/devServer.js": function (module.exports) {}})Copy the code
Next we implement webpack-dev-server/client/index.js
client/index.js
The client mainly listens for Websocket events when the latest hash is generated. Triggers the corresponding event
let currentHash;
let lastHash;
class EventEmitter {
constructor() {
this.events = {}
}
on(eventName, fn) {
this.events[eventName] = fn
}
emit(eventName, ... args) {
this.events[eventName](... args) } };let hotEmitter = new EventEmitter();
EventEmitter implements a publishing subscriber model of its own, and you can use some third-party libraries instead
const socket = window.io('/')
socket.on('hash'.(hash) = > {
currentHash = hash
})
// Listen for the OK event
socket.on('ok'.() = > {
console.log('ok')
reloadApp()
})
function reloadApp() {
// Send an event. Tell that a hot update has occurred
hotEmitter.emit('webpackHotUpdate')}Copy the code
This completes our client/index.js logic
webpack/hot/devServer.js
When hash changes. We’ll call module.hot.check()
hotEmitter.on('webpackHotUpdate'.() = > {
// console.log('check')
// Determine if the hash has changed
if(! lastHash) { lastHash = currentHashreturn
}
// If the hash changes, call the hot module update function
module.hot.check()
})
Copy the code
Check and accept
To truly implement hot module updates, we must maintain the parent-child relationship before the module. When the submodule changes. We tell the parent module’s Accept method to reload the child module. So next we will modify the files packed by Webpack above to make the modules have corresponding parent-child relationship. And implement the module’s check and accept methods
function __webpack_require__(modulesId) {
if (installedModules[modulesId]) {
return installedModules[modulesId]
}
let module = installedModules[modulesId] = {
i: modulesId,
l: false.exports: {},
hot:hotCreateModule(), // Implement the module's check method
parents: [].// Parent relationship before user maintenance module
children: []
}
modules[modulesId].call(module.exports, module.module.exports, hotCreateRequire(modulesId))
module.l = true
return module.exports
}
__webpack_require__.c = installedModules
return hotCreateRequire('./src/index.js') ('./src/index.js')
// The parent relationship of the main implementation module
function hotCreateRequire(parentsModuleId) {
let parentModule = installedModules[parentsModuleId]
// Indicates a top-level module. No server module
if(! parentModule)return __webpack_require__;
// Has a parent module
let hotRequire = function (childModuleId) {
__webpack_require__(childModuleId)
let childModule = installedModules[childModuleId]
childModule.parents.push(parentModule)
parentModule.children.push(childModule)
// console.log(childModule)
return childModule.exports
}
return hotRequire
}
// The main purpose here is to implement the module's hot-.accept and check methods
function hotCreateModule() {
let hot = {
_acceptDependencies: {},
// Collect related dependencies
accept(deps,callback) {
deps.forEach(dep= > hot._acceptDependencies[dep]=callback);
},
check: hotcheck
}
return hot
}
Copy the code
In this way, we successfully maintain the parent-child relationship before the module, which is convenient for hot update later
hotcheck
The module.hot.check() method is called when the hash above changes. So here’s what we’re doing to our hotcheck based on the hash change. Use hash to find the corresponding JSON file. Use Ajax to download files. After the download is successful. Let’s check which files have changed. Go download the latest code for these modules
// Use Ajax to download the corresponding JSON file
function hotDownloadManifest() {
return new Promise((resolve,reject) = >{
let xhr =new XMLHttpRequest()
let url = `${lastHash}.hot-update.json`
xhr.open('get',url)
xhr.responseType = 'json'
xhr.onload = function() {
resolve(xhr.response)
}
xhr.send()
})
}
function hotcheck() {
hotDownloadManifest().then(update= >{
let chunkIds = Object.keys(update.c)
chunkIds.forEach(chunkId= >{
hotDownloadUpdateChunk(chunkId)
})
lastHash = currentHash
}).catch(() = >{
window.location.reload()
})
}
Copy the code
hotDownloadUpdateChunk
Once you have the changed module, download the code for the latest changed module
function hotDownloadUpdateChunk(chunkId) {
let script = document.createElement('script')
script.src = `${chunkId}.${lastHash}.hot-update.js`
document.head.appendChild(script)
}
Copy the code
webpackHotUpdate
When the browser gets the latest code. I’m going to go ahead and execute the webpackHotUpdate method and basically get the latest module. Record the latest module code
window.webpackHotUpdate = function(chunkId,moreModules) {
HotUpdateChunk(chunkId,moreModules)
}
let hotUpdate = {}
function HotUpdateChunk(chunkId,moreModules) {
for(moduleId in moreModules) {
modules[moduleId] = hotUpdate[moduleId] = moreModules[moduleId]
}
hotApply()
}
Copy the code
hotApply
Get the old module from before. Delete the previous old module. To render the newly generated module. To execute the corresponding callback function. So we have hot module replacement
function hotApply() {
for(moduleId in hotUpdate) {
let oldModule = installedModules[moduleId]
delete installedModules[moduleId]
// Get the parent module of the old module. To execute the corresponding callback
oldModule.parents.forEach(parentModule= >{
let cb = parentModule.hot._acceptDependencies[moduleId]
cb&&cb()
})
}
}
Copy the code
The full code can be accessed at gitee.com/yujun96/web…