Understanding and Implementing the Hot Update Principle of Web Pack

Keywords: Javascript Webpack socket JSON hot update

Catalog

What is HMR

HMR, or Hot Module Replacement, means that when you modify and save the code, webpack will repackage the code and send the changed module to the browser side. The browser replaces the old module with the new module to update the page locally rather than refresh the page as a whole. Next, we will take you deep into HMR from the use to the realization of a simple version of the function.

The article was first published in @careteen/webpack-hmr For reprinting, please indicate the source.

Use scenarios

scenario

As shown in the figure above, a registration page contains user name, password, mailbox three mandatory input boxes, and a submit button. When you debug the mailbox module to change the code, you will refresh the entire page without any processing. Frequent code changes will waste a lot of time to fill in the content. It is expected that the input of user name and password will be retained and only the mailbox module will be replaced. This appeal requires the help of the hot module update function of webpack-dev-server.

Compared with live reload, HMR has the advantage of saving the state of application and improving the efficiency of development.

Configure to use HMR

Configuring webpack

First, build a project with the help of webpack

  • Initial recognition of projects and importation of dependencies
mkdir webpack-hmr && cd webpack-hmr
npm i -y
npm i -S webpack webpack-cli webpack-dev-server html-webpack-plugin
  • Configuration file webpack.config.js
const path = require('path')
const webpack = require('webpack')
const htmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'development', // The development mode does not compress the code to facilitate debugging
  entry: './src/index.js', // Entry file
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'main.js'
  },
  devServer: {
    contentBase: path.join(__dirname, 'dist')
  },
  plugins: [
    new htmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html'
    })
  ]
}
  • New src/index.html template file
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Webpack Hot Module Replacement</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
  • Create a new src/index.js entry file to write simple logic
var root = document.getElementById('root')
function render () {
  root.innerHTML = require('./content.js')
}
render()
  • New dependency file src/content.js export character for index rendering page
var ret = 'Hello Webpack Hot Module Replacement'
module.exports = ret
// export default ret
  • Configure package.json
  "scripts": {
    "dev": "webpack-dev-server",
    "build": "webpack"
  }
  • Then npm run dev can start the project
  • Packing static resources to dist directory through npm run build

Next, analyze the files in the dist directory

Parsing the contents of packaged web packages

  • Explanation of a set of commonjs specification implemented by webpack itself
  • Distinguish commonjs from esmodule

dist directory structure

.
├── index.html
└── main.js

The index.html is as follows

<!-- ... -->
<div id="root"></div>
<script type="text/javascript" src="main.js"></script></body>
<!-- ... -->

Using html-webpack-plugin plug-in to import entry files and their dependencies through script tags

Firstly, the main.js content is analyzed by removing comments and irrelevant content.

(function (modules) { // webpackBootstrap
  // ...
})
({
  "./src/content.js":
    (function (module, exports) {
      eval("var ret = 'Hello Webpack Hot Module Replacement'\n\nmodule.exports = ret\n// export default ret\n\n");
    }),
  "./src/index.js": (function (module, exports, __webpack_require__) {
    eval("var root = document.getElementById('root')\nfunction render () {\n  root.innerHTML = __webpack_require__(/*! ./content.js */ \"./src/content.js\")\n}\nrender()\n\n\n");
  })
});

It can be seen that a self-executing function is generated after the web pack is packaged, and its parameters are an object.

"./src/content.js": (function (module, exports) {
  eval("...")
}

The key is the entry file or the relative path of the dependent file relative to the root directory, and the value is a function in which eval is used to execute the content character of the file.

  • Entering the self-executing function body, we can see that webpack has implemented a set of commonjs specifications by itself.
(function (modules) {
  // Module caching
  var installedModules = {};
  function __webpack_require__(moduleId) {
    // Determine whether there is a cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // If there is no cache, create a module object and put it into the cache
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false, // Has it been loaded?
      exports: {}
    };
    // Execution module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // Set the state to loaded
    module.l = true;
    // Return module object
    return module.exports;
  }
  // ...
  // Load the entry file
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})

