Webpack compilers of the source code of our project are packaged into static resources that can be distributed online. In the development phase, if we want to preview the effect of the page, we need to start a server to compile the static resources compiled by webpack. Webpack-dev-server is used to start static resources such as webpack compilation and server.

In addition, it provides liveReload functionality by default, which automatically refreshes the page after a Webpack is compiled to read the latest compiled resources. To improve the development experience and efficiency, it also provides a hot option to enable hotReload, which, in contrast to liveReload, does not refresh the entire page, but only updates the modules that have been changed.



The service side

The webpack-dev-server service actually starts an Express server service in which the Compile object for webPack is passed.

The entrance

webpack-dev-server/index.js:

const webapck = require('webpack'); // config object const config = require('.. /webpack.config.js');
const Server = require('./lib/server/Server.js'); // Compiler object const compiler = webapck(config); // Create Server Server const Server = new Server(compiler); server.listen(9099.'localhost', () => {
  console.log('Service already started on port 9099 http://localhost:9099');
});
Copy the code

The core code

webpack-dev-server/lib/server/Server.js

const express = require('express');
const http = require('http');
const path = require('path');
// const MemoryFS = require('memory-fs');
const fs = require('fs-extra');
fs.join = path.join;
const mime = require('mime');
const socketIO = require('socket.io');
const updateCompilrer = require('.. /utils/updateCompilrer.js');

class Server {
  constructor (compiler) {
    this.compiler = compiler; // Save the compiler objectupdateCompilrer(compiler); / / injectionclientandserverCorrespondence filethis.currentHash; / / the currenthashValue, a new one is generated each time you compilehashvaluethis.clientSocketList= []; // Store all passeswebsocketThe client that connects to the serverthis.setupHooks(a); //webpackLifecycle listening hooksdoneEvent listenersthis.setupApp(a); / / createApp
    this.setupDevMiddleware(a);this.routes(a); // Configure the routethis.createServer(a); / / createHTTPServer, toappAs the routingthis.createSocketServer(a); / / createsocketServer}createSocketServer () {
    // websocketProtocol handshake is dependenthttpThe server'sconst io = socketIO(this.server); // The server must listen for the connection of the client.socket: represents the object connected to the clientio.on('connection', (socket) => {
      console.log('[New client connection completed]');
      this.clientSocketList.push(socket); // Add the new socket to the array socket.emit('hash', this.currentHash); / / newhashSend to client socket.emit('ok'); // Send an acknowledgement socket.on('disconnect', () => {
        let index = this.clientSocketList.indexOf(socket);
        this.clientSocketList.splice(index, 1);
      })})}routes () {
    let { compiler } = this;
    let config = compiler.options;
    this.app.use(this.middleware(config.output.path));
  }
  createServer () {
    this.server = http.createServer(this.app);
  }
  listen (port, host, callback) {
    this.server.listen(port, host, callback);
  }
  setupDevMiddleware () {
    this.middleware = this.webapckDevMiddleware(a); // return oneexpressMiddleware}webapckDevMiddleware () {
    let { compiler } = this; // Start compilation in listening mode and recompile if the file changes latercompiler.watch({},) => {
      console.log('Listen mode compiled successfully');
    }) / /let fs = new MemoryFS(a); // The packaged file is written to the in-memory file system and read from the in-memory file systemthis.fs = compiler.outputFileSystem = fs; // Returns a middleware request from the client for the output filereturn (staticDir) = > {return (req, res, next) = > {let { url } = req;
        if (url === '/favicon.ico') return res.sendStatus(404);
        url= = = '/'?url = '/index.html': null;
        let filePath = path.join(staticDir, url);
        try{// Return a description object for the file on this path, if not present, throw an exception let statObj = this.fs.statsync (filePath); // console.log('[statObj]', statObj);
          if(statObj.isFile()){ let content = this.fs.readFileSync(filePath); Res.setheader ('Content-Type', mime.getType(filePath)); // Set the corresponding header to tell the browser the contents of this file res.send(content); // Send the content to the browser}}catch(error){return res.sendStatus(404);
        } 
      }
    }
  }
  setupHooks () {
    let { compiler } = this;
    compiler.hooks.done.tap('webpack-dev-server', (stats) => {// stats is a description object that contains the packagedhash, chunkHash, contentHash code block module, etc console.log('[hash]', stats.hash);
      this.currentHash = stats.hash; / / compile successfully, to all the client radio enclosing clientSocketList. ForEach (socket = > {socket. Emit ('hash', this.currentHash); / / newhashSend to client socket.emit('ok'); })})} setupApp () {this.app HTTP application object this.app = express(); } } module.exports = Server;Copy the code

webpack-dev-server/utils/updateCompilrer.js

