Those of you who have used WebPack know that there is a particularly useful “hot update” that pushes code to the browser without refreshing the page.

Today’s article will explore the secrets of webPack hot updates.

How do I configure hot updates

Let’s start by installing some of the packages we need:

npm i webpack webpack-cli -D
npm i webpack-dev-server -D
npm i html-webpack-plugin -D
Copy the code

Then, we need to understand that after webpack from version webpack@4, we need to start the service through the Webpack CLI, which provides packaged commands and commands to start the development service.

Package to the specified directory
webpack build --mode production --config webpack.config.js
Start the development server
webpack serve --mode development --config webpack.config.js
Copy the code
// pkg.json
{
  "scripts": {
    "dev": "webpack serve --mode development --config webpack.config.js"."build": "webpack build --mode production --config webpack.config.js"
  },
  "devDependencies": {
    "webpack": "^ 5.45.1"."webpack-cli": "^ 4.7.2." "."webpack-dev-server": "^ 3.11.2"."html-webpack-plugin": "^ 5.3.2." ",}}Copy the code

When starting the development service, configure the devServe property in the WebPack configuration file to enable hot update mode.

// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/index.js'.output: {
    filename: 'main.js'.path: path.resolve(__dirname, 'dist'),},devServer: {
    hot: true.// Enable hot update
    port: 8080.// Specify the server port number
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html'}})]Copy the code

Once configured, we can start to create new files according to the following directory structure.

├─ SRC │ ├─ ├─ ├─ ├─ ├─ download.config.jsCopy the code

In order to use jQuery (YYDS) directly, jQuery CDN is introduced into THE HTML file because DOM manipulation is required.

<! DOCTYPEhtml>
<html lang="en">
<head>
  <title>Webpack Demo</title>
  <script src="https://unpkg.com/[email protected]/dist/jquery.js"></script>
</head>
<body>
  <div id="app"></div> 
</body>
</html>
Copy the code

Then do something to div#app in index.js.

// src/index.js
import { setNum } from './num'

$(function() {
  let num = 0
  const $app = $('#app')
  $app.text('Synchronize modification result:${num}`)

  setInterval(() = > {
    num = setNum(num) // Call setNum to update the value of num
    $app.text('Synchronize modification result:${num}`)},1e3)})Copy the code

Here the setNum method is called once a second, updating the value of the variable num, and then modifying the text of div#app. The setNum method is in the num.js file, and this is where we need to change it. By modifying this method, the page will be hot updated directly.

// src/num.js
export const setNum = (num) = > {
  return ++num // let num increment
}
Copy the code

When I changed the setNum method, I found that the page was refreshed directly, which did not achieve the desired hot update.

The official documentation doesn’t seem to say what else to do, which is confusing.

After some final documentation, it turns out that in addition to changing the devServer configuration, hot updates also need to tell WebPack in the code which modules need to be hot updated.

Module hot replacement: webpack.docschina.org/guides/hot-…

Similarly, we need to modify SRC /index.js to tell webpack that the SRC /num. Js module needs to be hot updated.

import { setNum } from './num'

if (module.hot) {
  // The num module needs a hot update
  module.hot.accept('./num'The $()}function() {
  ……
})
Copy the code

More on the module hot replacement API can be found here:

Module hot replacement (hot module replacement) – https://www.webpackjs.com/api/hot-module-replacement

If you didn’t configure WebPack manually like I did, you wouldn’t even notice this configuration using jQuery. In some loaders (style-loader, vue-loader, react-hot-loader), module hot API is called internally, which saves a lot of worries for developers.

Style-loader hot update code

Github.com/webpack-con…

Vue-loader hot update code

Github.com/vuejs/vue-l…

The principle of hot renewal

Before we talk about hot updates, we need to look at how WebPack packages files.

Webpack packaging logic

Let’s review the previous code and change the ESM syntax to require, because webpack also changes ESM to require internally.

// src/index.js
$(function() {
  let num = 0
  const $app = $('#app')
  $app.text('Synchronize modification result:${num}`)
  setInterval(() = > {
    num = require('./num').setNum(num)
    $app.text('Synchronize modification result:${num}`)},1e3)})// src/num.js
exports.setNum = (num) = > {
  return --num
}
Copy the code

As we all know, Webpack is essentially a packaging tool that packs multiple JS files into a single JS file. The following code is packaged with Webpack:

// webpackBootstrap
(() = > {
  // All modules are packaged in one object
  // Key is the file name, value is an anonymous function, and inside the function is the code in the file
  var __webpack_modules__ = ({
    "./src/index.js": ((module, __webpack_exports__, __webpack_require__) = > {
      "use strict";
      $(function() {
        let num = 0
        const $app = $('#app')
        $app.text('Synchronize modification result:${num}`)
        setInterval(() = > {
          num = (0,__webpack_require__("./src/num.js").setNum)(num)
          $app.text('Synchronize modification result:${num}`)},1e3)})}),"./src/num.js": ((module, __webpack_exports__, __webpack_require__) = > {
      "use strict";
      Object.assign(__webpack_exports__, {
        "setNum": (num) = > {
          return ++num
        }
      })
    })

  });

  // Implement a require method internally
  function __webpack_require__(moduleId) {
    // Execute the module function
    try {
      var module = {
        id: moduleId,
        exports: {}};// Take out the module and execute
      var factory = __webpack_modules__[moduleId]
      factory.call(module.exports, module.module.exports, __webpack_require__);
    } catch(e) {
      module.error = e;
      throw e;
    }
    // returns the executed exports
    return module.exports;
  }

  / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
  / / start
  // Load entry module and return exports
  __webpack_require__("./src/index.js");
})
Copy the code

Of course, the code above is simplified code, and webPack actually packages code with some caching, fault tolerance, ESM module compatibility, etc.

We can simply simulate the packaging logic of WebPack.

// build.js
const path = require('path')
const minimist = require('minimist')
const chokidar = require('chokidar')

const wrapperFn = (content) = > {
  return  `function (require, module, exports) {\n  ${content.split('\n').join('\n ')}\n}`
}

const modulesFn = (files, contents) = > {
  let modules = 'const modules = {\n'
  files.forEach(file= > {
    modules += `"${file}": ${wrapperFn(contents[file])},\n\n`
  })
  modules += '} '
  return modules
}
const requireFn = () = > `const require = function(url) { const module = { exports: {} } const factory = modules[url] || function() {} factory.call(module, require, module, module.exports) return module.exports }`

const template = {
  wrapperFn,
  modulesFn,
  requireFn,
}

module.exports = class Build {
  files = new Set()
  contents = new Object(a)constructor() {
    // Parse the parameters
    // index: template for entry HTML
    // Entry: package entry JS file name
    // output: output JS file name
    const args = minimist(process.argv.slice(2))
    const { index, entry, output } = args

    this.index = index || 'index.html'
    this.entry = path.join('/', entry)
    this.output = path.join('/', output)
    this.getScript()
  }

  getScript() {
    // Get all dependencies from the entry js file
    this.files.add(this.entry)
    this.files.forEach(file= > {
      const dir = path.dirname(file)
      const content = fs.readFileSync(file, 'utf-8')
      const newContent = this.processJS(dir, content)
      this.contents[file] = newContent
    })
  }

  processJS(dir, content) {
    let match = []
    let result = content
    const depReg = /require\s*\(['"](.+)['"]\)/g

    while((match = depReg.exec(content)) ! = =null) {
      const [statements, url] = match
      let newUrl = url
      // Add file suffixes manually if no file suffixes exist
      if(! newUrl.endsWith('.js')) {
        newUrl += '.js'
      }

      newUrl = path.join(dir, newUrl)
      // Replace the relative address in require with an absolute address
      let newRequire = statements.replace(url, newUrl)
      newRequire = newRequire.replace('('.` (/ *${url}* / `)
      result = result.replace(statements, newRequire)
      this.files.add(newUrl)
    }

    return result
  }

  genCode() {
    let outputJS = ' '
    outputJS += `/* all modules */${template.modulesFn(this.files, this.contents)}\n`
    outputJS += `/* require */${template.requireFn()}\n`
    outputJS += `/* start */require('The ${this.entry}')\n`

    return outputJS
  }
}
Copy the code
// index.js
const fs = require('fs')
const Build = require('./build')
const build = new Build()

// Generate the packaged code
const code = build.genCode()
fs.writeFileSync(build.output, code)
Copy the code

Startup code:

node index.js --entry ./src/index.js --output main.js
Copy the code

The generated code looks like this:

/* All modules are placed in one object. The key of the object is the module's file path; Object value is an anonymous function; * /
const modules = {
  "src/index.js": function (require.module.exports) {$(function() {
      let num = 0
      const $app = $('#app')
      $app.text('Synchronize modification result:${num}`)
      setInterval(() = > {
        num = require('./num').setNum(num)
        $app.text('Synchronize modification result:${num}`)},1e3)})},"src/num.js": function (require.module.exports) {
    exports.setNum = (num) = > {
      return ++num
    }
  },
}

/* Implement a require method internally, get modules from modules, inject require, module, exports, etc. */
const require = function(url) {
  const module = { exports: {}}const factory = modules[url] || function() {}
  factory.call(module.require.module.module.exports)
  return module.exports
}

/* Start the entry's index.js */
require('src/index.js')
Copy the code

In addition to packing all JS modules into a single file, the introduction of the HTMl-webpack-plugin also automatically inserts the generated output into the HTML.

new HtmlWebpackPlugin({
  template: './index.html'
})
Copy the code

Here we also add a method in build.js to simulate this behavior.

module.exports = class Build {
  constructor() {
    ……
  }
  genIndex() {
    const { index, output } = this
    const htmlStr = fs.readFileSync(index, 'utf-8')
    const insertIdx = htmlStr.indexOf('</head>')
    const insertScript = `<script src="${output}"></script>`
    // Insert the SRcript tag inside the head tag
    return htmlStr.slice(0, insertIdx) + insertScript + htmlStr.slice(insertIdx)
  }
}
Copy the code

To do the hot update, WebPack also needs to start a service itself to do the static file transfer. We use KOA to start a simple service.

// index.js
const koa = require('koa')
const nodePath = require('path')

const Build = require('./build')
const build = new Build()

// Start the service
const app = new koa()
app.use(async ctx => {
  const { method, path } = ctx
  const file = nodePath.join('/', path) 
  if (method === 'GET') {
    if (path === '/') {
      / / returns the HTML
      ctx.set(
        'Content-Type'.'text/html; charset=utf-8'
      )
      ctx.body = build.genIndex()
      return
    } else if (file === build.output) {
      ctx.set(
        'Content-Type'.'application/x-javascript; charset=utf-8'
      )
      ctx.body = build.genCode()
      return
    }
  }
  ctx.throw(404.'Not Found');
})

app.listen(8080)
Copy the code

After starting the service, you can see that the page is working properly.

node index.js --entry ./src/index.js --output main.js
Copy the code

Implementation of hot update

Webpack in hot update mode, after starting the service, the server will establish a long link with the client. After the file is modified, the server will push a message through the long link to the client. Upon receiving the message, the client will request a new JS file. The returned JS file will call the webpackHotUpdatehmr method, which replaces part of the code in __webpack_Modules__.

It can be seen from the experiment that the specific process of hot update is as follows:

  1. Webpack Server and Client establish a long link;
  2. Webpack listens for file changes and notifies the client through a long link.
  3. The Client requests the file again and replaces it__webpack_modules__Corresponding part of;

Creating long links

To establish a long link between the Server and Client, you can directly use the open-source solution of socket. IO.

// index.js
const koa = require('koa')
const koaSocket = require('koa-socket-2')

const Build = require('./build')
const build = new Build()

const app = new koa()
const socket = new koaSocket()

socket.attach(app) // Start the long link service

app.use(asyncCTX = > {...... }...// build.js
module.exports = class Build {
  constructor() {
    ……
  }
  genIndex(){...// Add socket. IO client code
    const insertScript = `
    <script src="/socket.io/socket.io.js"></script>
    <script src="${output}"></script>
    `... }genCode() {
    let outputJS = ' '...// Add code to listen for messages pushed by the server
    outputJS += '/* socket */ const socket = IO () socket.on('updateMsg', function (MSG){// Monitor server push message})\n'... }}Copy the code

Listening for file changes

When we implemented build.js earlier, we collected all the dependency files using the getScript() method. All you need to do here is listen on all dependent files through Chokidar.

// build.js
module.exports = class Build {
  onUpdate = function () {}
  constructor(){...// Get all js dependencies
    this.getScript()
    // Enable file listening
    this.startWatch()
  }
  startWatch() {
    // Listen for all dependent files
    chokidar.watch([...this.files]).on('change'.(file) = > {
      // Get the updated file
      const dir = path.dirname(file)
      const content = fs.readFileSync(file, 'utf-8')
      const newContent = this.processJS(dir, content)
      // Write the updated file to memory
      this.contents[file] = newContent
      this.onUpdate && this.onUpdate(file)
    })
  }
  onWatch(callback) {
    this.onUpdate = callback
  }
}
Copy the code

After the file is modified, the text content in build.contents is overridden, and the onUpdate method is triggered. So, when we start the service, we need to implement this method, and every time an update is triggered, we need to push a message to the client.

// index.js
const koa = require('koa')
const koaSocket = require('koa-socket-2')

const Build = require('./build')
const build = new Build()
const app = new koa()
const socket = new koaSocket()

// Start the long link service
socket.attach(app)

// After the file is modified, broadcast the modified file name to all clients
build.onWatch((file) = > {
  app._io.emit('updateMsg'.JSON.stringify({
    type: 'update', file
  }));
})
Copy the code

Requesting module update

After receiving the message, the client requests the module that needs to be updated.

// build.js
module.exports = class Build {
  genCode() {
    let outputJS = ' '...// Add code to listen for messages pushed by the server
    outputJS += `/* socket */ const socket = io() socket.on('updateMsg', Parse (MSG){const json = json.parse (MSG) if (json.type === 'update') {const json = json.parse (MSG) if (json.type === 'update') { Request to update the module of the fetch ('/update / + json. The file). Then (RSP = > RSP. The text ()). Then (text = > {eval (text) / / execution module})}}) \ n `... }}Copy the code

The /update/ related requests are then processed in the server middleware.

app.use(async ctx => {
  const { method, path } = ctx
  
  if (method === 'GET') {
    if (path === '/') {
      / / returns the HTML
      ctx.body = build.genIndex()
      return
    } else if (nodePath.join('/', path) === build.output) {
      // Returns the packaged code
      ctx.body = build.genCode()
      return
    } else if (path.startsWith('/update/')) {
      const file = nodePath.relative('/update/', path)
      const content = build.contents[file]
      if (content) {
        // Replace the files in modules
        ctx.body = `modules['${file}'] = ${ template.wrapperFn(content) }`
        return}}}}Copy the code

End result:

The complete code

👉 Shenfq/HRM

🔗 https://github.com/Shenfq/hmr

conclusion

This time, I realized an HMR by myself, which is definitely a little different from the real HMR of Webpack, but it is helpful to understand the principle of HMR. I hope you can gain something after reading the article.