As those of you who have used Webpack know, there’s a particularly handy “hot update” that pushes code to the browser without refreshing the page.

Today’s article will explore the secrets of the Webpack craze.

How do I configure hot updates

Let’s first install some of the packages we need:

npm i webpack webpack-cli -D
npm i webpack-dev-server -D
npm i html-webpack-plugin -D

Then, we need to understand that Webpack needs to start the service through the Webpack CLI after the Webpack version webpack@4, which provides commands to package and commands to start developing the service.

# webpack build --mode production --config webpack.config.js # webpack serve --mode development --config  webpack.config.js
// 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",}}

To turn on the hot update mode, configure the devServe property in the Webpack configuration file when starting the development service.

// 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, // port: 8080, // specify port number}, plugins: [ new HtmlWebpackPlugin({ template: './index.html' }) ] }

After the configuration is complete, we can start to create new files according to the following directory structure.

├─ SRC │ ├─ index.js ├─ num. Js ├─ index.html ├─ package.json ├─ webpack.config

Because DOM operation is needed here, for convenience, we directly use jQuery (YYDS) and introduce the CDN of jQuery into the HTML file.

<! DOCTYPE html> <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 >

Then operate on div#app in index.js.

// SRC /index.js import {setNum} from './num' $(function() {let num = 0 const $app = $('#app') $app. ${num} ') setInterval(() => {num = setNum(num) $app.text(' ${num} ')}, 1e3)})

The setNum method is called once a second to update the value of the variable num, and then to modify the text of div#app. The setNum method is in the num. Js file, and this is where we need to change the method so that the page will be hot updated directly.

// SRC /num. Js export const setNum = (num) => {return ++num // return num ++}

In the process of modifying the setNum method, we found that the page was flushed directly, and did not achieve the expected hot update operation.

The official documentation also doesn’t seem to say that there are any other configurations to be done, which is confusing.

Finally, after digging through the documentation, it turns out that in addition to changing the devServer configuration, the hot update also requires telling Webpack in the code which modules need to be hot updated.

Module hot replacement:
https://webpack.docschina.org…

Similarly, we need to change SRC /index.js to tell Webpack that the SRC /num.js module needs to be hot updated.