If you are interested in the above commonjs specification, you can go to another article of mine. Hand Touch to Implement the commonjs Specification

Give the above code is mainly to be familiar with the output file of webpack, don't be afraid. In fact, no matter how complex a thing is, it is made up of smaller and simpler things. Open it up and know that it loves it.

Configuration of HMR

Next, configure and feel the convenience of hot update development

webpack.config.js configuration

  // ...
  devServer: {
    hot: true
  }
  // ...

. / src/index.js configuration

// ...
if (module.hot) {
  module.hot.accept(['./content.js'], () => {
    render()
  })
}

When you change the content of. / content.js and save it, you can see that the page has not been refreshed, but the content has been replaced.

This is of great significance to improve the efficiency of development. Next, we will dissect it layer by layer and understand its implementation principle.

HMR Principle

As shown in the figure above, the right Server uses webpack-dev-server to start the local service, and the internal implementation mainly uses webpack, express and websocket.

  • Start the local service with express and respond to it when the browser accesses resources.
  • Long connection between server and client using websocket
  • Webpack monitors changes in source files, triggering the recompilation of webpack when the developer saves the file.

    • Each compilation generates hash values, json files for changed modules, and js files for changed module code.
    • After the compilation is completed, the hash stamp of the current compilation is pushed to the client through socket
  • The client's websocket listens for hash stamps pushed by file changes and compares them with the previous one.

    • Consistency is caching
    • Inconsistencies then get the latest resources from the server through ajax and jsonp
  • Local refresh using memory file system to replace modified content

The figure above just looks at the outline, and the following is a detailed analysis of both the server and the client.

debug server source code

Now you just need to pay attention to the right side of the service end section in the figure above, and the left side can be ignored temporarily. The next step is mainly debug server source code analysis of its detailed ideas, but also gives the specific location of the code. Interested can first locate the following code to set breakpoints, and then observe the changes in data. You can also skip reading this step first.

  1. Start webpack-dev-server server, source address @webpack-dev-server/webpack-dev-server.js#L173
  2. Create a webpack instance, source code address @webpack-dev-server/webpack-dev-server.js#L89
  3. Create Server Server, Source Code Address @webpack-dev-server/webpack-dev-server.js#L107
  4. Add a done event callback for webpack, source code address @webpack-dev-server/Server.js#L122

    1. Send message to client after compilation, source code address @webpack-dev-server/Server.js#L184
  5. Create express application app, source code address @webpack-dev-server/Server.js#L123
  6. Set file system to memory file system, source code address @webpack-dev-middleware/fs.js#L115
  7. Add webpack-dev-middleware, source code address @webpack-dev-server/Server.js#L125

    1. Middleware is responsible for returning the generated file, source code address @webpack-dev-middleware/middleware.js#L20
  8. Start webpack compilation, source address @webpack-dev-middleware/index.js#L51
  9. Create an http server and start the service, source code address @webpack-dev-server/Server.js#L135
  10. Use sockjs to establish a long web socket connection between browser and server, source code address @webpack-dev-server/Server.js#L745

    1. Create socket server, source address @webpack-dev-server/SockJSServer.js#L34

Simple implementation of server

Above are the core points of dev-server running process that I got from debug. Below are the following points Integrate abstractions into a file.

Start the webpack-dev-server server server

Import all dependencies first

const path = require('path') // Parse file path
const express = require('express') // Start local services
const mime = require('mime') // Get the file type to implement a static server
const webpack = require('webpack') // Read configuration files for packaging
const MemoryFileSystem = require('memory-fs') // Using a memory file system is faster, and files are generated in memory rather than real files.
const config = require('./webpack.config') // Get the webpack configuration file

Create a webpack instance

const compiler = webpack(config)

Compoiler represents the entire webpack compilation task, and there is only one global compiler

Create Server Server Server

class Server {
  constructor(compiler) {
    this.compiler = compiler
  }
  listen(port) {
    this.server.listen(port, () => {
      console.log(`The server is already there ${port}Start on port`)
    })
  }
}
let server = new Server(compiler)
server.listen(8000)

