preface

This article to analyze the source of Webpack-dev-server, from scratch to implement a Webpack hot update HMR, Dig deep into webpack-dev-server, Webpack-dev-middleware, and Webpack-hot-middleware to understand how they work. It’s a great idea to answer in a job interview, and a great idea to build scaffolding. Know what it is and why it is, and go to the next level.

Warm tips ❤️ ~ the length is longer, it is recommended to collect to the computer side to eat better.

Source link schematic link

What is HMR

1. The concept

Hot Module Replacement means that after the code is modified and saved, webpack will repackage the code and send the new Module to the browser. The browser will replace the old Module with the new Module to update the page without refreshing the browser.

2. The advantages

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

3. Let’s use it

./src/index.js

// Create an input into which you can type something so that we can see the effect of hot updates
let inputEl = document.createElement("input");
document.body.appendChild(inputEl);

let divEl = document.createElement("div")
document.body.appendChild(divEl);

let render = (a)= > {
    let content = require("./content").default;
    divEl.innerText = content;
}
render();

// To implement hot updates, this code is essential. It describes what happens when the module is updated
// How can vue-cli. Vue implement hot update without writing extra logic? That's because vue-loader does it for us. Many loaders implement hot updates
if (module.hot) {
    module.hot.accept(["./content.js"], render);
}
Copy the code

./src/content.js

let content = "hello world"
console.log("welcome");
export default content;
Copy the code

CD Project root directory

npm run dev

4. Look at the picture

When we enter 123 in the input field, update the code in content.js to find hello World!!!! It becomes Hello World, but the value of the input field remains, which is what HMR is all about, preserving state during page refresh

5. Understand chunk and Module

Chunk is a package composed of several modules. A chunk should contain multiple modules and generally form a file. For resources other than JS, Webpack will be converted into a module through various loaders, and this module will be packaged into a chunk instead of forming a separate chunk.

Webpack compilation

Webpack watch: Start webpack compilation using monitoring mode. In WebPack’s Watch mode, when a file in the file system is modified, WebPack detects the file change and recompiles and packages the module according to the configuration file. Each compilation generates a unique hash value.

1. HotModuleReplacementPlugin do what

  1. Generate two patch files
  • manifest(JSON)Hash.hot-update. json from the previous compilation(for example, b1f49e2fC76aae861d9f.hot-update.json)
  • updated chunk (JavaScript) The chunk name. Hash.hot-update.js generated by the last compilation(such as the main. B1f49e2fc76aae861d9f. Hot – update. Js)

    This is calling a globalwebpackHotUpdateFunction, pay attention to the structure of this JS

  • Yes this two files not webpack generated, but this plugin, you can remove the HotModuleReplacementPlugin try in the configuration file
  1. Inject the HMR Runtime runtime code into the chunk file: Our hot update client main logic (Pull new module code,Execute the new module code,Perform the accept callback for a local updateIt is the plugin that injects functions into our chunk files instead of Webpack-dev-server, which simply calls these functions

2. Understand packaging

Is to use the following code HotModuleReplacementPlugin compiler to generate the chunk, injected HMR runtime code, start the service NPM run dev, enter http://localhost:8000/main.js, Cut the main logic, save the details (first look, get a general impression)

(function (modules) {
  	//(HMR Runtime code) Module. hot is the result of the hotCreateModule function. All hot attributes include Accept and check
  	function hotCreateModule() {
        var hot = {
            accept: function (dep, callback) {
                for (var i = 0; i < dep.length; i++)
                    hot._acceptedDependencies[dep[i]] = callback;
            },
            check: hotCheck,// Call module.hot.accept to hotCheck in webpack/hot/dev-server.js
        };
        return hot;
    }
  	
    //(HMR Runtime code) The following methods are used to pull the updated module code
    function hotCheck(apply) {}
    function hotDownloadUpdateChunk(chunkId) {}
    function hotDownloadManifest(requestTimeout) {}

    //(HMR Runtime code) The following methods execute the new code and execute the Accept callback
    window["webpackHotUpdate"] = function webpackHotUpdateCallback(chunkId, moreModules) {
        hotAddUpdateChunk(chunkId, moreModules);
    };
    function hotAddUpdateChunk(chunkId, moreModules) {hotUpdateDownloaded(); }function hotUpdateDownloaded() {hotApply()}
    function hotApply(options) {}

    //(HMR Runtime code) hotCreateRequire assigns values to the module parents and children
    function hotCreateRequire(moduleId) {
      	var fn = function(request) {
            return __webpack_require__(request);
        };
        return fn;
    }
  
    // The module caches objects
    var installedModules = {};

    // Implement a require method
    function __webpack_require__(moduleId) {
        // Check whether the module is in the installedModules cache
        if (installedModules[moduleId]) {
            // In the cache, return the exported object of that module directly in the installedModules cache
            return installedModules[moduleId].exports;
        }
      
        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false.// Whether the module is loaded
            exports: {},  // The export object of the module
            hot: hotCreateModule(moduleId), // module.hot === hotCreateModule Exported object
            parents: [], // Which modules are referenced by this module
            children: [] // Which modules are referenced by this module
        };

        // (HMR Runtime code) executes the module's code, passing in arguments
        modules[moduleId].call(module.exports, module.module.exports, hotCreateRequire(moduleId));

        // Setup module is loaded
        module.l = true;

        // Returns the exported object of the module
        return module.exports;
    }
  
    // Expose the module's cache
    __webpack_require__.c = installedModules;

    // Load the entry module and return the exported object
    return hotCreateRequire(0)(__webpack_require__.s = 0); ({})"./src/content.js":
            (function (module, __webpack_exports__, __webpack_require__) {}),
        "./src/index.js":
            (function (module, exports, __webpack_require__) {}),// All require used in modules are compiled to __webpack_require__

        "./src/lib/client/emitter.js":
            (function (module, exports, __webpack_require__) {}),
        "./src/lib/client/hot/dev-server.js":
            (function (module, exports, __webpack_require__) {}),
        "./src/lib/client/index.js":
            (function (module, exports, __webpack_require__) {}),

        0:/ / the main entry
            (function (module, exports, __webpack_require__) {
                eval(` __webpack_require__("./src/lib/client/index.js"); __webpack_require__("./src/lib/client/hot/dev-server.js"); module.exports = __webpack_require__("./src/index.js"); `); })});Copy the code