/** * Implement communication between Client and Server. Js * (webpack)-dev-server/client/index.js * (webapck)/hot/dev-server.js *./ SRC /index.js */ const path = require('path');
function updateCompilrer (compiler) {
  const config = compiler.options;
  config.entry = {
    main: [
      path.resolve(__dirname, '.. /.. /client/index.js'),
      path.resolve(__dirname, '.. /.. /.. /webpack/hot/dev-server.js'),
      config.entry, // ./src/index.js
    ]
  }
}

module.exports = updateCompilrer;
Copy the code

Taken together, the Server service does several things

  1. Bind the Webpack Compiler hooks, which are mainly focused on done hooks, and webscoket will broadcast the compilation information of Webpack after the compilation of the Webpack Compiler instance is triggered each time. See setupHooks for details
  2. Instantiate the Express server, setupApp, which assigns the express to this.app. The createServer function actually creates the service
  createServer () {
    this.server = http.createServer(this.app);
  }
Copy the code
  1. The webapckDevMiddleware function creates a closure function that returns an Express middleware request. The webapckDevMiddleware function creates a closure function that returns an Express middleware request. StaticDir is the incoming output path, the middleware handles the request, gets the requested path in the URL, gets the requested file in the requested path, and returns the file contents in the response.

The webpackMiddleware function returns an Expressjs middleware that has the following functionality:

  • Receives files from the output of the Webpack Compiler instance, but does not export the files to disk, but saves them in memory.
  • Expressjs app registers a route, intercepts HTTP requests, and responds to the corresponding file content according to the request path;
	return (staticDir) => {
      return (req, res, next) => {
        let { url } = req;
        if (url === '/favicon.ico') return res.sendStatus(404);
        url === '/' ? url = '/index.html' : null;
        let filePath = path.join(staticDir, url);
        try{// Return a description object for the file on this path, if not present, throw an exception let statObj = this.fs.statsync (filePath); // console.log('[statObj]', statObj);
          if(statObj.isFile()){ let content = this.fs.readFileSync(filePath); Res.setheader ('Content-Type', mime.getType(filePath)); // Set the corresponding header to tell the browser the contents of this file res.send(content); // Send the content to the browser}}catch(error){return res.sendStatus(404); }}}Copy the code
  1. Create socket communication, focusing on the createSocketServer function, which creates WebSocket communication depending on the HTTP server you just created.
CreateSocketServer () {// WebSocket protocol handshake is required to rely on HTTP server const IO = socketIO(this.server); // The server listens for the connection of the client. When the client is connected, socket: represents the object connected to the client IO.'connection', (socket) => {
      console.log('[New client connection completed]'); this.clientSocketList.push(socket); // Add the new socket to the array socket.emit('hash', this.currentHash); / / newhashSend to client socket.emit('ok'); // Send an acknowledgement socket.on('disconnect', () => {
        let index = this.clientSocketList.indexOf(socket);
        this.clientSocketList.splice(index, 1); })})}Copy the code

At this point, the logic on the server side is created and the service is started by running index.js in the entry file.



The client

When we edit the source code, we trigger a WebPack recompilation and execute the callback on the Done hook once the compilation is complete. See setupHooks method in server.js above for details. Stats is a description object that contains packaged hash, chunkHash, contentHash code blocks, and so on, saving the hash first. Broadcast a message of type hash to all linked clients, and broadcast warnings/errors/ OK messages based on the compilation information. Here we will focus only on the normal flow OK message. webpack-dev-server/client/index.js

let io = require('socket.io'); // The client records the currenthashValue the let currentHash;class EventEmitter {
  constructor() {this.events = {};
  }
  on(eventName, fn) {this.events[eventName] = fn;
  }
  emit(eventName, ... args) {
    this.events[eventName] (. args); }}let hotEmitter = new EventEmitter() // 1websocketThe serverconst socket = io('/');
socket.on('hash', (hash) => {
  currentHash = hash;
})

socket.on('ok', () => {
  console.log('[ok]');
  reloadApp();
})

function reloadApp () {
  hotEmitter.emit('webpackHotUpdate');
}
Copy the code

Client /index.js basically initializes the Webscoket client and sets up callback functions for different message types.

The server broadcasts the hash message after each compilation, and the client stores the hash value generated by the Webpack compilation temporarily after receiving it. An OK message is broadcast if the compilation is successful without warning or error, and the client receives the OK message and executes the reloadApp in the OK callback to refresh the application. The refresh application emits a webpackHotUpdate event in publish-subscribe mode.

webpack/hot/dev-server.js

let lastHash;

hotEmitter.on('webpackHotUpdate', () = > {if(! lastHash || lastHash == currentHash) {return lastHash = currentHash
  }
  hotCheck()
});