Later, the service is started by express.

done event callback with webpack added

  constructor(compiler) {
    let sockets = []
    let lasthash
    compiler.hooks.done.tap('webpack-dev-server', (stats) => {
      lasthash = stats.hash
      // Every time a new compilation is completed, a message is sent to the client.
      sockets.forEach(socket => {
        socket.emit('hash', stats.hash) // Send the latest hash value to the client first
        socket.emit('ok') // Send another ok to the client
      })
    })
  }

After compilation, webpack provides a series of hook functions for plug-ins to access its life cycle nodes and modify its packaging content. Compoiler. hooks. done is the last node where the plug-in can modify its content.

The compiler sends the message to the client through socket and pushes the hash generated by each compilation. In addition, if it is a hot update, two patch files will be produced, which describe what chunk s and modules have changed from the last result to this one.

Use the let sockets = [] array to store socket instances of each Tab when multiple Tabs are opened.

Create express application app

let app = new express()

Setting File System to Memory File System

let fs = new MemoryFileSystem()

Use Memory File System to package the compiler output file into memory.

Add webpack-dev-middleware Middleware

  function middleware(req, res, next) {
    if (req.url === '/favicon.ico') {
      return res.sendStatus(404)
    }
    // /index.html   dist/index.html
    let filename = path.join(config.output.path, req.url.slice(1))
    let stat = fs.statSync(filename)
    if (stat.isFile()) { // Determine if the file exists, and if so, read it out and send it directly to the browser
      let content = fs.readFileSync(filename)
      let contentType = mime.getType(filename)
      res.setHeader('Content-Type', contentType)
      res.statusCode = res.statusCode || 200
      res.send(content)
    } else {
      return res.sendStatus(404)
    }
  }
  app.use(middleware)

After using expres to start the local development service, we use middleware to construct a static server for it, and use the memory file system to store the files in memory after reading, improve the efficiency of reading and writing, and finally return the generated files.

Start webpack compilation

  compiler.watch({}, err => {
    console.log('Once again the compilation task was successfully completed')
  })

Start a webpack compilation in monitored mode, and execute callbacks when the compilation is successful

Create an http server and start the service

  constructor(compiler) {
    // ...
    this.server = require('http').createServer(app)
    // ...
  }
  listen(port) {
    this.server.listen(port, () => {
      console.log(`The server is already there ${port}Start on port`)
    })
  }

Using sockjs to establish a long web socket connection between browser and server

  constructor(compiler) {
    // ...
    this.server = require('http').createServer(app)
    let io = require('socket.io')(this.server)
    io.on('connection', (socket) => {
      sockets.push(socket)
      socket.emit('hash', lastHash)
      socket.emit('ok')
    })
  }

Start a websocket server and wait for the connection to come in and store it in the sockets pool

When there are file changes and webpack recompiles, push hash and ok events to the client

Server Debugging Phase

Interested can be based on the above debug server source code The location of the source code and the breakpoint in the debugging mode of the browser are set to view the values of each stage.

node dev-server.js

Using our own compiled dev-server.js to start the service, you can see that the page can be displayed properly, but hot updates have not yet been implemented.

Next, we will analyze the source code of the mode client and its implementation process.

debug client source code

Now you just need to focus on the left side of the client, which can be ignored for the time being. The next step is mainly debug client source code analysis of its detailed ideas, but also gives the specific location of the code. Interested can first locate the following code to set breakpoints, and then observe the changes in data. You can also skip reading this step first.