Sort out the general process:

  • HotCreateRequire (0)(__webpack_require__.s = 0) main entry

  • When the browser executes this chunk, it passes each module a module object, structured as follows, and places this module object in the cache installedModules. We can retrieve the module cache object with __webpack_require__.c

      var module = installedModules[moduleId] = {
          i: moduleId,
          l: false.exports: {},
          hot: hotCreateModule(moduleId),
          parents: [].children: []};Copy the code
  • HotCreateRequire will help us assign values to the module’s parents and children

  • What does the hotCreateModule(moduleId) return? Yes, hot is an object with accept and check properties. Next, we will analyze module. Hot and module.hot

      function hotCreateModule() {
          var hot = {
              accept: function (dep, callback) {
              for (var i = 0; i < dep.length; i++)
                  hot._acceptedDependencies[dep[i]] = callback;
              },
              check: hotCheck,
          };
          return hot;
      }
    Copy the code

3. Talk about Module. hot and module.hot. Accept

1. Use the accept

The following code is essential if hot updates are to be implemented. The accept callback is the partial refresh logic that is executed when the./content.js module changes

if (module.hot) {
    module.hot.accept(["./content.js"], render);
}
Copy the code

2. Accept principle

Module.hot.accept ([“./content.js”], render); To implement hot updates, we start with the accept function. Let’s look at module.hot and module.hot

function hotCreateModule() {
    var hot = {
        accept: function (dep, callback) {
            for (var i = 0; i < dep.length; i++) hot._acceptedDependencies[dep[i]] = callback; }};return hot;
} 
var module = installedModules[moduleId] = {
    // ...
    hot: hotCreateModule(moduleId),
};
Copy the code

Accept stores a local update callback to hot._acceptedDependencies. (When the module file changes, we call the callback collected by acceptedDependencies.)

3. Look at the accept

// The following code is a bit clear
if (module.hot) {
    module.hot.accept(["./content.js"], render);
    // equivalent to module.hot._acceptedDependencies["./content.js"] = render
    < span style = "box-sizing: border-box! Important; word-wrap: break-word! Important;
    // So that when the content.js module changes, its parent module index.js knows what to do with _acceptedDependencies
}
Copy the code

Second, the overall process

1. The whole process is divided into client and server

2. BywebsocketEstablishes communication between browser and server

3. The server is divided into four key points

  • The Compiler instance is created with Webpack, which compiles in Watch mode
    • Compiler instances: listen for local file changes, automatically compile file changes, and compile output
    • Change the entry attribute in config to inject lib/client/index.js and lib/client/hot/dev-server.js into the chunk file that packages the output
    • Register events to compiler.hooks. Done hooks (triggered after webpack compilation is complete) : they fire to the clienthashandokThe event
  • Call Webpack-dev-Middleware to initiate compilation, set files to an in-memory file system, and have middleware that returns compiled files
  • Create a WebServer static server: Let browsers request compiled static resources
  • Create webSocket service: establish bidirectional communication between local service and browser; Tell the browser to perform hot update logic as soon as a new build is available