Copy the code

What dev-server.js does is subscribe to webpackHotUpdate and set hotCheck callback. Webpack broadcasts hash events and compiled OK events to Socktes when compilation is complete. When the client receives the OK event, it sends webpackHotUpdate in publish-subscribe mode, which triggers the hotCheck() function

The above code is what we talked about above and is injected into bundle.js when WebPack is compiled.

The final packaged code should look like this:

// The client records the currenthashValue the let currentHash; let lastHash; // Last timehashvalueclass EventEmitter {
  constructor() {this.events = {};
  }
  on(eventName, fn) {this.events[eventName] = fn;
  }
  emit(eventName, ... args) {
    this.events[eventName] (. args); }}let hotEmitter = new EventEmitter(a); (function(modulesVar installedModules = {}; function hotCheck ({/ / {"h":"e4113d05239b9e227b26"."c": {"main":true}}
    hotDownloadManifest().then(update => {
      let chunkIds = Object.keys(update.c);
      chunkIds.forEach(chunkId => {
        hotDownloadUpdateChunk(chunkId);
      })
      lastHash = currentHash;
    }).catch(() => {
      window.location.reload();
    })
  }
  function hotDownloadUpdateChunk (chunkId) {
    let script = document.createElement('script');
    script.src = `${chunkId}.${lastHash}.hot-update.js`;
    document.head.appendChild(script);
  }
  window.webpackHotUpdate = (chunkId, moreModules) => {
    hotAddUpdateChunk(chunkId, moreModules);
  }
  let hotUpdate = {};
  function hotAddUpdateChunk (chunkId, moreModules) {
    for(let moduleId in moreModules){
      modules[moduleId] = hotUpdate[moduleId] = moreModules[moduleId];
    }
    hotApply();
  }
  function hotApply () {
    for(let moduleId in hotUpdate){ let oldModule = installedModules[moduleId]; // Delete installedModules[moduleId]; / / delete the old modules / / circulation in the cache the father of all modules, remove the parent module callback callback, have execute oldModule parents. ForEach (parentModule => {
        let cb = parentModule.hot._acceptDependencies[moduleId];
        cb&&cb();
      })
    }
  }
  function hotDownloadManifest () {
    return new Promise((resolve, reject) => {
      let xhr = new XMLHttpRequest();
      let url = `${lastHash}.hot-update.json`;
      xhr.open('get', url);
      xhr.responseType = 'json';
      xhr.onload = function () {
        resolve(xhr.response)
      }
      xhr.send();
    })
  }
  function hotCreateModule () {
    let hot = {
      _acceptDependencies: {},
      accept (deps, callback) {
        deps.forEach((dep) => {
          hot._acceptDependencies[dep] = callback;
        })
      },
      check: hotCheck
    }
    returnhot; } function hotCreateRequire (parentModuleId(parentModule = installedModules[parentModuleId]) {// Let parentModule = installedModules[parentModuleId]; // If parentModule is not present in the cache, the submodule is the top-level moduleif (! parentModule) return __webpack_require__;
    let hotRequire = function (childModuleId) {
      __webpack_require__(childModuleId); // let childModule = installedModules[childModuleId]; Childmodule.parents.push (childmodule.parents.push (parentModule);
      // console.log('[childModule]', childModule);
      parentModule.children.push(childModule);
      return childModule.exports;
    }
    return hotRequire;
  }
  function __webpack_require__(moduleId){// Cache hit, return directlyif(installedModules[moduleId]) {returnInstalledModules [moduleId]} // Create a new module object and cache it in the cache let Module = installedModules[moduleId] = {I: moduleId, // module ID l: Exports: {}, // parents: [], // children: [], // Hot: hotCreateModule()),
    }
    modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId)); module.l = true; // The module has been loadedreturn module.exports;
  }
  return hotCreateRequire("./src/index.js") ("./src/index.js") / /return __webpack_require__("./src/index.js")}) ({
  "./src/index.js": function (module, exports, __webpack_require__) {// Listen for webpackHotUpdate message __webpack_require__("webpack/hot/dev-server.js"); // Connect to webSocket server, the server sends tohashIf the server sends OK, the webpackHotUpdate event __webPack_require__ ("webpack-dev-server/client/index.js");
    let input = document.createElement('input');
    document.body.appendChild(input);

    let div = document.createElement('div');
    document.body.appendChild(div);

    let render = () => {
      let title = __webpack_require__('./src/title.js');
      div.innerHTML = title;
    }
    render();
    if(module.hot){
      module.hot.accept(['./src/title.js'],render); }},"./src/title.js": function(module, exports){
    module.exports = "title"
  },
  "webpack-dev-server/client/index.js": function(module, exports{/ /1Const socket = window.io('/');
    socket.on('hash', (hash) => {
      currentHash = hash;
    })

    socket.on('ok', () => {
      console.log('[ok]');
      reloadApp();
    })

    function reloadApp () {
      hotEmitter.emit('webpackHotUpdate'); }},"webpack/hot/dev-server.js": function(module, exports){
    hotEmitter.on('webpackHotUpdate', () => {// First renderif (! lastHash) {
        lastHash = currentHash
        return; } // Call hot.check to check the server for updates and pull the latest code module.hot.check();
    }); }})