Deug Client Source Code Analysis

  1. The webpack-dev-server/client side monitors this hash message, source code address @webpack-dev-server/index.js#L54
  2. The client will execute reloadApp method to update the source code address after receiving the ok message. index.js#L101
  3. In reloadApp, it is judged whether hot updates are supported, webpackHotUpdate events are launched if supported, and browsers and source code addresses are refreshed directly if not supported. reloadApp.js#L7
  4. In webpack/hot/dev-server.js, the webpackHotUpdate event, source code address is monitored dev-server.js#L55
  5. The module.hot.check method, source code address, is called in the check method. dev-server.js#L13
  6. HotModuleReplacement.runtime requests Manifest, source address HotModuleReplacement.runtime.js#L180
  7. It calls JsonpMainTemplate.runtime's hotDownload Manifest method, source code address JsonpMainTemplate.runtime.js#L23
  8. Call the hotDownload Update Chunk method of JsonpMainTemplate.runtime to get the latest module code, source code address through JSONP request JsonpMainTemplate.runtime.js#L14
  9. When the patch JS is retrieved, the webpackHotUpdate method of JsonpMainTemplate.runtime.js will be called, with the source address JsonpMainTemplate.runtime.js#L8
  10. The HotModuleReplacement.runtime.js hotAddUpdateChunk method is then called to dynamically update the module code, source code address. HotModuleReplacement.runtime.js#L222
  11. Then call the hotApply method for hot update, source code address HotModuleReplacement.runtime.js#L257,HotModuleReplacement.runtime.js#L278

Simple Implementation of Client

Above are the core points of dev-server running process that I got from debug. Below are the following points Integrate abstractions into a file.

The webpack-dev-server/client side monitors this hash message

Before developing client functions, socket.io needs to be introduced into src/index.html.

<script src="/socket.io/socket.io.js"></script>

Connect the socket and accept the message

let socket = io('/')
socket.on('connect', onConnected)
const onConnected = () => {
  console.log('Successful client connection')
}
let hotCurrentHash // Last hash value 
let currentHash // This time hash value
socket.on('hash', (hash) => {
  currentHash = hash
})

Caching hash generated by each compilation of server-side Web pack

Update the reloadApp method when the client receives the ok message

socket.on('ok', () => {
  reloadApp(true)
})

Determine whether hot updates are supported in reloadApp

// When the ok event is received, the app is refreshed
function reloadApp(hot) {
  if (hot) { // If the hot update logic is true
    hotEmitter.emit('webpackHotUpdate')
  } else { // If hot update is not supported, direct reload
    window.location.reload()
  }
}

In reloadApp, it is judged whether hot updates are supported, webpackHotUpdate events are launched if supported, and browsers are refreshed directly if not supported.

Monitoring webpackHotUpdate events in webpack/hot/dev-server.js

First, you need a publish subscription to bind events and trigger them at the right time.

class Emitter {
  constructor() {
    this.listeners = {}
  }
  on(type, listener) {
    this.listeners[type] = listener
  }
  emit(type) {
    this.listeners[type] && this.listeners[type]()
  }
}
let hotEmitter = new Emitter()
hotEmitter.on('webpackHotUpdate', () => {
  if (!hotCurrentHash || hotCurrentHash == currentHash) {
    return hotCurrentHash = currentHash
  }
  hotCheck()
})

It determines whether the page and code are updated for the first time.

The above publish and subscribe functions are relatively simple and only support publish and subscribe first. For some more complex scenarios, you may need to subscribe before publishing, at which point you can move @careteen/event-emitter . The principle of implementation is simple. An offline event stack needs to be maintained to store events that are subscribed before they are published, and all events can be retrieved and executed when they are subscribed.

The module.hot.check method is called in the check method.

function hotCheck() {
  hotDownloadManifest().then(update => {
    let chunkIds = Object.keys(update.c)
    chunkIds.forEach(chunkId => {
      hotDownloadUpdateChunk(chunkId)
    })
  })
}

As mentioned above, every compilation of webpack generates hash values, json files for changed modules, and js files for changed module codes.

At this point, you use ajax to request Manifest, which module s and chunk s the server has changed from the previous compilation.

The code for these modified module s and chunk s is then retrieved through jsonp.

Call the hotDownload Manifest method

function hotDownloadManifest() {
  return new Promise(function (resolve) {
    let request = new XMLHttpRequest()
    //The hot-update.json file holds the differences from the last compilation to this compilation.
    let requestPath = '/' + hotCurrentHash + ".hot-update.json"
    request.open('GET', requestPath, true)
    request.onreadystatechange = function () {
      if (request.readyState === 4) {
        let update = JSON.parse(request.responseText)
        resolve(update)
      }
    }
    request.send()
  })
}