4. The client is divided into two key points

  • Create a WebSocket client to connect to the WebSocket server, and the WebSocket client listenshashokThe event
  • Major hot update client implementation logic, the browser will receive the server push message, if you need hot update, the browser has launched an HTTP request to the server for new module parse resources and partial refresh the page. (this is HotModuleReplacementPlugin help us to do, He injected the HMR runtime code into Chunk, but I’ll show you how to do thatHMR runtime)

5. Schematic diagram

Three, source code implementation

A, structure,

. ├ ─ ─ package - lock. Json ├ ─ ─ package. The json ├ ─ ─ the SRC │ ├ ─ ─ content. js test code │ ├ ─ ─ index. The js test code entry │ ├ ─ ─ lib │ │ ├ ─ ─ the client Hot update client implementation logic │ │ │ ├ ─ ─ index. The js is equivalent to the source code in webpack - dev - server/client/index. Js │ │ │ ├ ─ ─ emitter. Js │ │ │ └ ─ ─ hot │ │ │ └ ─ ─ Dev - server. Js is equivalent to the source code in webpack/hot/dev - server. Js and HMR runtime │ │ └ ─ ─ server hot update server implementation logic │ │ ├ ─ ─ for server js │ │ └ ─ ─ UpdateCompiler. Js │ └ ─ ─ myHMR webpack - dev - server. Js hot update server main entrance └ ─ ─ webpack. Config. Js webpack configuration fileCopy the code

Look at webpack.config.js

// /webpack.config.js