Copy the code

So what does this hotCheck method basically do? Here’s a quick summary. Used in webpack HotModuleReplacementPlugin at compile time, and each incremental compilation can output two files, like c390bbe0037a0dd079a6. Hot – update. Json, Main. C390bbe0037a0dd079a6. Hot – update. Js, respectively is to describe the chunk update the manifest file and update the chunk files. Then the browser calls the hotDownloadManifest method to download the manifest.json file updated by the module, and then calls the hotDownloadUpdateChunk method to download the chunk to be updated by jSONP.

HotCheck communicates with the server to get the updated chunk, and then executes the webpackHotUpdate callback in the HMR Runtime after the chunk is downloaded.

  window.webpackHotUpdate = (chunkId, moreModules) => {
    hotAddUpdateChunk(chunkId, moreModules);
  }
  let hotUpdate = {};
  function hotAddUpdateChunk (chunkId, moreModules) {
    for(let moduleId in moreModules){
      modules[moduleId] = hotUpdate[moduleId] = moreModules[moduleId];
    }
    hotApply();
  }
  function hotApply () {
    for(let moduleId inhotUpdate){ let oldModule = installedModules[moduleId]; // Delete installedModules[moduleId]; // Remove the old module from the cache // loop through all the parent modules, retrieve the parent module callback, Have execute oldModule. Parents. ForEach (parentModule = > {let cb = parentModule. Hot. _acceptDependencies [moduleId]; cb&&cb(); }}})Copy the code

Finally, we come to the hotApply method, which applies the updated modules to the business. After looking at the WebPack Runtime code, we know that the Runtime declares installedModules. It caches all modules loaded after the webpack_require call, and the modules variable stores all modules. If you are not familiar with WebPack Runtime, you can understand the implementation mechanism of WebPack Runtime. If a module is accepted, the old module is removed from installedModules, the parent module is removed from the parent dependency, and the module in the module is replaced with the new module.

Program source code

Address: webpack – hotReload

How to start

  1. Install dependencies

npm i

  1. Start the service

NPM run dev starts a webpack-dev-server service that packages up the dist folder and starts an HTTP service with the dist as the path resource folder

  1. Move hmr.html and HRM.js from the dist1 package to dist

Mv dist1/* dist/ This step is due to the complexity of the webpack code injecting hot updates to the client, so the main process events are written to hRM.js and the hot updates can be achieved by accessing this file directly at that time

  1. Open your browser and go to localHost9099 /hmr.html

  2. Change the title of the SRC /title.js content and observe that the browser input automatically changes

The following communication process occurs between the server and client:



conclusion

To wrap up, Webpack-dev-server can be used as a command-line tool, with core module dependencies on Webpack and Webpack-dev-middleware. Webapck-dev-server is responsible for starting an Express server to listen for client requests; Instantiate Webpack Compiler; Start the Webscoket server that pushes webpack build information The Webscoket client code and processing logic responsible for injecting bundle.js into and communicating with the server. Webapck-dev-middleware changes the outputFileSystem of webpack compiler to in-memory fileSystem; Start webPack Watch compilation; Handles requests for static resources from the browser and responds to webpack output to the file in memory.

After webpack compilation is complete, an OK message is broadcast to the client. After receiving the message, the client uses liveReload page-level refresh mode or hotReload module hot replacement according to whether hot mode is enabled. HotReload fails, in which case it degrades to page level refresh. Enable the HOT mode, that is, enable the HMR plug-in. Hot mode will request the updated module to the server, and then go back to the parent module of the module to determine the dependent path. If each dependent path is configured with the business processing callback function required after the module update, it is in the accepted state. Otherwise, the page will be degraded and refreshed. After determining the accepted status, replace and delete the old cache module and parent-child dependent module, then execute the accept method callback function, execute the new module code, introduce the new module, and execute the business processing code.

To get more familiar with the full compilation process, initialize a webpack-dev-server project and read the source code using vscode debug for breakpoints.

References:

  • Use DevServer- Get the basics out of Webpack
  • Use Webpack Dev Middleware- Get simple Webpack
  • How does webpack-dev-server work