Call the hotDownload Update Chunk method to get the latest module code through a JSONP request

function hotDownloadUpdateChunk(chunkId) {
  let script = document.createElement('script')
  script.charset = 'utf-8'
  // /main.xxxx.hot-update.js
  script.src = '/' + chunkId + "." + hotCurrentHash + ".hot-update.js"
  document.head.appendChild(script)
}

Here's why you use JSONP instead of socket to get the latest code. This is mainly because the code acquired by JSONP can be executed directly.

Call the webpackHotUpdate method

When the client pulls the latest code to browse

window.webpackHotUpdate = function (chunkId, moreModules) {
  // Loop new pull-in modules
  for (let moduleId in moreModules) {
    // Retrieving old module definitions from module caches
    let oldModule = __webpack_require__.c[moduleId]
    // Which modules of parents refer to this module, children, which modules this module refers to?
    // parents=['./src/index.js']
    let {
      parents,
      children
    } = oldModule
    // Update cache updates for the latest code cache
    let module = __webpack_require__.c[moduleId] = {
      i: moduleId,
      l: false,
      exports: {},
      parents,
      children,
      hot: window.hotCreateModule(moduleId)
    }
    moreModules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
    module.l = true // To change state to load is to assign value to module.exports
    parents.forEach(parent => {
      // parents=['./src/index.js']
      let parentModule = __webpack_require__.c[parent]
      // _acceptedDependencies={'./src/title.js',render}
      parentModule && parentModule.hot && parentModule.hot._acceptedDependencies[moduleId] && parentModule.hot._acceptedDependencies[moduleId]()
    })
    hotCurrentHash = currentHash
  }
}

Implementation of hotCreateModule

Implementation allows us to define modules and callback functions that need hot updates in business code and store them in hot._acceptedDependencies.

window.hotCreateModule = function () {
  let hot = {
    _acceptedDependencies: {},
    dispose() {
      // Destroy old elements
    },
    accept: function (deps, callback) {
      for (let i = 0; i < deps.length; i++) {
        // hot._acceptedDependencies={'./title': render}
        hot._acceptedDependencies[deps[i]] = callback
      }
    }
  }
  return hot
}

Then call it in webpackHotUpdate

    parents.forEach(parent => {
      // parents=['./src/index.js']
      let parentModule = __webpack_require__.c[parent]
      // _acceptedDependencies={'./src/title.js',render}
      parentModule && parentModule.hot && parentModule.hot._acceptedDependencies[moduleId] && parentModule.hot._acceptedDependencies[moduleId]()
    })

Finally, call the hotApply method for hot update

Client debugging phase

After the above implementation of a basic version of HMR, the browser can change the code save while viewing the browser is not the overall refresh, but the local update code and then update the view. The efficiency of development is greatly improved when a large number of forms are involved.

problem

  • How to implement the commonjs specification?

Interested and accessible debug CommonJs specification Understand the principle of its implementation.

  • What are the implementation processes of webpack and the roles of each lifecycle?

webpack mainly uses a series of synchronous / asynchronous hook functions provided by tapable library throughout its life cycle.Based on this, I realized a simple version. webpack Source code 100 + lines, easy to digest with notes when eating, you can go to see a train of thought if you are interested.

  • The use and implementation of publishing and subscribing, and how to implement a mechanism of first subscribing and then publishing?

The need for publish-subscribe mode is also mentioned above, and only publish-and-subscribe functionality is supported. For some more complex scenarios, you may need to subscribe before publishing, at which point you can move @careteen/event-emitter . The principle of implementation is simple. An offline event stack needs to be maintained to store events that are subscribed before they are published, and all events can be retrieved and executed when they are subscribed.

  • Why use JSONP instead of socke communication to get updated code?

Because what you get through socket communication is a string of strings that need to be processed again. The code acquired through JSONP can be executed directly.

Quote

Posted by robster on Fri, 06 Sep 2019 02:03:36 -0700