HMR

Front knowledge

To understand HRM in Webpack, you should first have some knowledge of the following

  1. Learn the basics of Webpack and the implementation of require in Webpack juejin.cn/post/690377…
  2. Tapable (Core library for WebPack)
  3. Basic Node knowledge (Express or KOA)
  4. Understand the websocket

The refresh

  • Browser-based refresh, without preserving page state, is simple and straightforwardwindow.location.reload().
  • The other is based onWDS (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

  1. devServer: {hot:true}
  2. 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

  1. First we create an instance of WebPack (the compiler object has a list of webpack methods and properties)
  2. Create a server server.
  3. 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.)
  4. Listen for webapck’s package completion event. (When subsequent files are changed. Can trigger module updates.
  5. 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
  6. A static file server hosts the files so that we can access them in a browser
  7. 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
  8. The client receives the hash through the Websocket. I’m going to check to see if the file is up to date
  9. When the file is updated. The corresponding JSON file will be downloaded. Json files record which files are updated and the latest hash
  10. 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…