let webpack = require("webpack");
let HtmlWebpackPlugin = require("html-webpack-plugin")
let path = require("path");
module.exports = {
    mode: "development".entry:"./src/index.js".// Here we have not configured the client code, but instead change the entry property through the updateCompiler method
    output: {
        filename: "[name].js".path: path.resolve(__dirname, "dist")},plugins: [
        new HtmlWebpackPlugin(),// Output an HTML and import the packaged chunk
        new webpack.HotModuleReplacementPlugin()// Inject HMR Runtime code]}Copy the code

3. Dependent modules

"dependencies": {
    "express": "^ 4.17.1"."mime": "^ 2.4.4." "."socket.io": "^ 2.3.0." "."webpack": "^ 4.41.2"."webpack-cli": "^ 3.3.10"."memory-fs": "^ 0.5.0"."html-webpack-plugin": "^ 3.2.0",}Copy the code

Fourth, server implementation

  • / SRC/myhmr-webpack-dev-server. js Hot update server entry
  • / SRC/lib/server/server. Js server class is the main logic hot update server
  • . / SRC/lib/server/updateCompiler js change entry, add/SRC/lib/client/index, js and/SRC/lib/client/hot/dev – server. Js

1. Myhmr-webpack-dev-server.js

 // /src/myHMR-webpack-dev-server.js
 
 const webpack = require("webpack");
 const Server = require("./lib/server/Server");
 const config = require(".. /.. /webpack.config");
 
 // [1] Create webPack instance
 const compiler = webpack(config);
 // [2] Create a Server class that contains the main logic of the webpack-dev-server Server.
 const server = new Server(compiler);
 
 // Last step [10] Start the WebServer
 server.listen(8000."localhost", () = > {console.log(`Project is running at http://localhost:8000/`);
 })
Copy the code

2. Overview of the Server

// /src/lib/server/Server.js

const express = require("express");
const http = require("http");
const mime = require("mime");// The content-type can be generated according to the file suffix
const path = require("path");
const socket = require("socket.io");// Implement webSocket server through it and HTTP
const MemoryFileSystem = require("memory-fs");// Memory file system, whose main purpose is to package compiled files into memory
const updateCompiler = require("./updateCompiler");

class Server {
    constructor(compiler) {
        this.compiler = compiler;// Mount the WebPack instance to this
        updateCompiler(compiler);// [3] Entry Adds two files of the Websocket client to the chunk
        this.currentHash;// Hash for each compilation
        this.clientSocketList = [];// All webSocket clients
        this.fs;// will point to the memory file system
        this.server;// WebServer server
	      this.app;/ / express instance
        this.middleware;// Webpack-dev-middleware returns Express middleware to return compiled files

        this.setupHooks();// [4] Add webpack's done event callback, which will be triggered when compilation is complete; A message is sent to the client when compilation is complete, and two events are sent via webSocket to all WebSocket clients to tell the browser to pull the new code
      	this.setupApp();// [5] Create express instance app
        this.setupDevMiddleware();// [6] Webpack-dev-middlerware does the work of listening on local files, starting webpack compilation, setting the file system to in-memory file system (making the compilation output in memory), and there is a middleware responsible for returning compiled files
      	this.routes();// [7] Middleware returned by Webpack-dev-middlerware in app
        this.createServer();// [8] Create a Webserver that allows browsers to access compiled files
        this.createSocketServer();// [9] Create a WebSocket server, listen for connection events, store all webSocket clients, and send the latest compiled hash to the client by sending hash events
    }
    setupHooks() {}
    setupApp() {} 
    setupDevMiddleware() {}
    routes() {}
    createServer() {}
    createSocketServer() {}
    listen() {}// Start the server
}

module.exports = Server;
Copy the code

3. Change the Entry attribute of webpack to add the WebSocket client file and compile it into Chunk

Before we do webpack compilation, we call the updateCompiler(Compiler) method, which is crucial in sneaking two files into our chunk, Lib/client/client. Js and lib/client/hot – dev – server. Js

What are these two files for? When we say we use WebSocket to achieve two-way communication, our server will create a WebSocket server (discussed in step 9), every time the code changes, we will re-compile, generate a new compile file, at this time we webSocket server will notify the browser, you have to pull the new code

So a Websocket client, implementation and server communication logic, is there? So Webpack-dev-server gives us the client-side code, the two files above, and plants a spy for us to quietly pull new code and implement hot updates

Why split it into two files? Of course it is the module partition, balabala is not good to write in a lump, in the client implementation section I will elaborate on what these two files do

// /src/lib/server/updateCompiler.js

const path = require("path");
let updateCompiler = (compiler) = > {
    const config = compiler.options;
    config.entry = {
        main: [
            path.resolve(__dirname, ".. /client/index.js"),
            path.resolve(__dirname, ".. /client/hot-dev-server.js"),
            config.entry
        ]
    }
    compiler.hooks.entryOption.call(config.context, config.entry);
}
module.exports = updateCompiler;
Copy the code

The modified WebPack entry configuration is as follows:

{ 
    entry: {main: [
            'xxx/src/lib/client/index.js'.'xxx/src/lib/client/hot/dev-server.js'.'./src/index.js',}}Copy the code

4. Add webpackdoneEvent callback

We need to register an event on the compiler’s finished hook, which does one thing: each new compilation sends a message to all WebSocket clients, emitting two events that tell the browser to pull the code

The browser listens for these two events and pulls the hash. Hot-update. json generated by the previous compilation, which we’ll explain in detail in the client section below

// /src/lib/server/Server.js
setupHooks() {
    let { compiler } = this;
    compiler.hooks.done.tap("webpack-dev-server", (stats) => {
        // Each compilation produces a unique hash value
        this.currentHash = stats.hash;
        // A message is sent to all WebSocket clients whenever a new build is completed
        this.clientSocketList.forEach(socket= > {
            // Send the latest hash value to the client first
            socket.emit("hash".this.currentHash);
            // Send an OK to the client
            socket.emit("ok");
        });
    });
}
Copy the code

5. Create the Express instance app

setupApp() {
    this.app = new express();
}
Copy the code

6. Add Webpack-dev-middleware

About Webpack-dev-server and Webpack-dev-Middleware
  • The core of webpack-dev-server is to do the preparatory work (change entry, listen for Webpack done events, etc.), create webServer server and WebSocket server to establish communication between browser and server
  • Compile and file-related operations are pulled off to Webpack-dev-middleware
2. Webpack-dev-middleware does three main things (we’ll implement the logic ourselves)
  • Local file listening, start webpack compilation; In The Watch mode of Webpack, when a file in the file system is modified, Webpack detects the file change and recompiles and packages the module according to the configuration file.
  • Set the file system to an in-memory file system (to make compilation output in-memory)
  • Implements an Express middleware that returns compiled files
// /src/lib/server/Server.js
setupDevMiddleware() {
    let { compiler } = this;
    // It monitors file changes and recompiles the package whenever a file changes (CTRL + S)
    Hash.hot-update. json and chunk name.hash.hot-update.js are generated during compiling the output
    compiler.watch({}, () => {
        console.log("Compiled successfully!");
    });

    // Set the file system to memory and mount it to this for easy use in webServer
    let fs = new MemoryFileSystem();
    this.fs = compiler.outputFileSystem = fs;
  
    // Express middleware, which returns compiled files
    // Why not use express static middleware directly, because we need to read files in memory, so we can implement a simple version of static middleware
    let staticMiddleWare = (fileDir) = > {
        return (req, res, next) = > {
            let { url } = req;
            if (url === "/favicon.ico") {
              	return res.sendStatus(404);
            }
            url === "/" ? url = "/index.html" : null;
            let filePath = path.join(fileDir, url);
            try {
              	let statObj = this.fs.statSync(filePath);
                if (statObj.isFile()) {// Check whether it is a file or not.
                    // The path is the same as that used to be written to disk, but this is written to memory
                    let content = this.fs.readFileSync(filePath);
                    res.setHeader("Content-Type", mime.getType(filePath));
                    res.send(content);
              	} else {
                    res.sendStatus(404); }}catch (error) {
                res.sendStatus(404); }}}this.middleware = staticMiddleWare;// Mount the middleware on this instance for app use
}
Copy the code

7. Middleware returned by Webpack-dev-middlerware is used in app

routes() {
    let { compiler } = this;
    let config = compiler.options;// After webpack(config), the object exported from webpack.config.js will be hung on compiler.options
    this.app.use(this.middleware(config.output.path));// Middleware exported using webpack-dev-middleware
}
Copy the code

8. Create a WebServer

Allows the browser to request static resources compiled by WebPack

Using Express and native HTTP, you might wonder, right? Why not just use either Express or HTTP?

  • We don’t use Express directly, because we can’t get the server. We can look at the source code of Express, why we need the server, because we need to use the socket.
  • If you don’t use HTTP directly, you probably know that the native HTTP writing logic is just too bad. We have just written a simple static processing logic, so we can’t see anything, but there is a lot of logic in the source code, this is just to pick out the core logic implementation
  • We can create a service using native HTTP, and then we can get the server request logic. Express can handle the request logic of the server.this.server = http.createServer(app);One line of code is perfect
// /src/lib/server/Server.js
createServer() {
    this.server = http.createServer(this.app);
}
Copy the code

9. Create a WebSocket server

Use socket.js to set up a websocket long connection between the browser and server

// /src/lib/server/Server.js
createSocketServer() {
    // socket. IO + HTTP implements a websocket
    const io = socket(this.server);
    io.on("connection", (socket) => {
        console.log("a new client connect server");
        // Save all webSocket clients in order to send messages to the webSocket client after compilation (key for two-way communication)
        this.clientSocketList.push(socket);
        // Remove the WebSocket client whenever a client is disconnected
        socket.on("disconnect", () = > {let num = this.clientSocketList.indexOf(socket);
            this.clientSocketList = this.clientSocketList.splice(num, 1);
        });
        // Send the latest compiled hash to the client
        socket.emit('hash'.this.currentHash);
        // Send an OK to the client
        socket.emit('ok');
    });
}
Copy the code

10. Start the WebServer service and start listening

// /src/lib/server/Server.js
listen(port, host = "localhost", cb = new Function()) {
  	this.server.listen(port, host, cb);
}
Copy the code

Five, client implementation

  • / SRC /lib/client/index.js is responsible for listening for webSocket client hash and OK events. The ok callback only does one thing: emits webpackHotUpdate events

  • / SRC /lib/client/hot/dev-server.js is responsible for listening to webpackHotUpdate, calling hotCheck to start pulling code, and implementing local update

  • They pass/SRC/lib/client/emitter js share an EventEmitter instance

0. Emitter

// /src/lib/client/emiitter.js
const { EventEmitter } = require("events");
module.exports = new EventEmitter();
// The pattern of publishing subscriptions using Events is mainly decoupled
Copy the code

1. The index. Js

// /src/lib/client/index.js
const io = require("socket.io-client/dist/socket.io");// Websocket client
const hotEmitter = require("./emitter");// Shares an instance of EventEmitter with hot /dev/server.js, which is used to emit events
let currentHash;// The latest compiled hash

// [1] Connect to webSocket server
const URL = "/";
const socket = io(URL);

[2] The websocket client listens for events
const onSocketMessage = {
    // [2.1] Registers the hash event callback, which does one thing: get the latest compiled hash value
    hash(hash) {
        console.log("hash",hash);
        currentHash = hash;
    },
    // [2.2] Register the OK event callback to call reloadApp for hot updates
    ok() {
        console.log("ok");
        reloadApp();
    },
    connect() {
        console.log("client connect successfully!"); }};// Loop through onSocketMessage to register events with webSocket
Object.keys(onSocketMessage).forEach(eventName= > {
    let handler = onSocketMessage[eventName];
    socket.on(eventName, handler);
});

// [3] reloadApp emits webpackHotUpdate
let reloadApp = (a)= > {
    let hot = true;
  	// Whether hot updates are supported; We do this for hot updates, so simply set it to True
  	if (hot) {
        // Event notification: Emits the webpackHotUpdate event if supported
        hotEmitter.emit("webpackHotUpdate", currentHash);
    } else {
        // Refresh directly: Refresh the browser directly if not supported
        window.location.reload(); }}Copy the code

Webpack /hot/dev-server.js

Webpack-dev-server. js will change the entry configuration in updateCompiler(compiler) to webpack-dev-server/client/index.js? http://localhost:8080 is packaged in chunk with webpack/hot/dev-server.js, so let’s reveal the source code of hot/devserver.js

// source webpack/hot/dev-server.js
if (module.hot) {// Whether hot updates are supported
    var check = function check() {
    	module.hot
            .check(true)// Module.hot. check is the hotCheck function, see if the HRMPlugin injects the HMR Runtime code into the packed chunk
            .then( /* Log output */)
            .catch( /* Log output */)};// Shares an EventEmitter instance with client/index.js, which is used to listen for events
    var hotEmitter = require("./emitter");

    // Listen for the webpackHotUpdate event
    hotEmitter.on("webpackHotUpdate".function(currentHash) {
       	check();
    });
} else {
    throw new Error("[HMR] Hot Module Replacement is disabled.");
}
Copy the code

See, the real client hot update logic is HotModuleReplacementPlugin. Runtime runtime code, Through the module. Hot. Check = hotCheck webpack/hot/dev – server. Js and HotModuleReplacementPlugin injection in the chunk files hotCheck code puts up a bridge, etc

3. Overview of hot /dev/server.js

And the source of the discrepancy: source of hot/dev – server. Js is very simple, is to call the module. Hot. Check (namely HMR runtime hotCheck at runtime). HotModuleReplacementPlugin inserted code is the core hot update client

Then look at ourselves to achieve hot/dev – server. Js as a whole, we do not use HotModuleReplacementPlugin insert runtime code, but in hot/dev – server. Js ourselves again

let hotEmitter = require(".. /emitter");// Share an EventEmitter instance with client.js
let currentHash;// The hash generated by the latest compilation
let lastHash;// Indicates the hash generated by the previous compilation. The source code is hotCurrentHash, renamed to express its literal meaning

// [4] Listen for webpackHotUpdate events and check with hotCheck()
hotEmitter.on("webpackHotUpdate", (hash) => {
    hotCheck();
})

[5] call hotCheck to pull two patch files
let hotCheck = (a)= > {
    hotDownloadManifest().then(hotUpdate= >{ hotDownloadUpdateChunk(chunkID); })}// [6] Pull lashhash.hot-update.json, send Ajax request to server, server returns a Manifest file (lasthash.hot-update.json), The Manifest contains the hash value for the compilation and the chunk name for the module to be updated
let hotDownloadManifest = (a)= > {}
// [7] Pull the updated module chunkname.lashhash.hot-update. json, and obtain the updated module code through JSONP request
let hotDownloadUpdateChunk = (chunkID) = > {}

// [8.0] This hotCreateModule is important. The value of module.hot is the result of this function
let hotCreateModule = (moduleID) = > {
    let hot = {
        accept() {},
        check: hotCheck
    }
    return hot;
}

// [8] The webpackHotUpdate method will be called after the patch JS is retrieved (see the format of Update Chunk), which will implement hot update of the module
window.webpackHotUpdate = (chunkID, moreModules) = > {
    [9] Hot update key code implementation
}
Copy the code

4. Listen for the webpackHotUpdate event

And source code: the source is called check method, in the check method to call module.hot.check method – that is, hotCheck method, check inside will also carry out some log output. I’m going to write the core hotCheck method in check

hotEmitter.on("webpackHotUpdate", (hash) => {
    currentHash = hash;
    if(! lastHash) {// This is the first request
        return lastHash = currentHash
    }
    hotCheck();
})
Copy the code

5. hotCheck

let hotCheck = (a)= > {
    // [6] hotDownloadManifest is used to pull lasthash.hot-update.json
    hotDownloadManifest().then(hotUpdate= > {// {"h":"58ddd9a7794ab6f4e750","c":{"main":true}}
        let chunkIdList = Object.keys(hotUpdate.c);
        // [7] Call the hotDownloadUpdateChunk method through JSONP request to get the latest module code
        chunkIdList.forEach(chunkID= > {
            hotDownloadUpdateChunk(chunkID);
        });
        lastHash = currentHash;
    }).catch(err= > {
        window.location.reload();
    });
}
Copy the code

6. Pull patch code — lasthash.hot-update.json

// 6. Send an Ajax request to the server. The server returns a Manifest file (xxxlasthash.hot-update.json) containing the hash values and chunk names of all modules to be updated
let hotDownloadManifest = (a)= > {
    return new Promise((resolve, reject) = > {
        let xhr = new XMLHttpRequest();
        let hotUpdatePath = `${lastHash}.hot-update.json`
        xhr.open("get", hotUpdatePath);
        xhr.onload = (a)= > {
            let hotUpdate = JSON.parse(xhr.responseText);
            resolve(hotUpdate);// {"h":"58ddd9a7794ab6f4e750","c":{"main":true}}
        };
        xhr.onerror = (error) = >{ reject(error); } xhr.send(); })}Copy the code

7. Pull patch code — Updated module code lasthash.hot-update.json

The hotDownloadUpdateChunk method gets the latest module code via a JSONP request

Why JSONP? Since chunkname.lasthash.hot-update. js is a JS file, we want it to execute the JS script as soon as it gets it from the server

let hotDownloadUpdateChunk = (chunkID) = > {
    let script = document.createElement("script")
    script.charset = "utf-8";
    script.src = `${chunkID}.${lastHash}.hot-update.js`//chunkID.xxxlasthash.hot-update.js
    document.head.appendChild(script);
}
Copy the code

8.0 hotCreateModule

module.hot module.hot.accept module.hot.check

let hotCreateModule = (moduleID) = > {
    let hot = {// module.hot property value
        accept(deps = [], callback) {
            deps.forEach(dep= > {
                // Call ACCEPT saves the callback function in module.hot._acceptedDependencies
                hot._acceptedDependencies[dep] = callback || function () {}; })},check: hotCheck// module.hot.check === hotCheck
    }
    return hot;
}
Copy the code

8. WebpackHotUpdate implements hot update

So what does hotDownloadUpdateChunk look like

webpackHotUpdate("index", {
  "./src/lib/content.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      eval(""); })})Copy the code

Calling a webpackHotUpdate method means we need to have a webpackHotUpdate method globally

And source code: Source webpackHotUpdate inside will call hotAddUpdateChunk method dynamic update module code (with the new module to replace the old module), and then call hotApply method for hot update, here will be several method core directly written in webpackHotUpdate

window.webpackHotUpdate = (chunkID, moreModules) = > {
    // [9] Hot update
    // Loop the newly pulled module
    Object.keys(moreModules).forEach(moduleID= > {
        // the __webpack_require__.c module cache can be used to find the old module
        let oldModule = __webpack_require__.c[moduleID];

        // 2, update __webpack_require__.c to override the previous module with the moduleID
        let newModule = __webpack_require__.c[moduleID] = {
            i: moduleID,
            l: false.exports: {},
            hot: hotCreateModule(moduleID),
            parents: oldModule.parents,
            children: oldModule.children
        };

        // 3. Execute the module code generated by the latest compilation
        moreModules[moduleID].call(newModule.exports, newModule, newModule.exports, __webpack_require__);
        newModule.l = true;

        // Please review the principle of accept
        // execute _acceptedDependencies stored in the parent module
        newModule.parents && newModule.parents.forEach(parentID= > {
            letparentModule = __webpack_require__.c[parentID]; parentModule.hot._acceptedDependencies[moduleID] && parentModule.hot._acceptedDependencies[moduleID]() }); })}Copy the code

Webpack-dev -server, Webpack-hot-middleware, and Webpack-dev -middleware

Use Webpack-dev-middleware, Webpack-hot-middleware, Express to implement HMR Demo

1.Webpack-dev-middleware

  • Let webpack compile in Watch mode;

  • In addition, the file system is changed to an in-memory file system, which does not write packaged resources to disk but processes them in memory.

  • Middleware is responsible for returning compiled files;

2. Webpack – hot – middleware:

Provide a communication mechanism between the browser and the Webpack server, subscribe to and receive updates on the Webpack server side on the browser side, and then execute those changes using Webpack’s HMR API

1. The service side

  • The server listens for compiler.hooks. Done events;

  • Through SSE, the server sends building, Built, and sync events to the client after compiling.

    Webpack-dev-middleware uses an EventSource, also known as server-sent-Event (SSE), to send one-way messages to clients. The heartbeat detection is used to check whether the client is still alive. The SSE heartbeat detection 💓 is set on the server to send a setInterval to the client every 10 seconds

2. The client

  • Also the client code needs to be added to the Entry property of config,

    // /dev-hot-middleware demo/webpack.config.js
    entry: {
      	index: [
        		// Actively import client.js
        		"./node_modules/webpack-hot-middleware/client.js".// No need to introduce webpack/hot/dev-server, webpack/hot/dev-server is already integrated into the client.js module via require('./process-update')
        		"./src/index.js",]},Copy the code
  • Request / __webpack_hmr EventSource client creates an instance, monitor building, built, sync, the callback function will use HotModuleReplacementPlugin runtime code update;

3. Summary

  • In fact, we already implemented webpack-hot-middleware when implementing webpack-dev-server hot updates.
  • The biggest difference is the way the browser and the server communicate,webpack-dev-serverUsing thewebsocket.webpack-hot-middlewareUsing theeventSource; Webpack-dev-server uses hash and OK, while webpack-dev-middleware uses Build and sync to determine if a hot update process has started.

3. webpack-dev-server

Webpack-dev-server is built with Webpack-dev-Middleware and Express servers, and uses WebSocket instead of eventSource to implement Webpack-hot-middleware logic

4. The difference between

Why webpack-dev-server and webpack-dev-middleware with webpack-hot-middleware?

A: Webpack-dev-server is packaged, and it is difficult to customize development beyond webpack.config and command line parameters. When scaffolding, use Webpack-dev-middleware and Webpack-hot-middleware, as well as back-end services, to make your development more flexible.

Seven, source location

1. The service side

steps function Source link
1 Create a WebPack instance webpack-dev-server
2 Creating a Server instance webpack-dev-server
3 Change the entry property of config Server

updateCompiler
Entry to add dev – server/client/index. Js addEntries
Entry to add webpack/hot/dev – server. Js addEntries
4 Listen for webpack’s done event Server
After compiling, push message to websocket client, the most important information is the new modulehashThe following steps are based on thishashValue to perform module hot replacement Server
5 Create express instance app Server
6 Use the webpack – dev – middlerware Server
Start Webpack compilation in Watch mode. When a file in the file system is modified, WebPack detects the file change and recompiles and packages the module according to the configuration file webpack-dev-middleware
Set the file system to the memory file system webpack-dev-middleware
Returns a piece of middleware responsible for returning generated files webpack-dev-middleware
7 Middleware returned by Webpack-dev-middlerware in app Server
8 Create a WebServer server and start the service Server
9 Use SockJS to establish a websocket long connection between the browser and the server Server
Create a socket server and listen for connection events SockJSServer

2. The client

steps function Source link
1 Connect to the WebSocket server client/index.js
2 The WebSocket client listens for events client/index.js
Listen for the hash event and save the hash value client/index.js
Listen for the OK event and executereloadAppMethod to update client/index.js
3 Call reloadApp, and in reloadApp it will determine whether hot updates are supported, and if so, firewebpackHotUpdateEvent to refresh the browser if not supported client/index.js
ReloadApp emits the webpackHotUpdate event reloadApp
4 inwebpack/hot/dev-server.jsIt listens for the webpackHotUpdate event, webpack/hot/dev-server.js
The check() method is then executed to check webpack/hot/dev-server.js
This is called in the check methodmodule.hot.checkmethods webpack/hot/dev-server.js
5 Module.hot. check hotCheck HotModuleReplacement.runtime
6 Call hotDownloadManifest and send an Ajax request to the server. The server returns a Manifest file (lasthash.hot-update.json). The Manifest contains the hash value for the compilation and the chunk name for the module to be updated HotModuleReplacement.runtime

JsonpMainTemplate.runtime
7 Call the hotDownloadUpdateChunk method to get the latest module code via a JSONP request HotModuleReplacement.runtime

HotModuleReplacement.runtime

JsonpMainTemplate.runtime
8 Patch JS will be called after taking backwebpackHotUpdateMethod, which will be calledhotAddUpdateChunkMethod to replace the old module with a new one JsonpMainTemplate.runtime
9 callhotAddUpdateChunkMethod to dynamically update the module code JsonpMainTemplate.runtime

JsonpMainTemplate.runtime
10 callhotApplyMethod for hot update HotModuleReplacement.runtime

HotModuleReplacement.runtime
Remove old modules from the cache HotModuleReplacement.runtime
Execute the accept callback HotModuleReplacement.runtime
Executing a new module HotModuleReplacement.runtime

Viii. Flow chart

This is a flow chart for analyzing webpack-dev-server source code

Wrote last

Do not know whether comprehensive, if there is insufficient, welcome to correct.

The first article, if you have help and inspiration, but also hope to give a small praise yo ❤️ ~ charge me 🔋