Import {setNum} from './num' if (module.hot) {// Num module needs to be updated module.hot.accept('./num')} $(function() {... })

More API information about module hot replacement can be found here:

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

If you don’t configure Webpack manually like I did, and use jQuery, you won’t notice this configuration at all. In some loaders (style-loader, vue-loader, reaction-hot-loader), the module hot API is called internally, but also for developers to save a lot of heart.

Style-loader hot update code

https://github.com/webpack-co…

Vue-loader hot updates the code

https://github.com/vuejs/vue-…

The principle of thermal renewal

Before we talk about hot updates, we need to take a look at how Webpack packages files.

Webpack packaging logic

Let’s review the previous code and change the ESM syntax to require because Webpack internally changes ESM to require as well.

// SRC /index.js $(function() {let num = 0 const $app = $('#app') $app. ${num} `) setInterval (() = > {num = the require ('. / num.) setNum (num) $app. The text (` synchronous change results: ${num}`) }, 1e3) }) // src/num.js exports.setNum = (num) => { return --num }

As we all know, Webpack is essentially a packaging tool that packages multiple JS files into one JS file. The following code is the Webpack packaged code:

// WebPackBootstrap (() => {// All modules are packed in one object // Key is the filename, value is an anonymous function, Var __webpack_modules__ = ({"./ SRC /index.js": ((module, __webpack_exports__, __webpack_require__) => { "use strict"; $(function() {let num = 0 const $app = $('#app') $app. ${num} `) setInterval (() = > {num = (0, __webpack_require__ (". / SRC/num. Js "), setNum) (num) $app. The text (` synchronous change results: ${num}`) }, 1e3) }) }), "./src/num.js": ((module, __webpack_exports__, __webpack_require__) => { "use strict"; Object.assign(__webpack_exports__, { "setNum": (num) => { return ++num } }) }) }); Function __webpack_require__(moduleId) {// Execute the module function try {var module = {id: moduleId, exports: {} }; / / remove the module performs var factory = __webpack_modules__ [moduleId] factory. The call (module) exports, the module, the module exports, __webpack_require__); } catch(e) { module.error = e; throw e; } // exports return module. Exports; } / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / / / start / / the Load entry module and the return of exports __webpack_require__("./src/index.js"); })

Of course, the above code is simplified code, and the actual Webpack packaged code will also have some caching, fault tolerance, and ESM module compatibility.

We can simply simulate Webpack’s packaging logic.

// 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() constructor()) {// exports = class Build {files = new Set() contents = new Object() constructor()); // output: // output: // output: 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() {output) this.getScript(); 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 // If (! newUrl.endsWith('.js')) { newUrl += '.js' } newUrl = path.join(dir, NewUrl) // Replace relative addresses in require with absolute addresses 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('${this.entry}')\n` return outputJS } }
// index.js cosnt fs = require('fs') const Build = require('./ Build ') const Build = new Build() // Generate the package const code =  build.genCode() fs.writeFileSync(build.output, code)

Start code:

node index.js --entry ./src/index.js --output main.js

The generated code looks like this:

/* All modules are put into one object. Object key is the file path of the module; Object value is an anonymous function; */ const modules = { "src/index.js": Function (require, module, exports) {$(function() {let num = 0 const $app = $('#app') $app. ${num} `) setInterval (() = > {num = the require ('. / num.) setNum (num) $app. The text (` synchronous modify results: ${num} `)}, 1 e3)})}, "SRC/num. Js" : Function (require, module, exports) {exports. SetNum = (num) => {return ++num}},} /* */ const require = function(url) {const module = {exports: {} } const factory = modules[url] || function() {} factory.call(module, require, module, Exports) return module.exports} /* index.js */ require(' SRC /index.js')

In addition to packaging all JS modules into one file, the HTML-webpack-plugin is introduced, and the generated output is automatically inserted into the HTML.

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

Here we have also added 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 = htmlst.indexof ('</head>') const insertScript = '< script SRC ="${output}"></script> '// Return htmlst. slice(0, insertIdx) + insertScript + htmlst. slice(insertIdx)}

To complete the hot update, Webpack also needs to start a service of its own to complete the transfer of static files. We started a simple service using KOA.

// 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('./', If (method === 'GET') {if (path === '/') {return 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)

After starting the service, you can see the page running normally.

node index.js --entry ./src/index.js --output main.js

Hot update implementation

Webpack is in hot update mode, when the service is started, the server establishes a long link with the client. After the file is modified, the server will push a message to the client through a long link. After the client receives the message, it will request a JS file again. The returned JS file will call the WebpackHotupDateMR 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 establishes long links with Client;
  2. Webpack listens for file changes and notifies the client via a long link;
  3. The Client rerequests the file and replaces it__webpack_modules__In the corresponding part;

Making Long Links

Server and Client need to establish a long link, can directly use the open source solution Socket. IO solution.

// 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) // Use (async CTX) = > {...... }... // exports = class build {constructor() {... } genIndex () {... // Add socket.io client const insertScript = '<script SRC ="/socket.io/socket.io.js"></script> <script SRC = "${output}" > < / script > `... } genCode() {let outputJS = "... // Add new code, OutputJS += '/* Socket */ const Socket = IO () Socket. On ('updateMsg', function (MSG){// ListenMessage})\n'... }}

Listen for file changes

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

// exports = class Build {onUpdate = function () {} constructor() {... // Listen to this.startWatch()} startWatch() {// Listen to all dependent files chokidar.watch([...this.files]).on('change', (file) => {// Get the new file const dir = path.dirname(file) const content = fs.readFileSync(file, 'utf-8') const newContent = this.processJS(dir, This.contents [file] = newContent this.onUpdate && this.onUpdate(file)})} onWatch(callback) {this.onUpdate [file] = newContent this.onUpdate(file)})} onWatch(callback) {this.onUpdate [file] = newContent this.onUpdate(file)})} this.onUpdate = callback } }

After the file is modified, the text 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 the 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). Build. OnWatch ((file) => {app._io. Emit ('updateMsg', json.stringify ({type: 'update', file})); })

Request update module

When the client receives the message, it requests the module that needs to be updated.

// exports = class Build {genCode() {let outputJS = "... // Add new code, OutputJS += '/* socket */ const socket = IO () socket.on('updateMsg'), Function (MSG){const json = json.parse (MSG) if (json.parse === '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 `... }}

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

app.use(async ctx => { const { method, Path} = CTX if (method === 'GET') {if (path === '/') {// return HTML ctx.body = build.genIndex() return} else if (nodePath.join('./', 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 file in modules ctx.body = 'modules['${file}'] = ${ template.wrapperFn(content) }` return } } } }

End result:

The complete code

👉 Shenfq/HRM

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

conclusion

This time I realized a HMR by feeling, and certainly Webpack real HMR or a little different, but for the understanding of the principle of HMR is still a little help, I hope you read the article after harvest.