1. Introduction

After adding plugins, loaders and other processes to Webpack, the overall process has been basically completed. Let’s look at the outer circle of Webpack: very important, but not so low-level plugins.

Today’s plugins are webpack-dev-middleware and Webpack-hot-middleware, which are hot updates to Webpack

2. Benefits of hot updates

The advantage of hot updates is that you can do partial updates at development time without having to manually refresh the page. There are two points here: server and local updates without refreshing the page. We know of a plugin called Webpak-dev-server that serves two main purposes: to provide web services for compiled static files; Provide hot updates and hot replacements. What I want to analyze today is to replace webpack-dev-server and implement one myself.

3. How should we implement a hot update?

No matter how other people’s source code to write fancy, they have to find the underlying implementation logic, otherwise see more, not necessarily understand through.

The realization principle, I think, comes from two aspects:

1. You need a server that, when a request comes in, can pick up the latest build of Webpack and send it back to the browser (Webpack-dev-Middleware + Express).

  1. Webpack-hot-middleware how to communicate with the browser and how to get Webpack-hot-middleware

To put it bluntly, that is: how to unify webpack, static server and browser.

With that in mind, let’s take a look at some of the source code for Webpack-dev-middleware and Webpack-hot-middleware.

4. Source logic implementation

4.1 What does Webpack-dev-Middleware do

This plug-in returns a function that supports the Express interface. It is also the middleware of Express.

When a request comes in and we need to be able to get the latest build results, we also need to do the following:

  • You need to enable webpack’s Watch function to automatically recompile when business code changes;
  • You need to change webPack’s default output location from hard disk to memory storage, which will be much faster;
  • Returns a middleware function of Express that enables it to retrieve data stored in memory on request and return it to the browser;

These three points are exactly what this plugin does. Now let’s go back to the source code for verification.

  1. Webpack-dev-middleware github returns a function that can be seen in the code:
// index.js

import middleware from "./middleware";
// Since it is a plugin, it must receive compiler, i.e. an instance of Webpack
export default function wdm(compiler, options = {}) {
    // Set the hook to listen for events such as watch
    setupHooks(context);
    // Process the path where the packaged output file is stored
    setupOutputFileSystem(context);
    // Enable the watch listener
      if (context.compiler.watching) {
        context.watching = context.compiler.watching;
      } else {
        let watchOptions;
        if (Array.isArray(context.compiler.compilers)) {
          watchOptions = context.compiler.compilers.map(
            (childCompiler) = > childCompiler.options.watchOptions || {}
          );
        } else {
          watchOptions = context.compiler.options.watchOptions || {};
        }

        context.watching = context.compiler.watch(watchOptions, (error) = > {
          if (error) {
            / /...context.logger.error(error); }}); }// Instance is an exposed Express middleware function
    const instance = middleware(context);
    instance.getFilenameFromUrl = (url) = > getFilenameFromUrl(context, url);
      // Exposed API
      instance.waitUntilValid = (callback = noop) = > {
        ready(context, callback);
      };

      instance.invalidate = (callback = noop) = > {
        ready(context, callback);

        context.watching.invalidate();
      };

      instance.close = (callback = noop) = > {
        context.watching.close(callback);
      };

      instance.context = context;
    return instance

}

// middleware.js
import getFilenameFromUrl from "./utils/getFilenameFromUrl";
import handleRangeHeaders from "./utils/handleRangeHeaders";
import ready from "./utils/ready";
export default function wrapper(context) {
    // This is a standard Express middleware
    return async function middleware(req, res, next) {
         ready(context, processRequest, req);
         async function processRequest() {
             // ...
             try {
                 // Get file contents from memory by default (you can also make your own disk, but access will be slow)
                content = context.outputFileSystem.readFileSync(filename);
            } catch (_ignoreError) {
                await goNext();
                return; }}// Call the Express API to send to the browser
         if(res.send) { res.send(content); }}}// See how this works
// **setupHooks.js**
export default function setupHooks(context) {
    / /... Pre-logic or method
  // Listen on watchRun (in listen mode, after a new compilation is triggered, but before the compilation actually starts)
  context.compiler.hooks.watchRun.tap("webpack-dev-middleware", invalid);
  // Handle invalid listener cases (executed when a compilation under observation is invalid)
  context.compiler.hooks.invalid.tap("webpack-dev-middleware", invalid);
  The compilation is executed when the compilation is completed and when the done hook is triggered
  (context.compiler.webpack
    ? context.compiler.hooks.afterDone
    : context.compiler.hooks.done
  ).tap("webpack-dev-middleware", done);
}


Copy the code

With these crucial steps, the Webpack-dev-Middleware framework is in place. You can go to the source code to take a closer look. I believe that according to my above analysis, the recognition of this plugin will get twice the result with half the effort.

I’ll rewrite the Webpack-dev-Middleware wheel myself, so stay tuned.

4.2 What does Webpack-hot-Middleware Do

Webpack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack -> WebPack In order to achieve partial refresh, it does seem to be more effective to use the long link processing method, otherwise the communication will be broken once, and you need to reconnect, which will be more troublesome, the server can’t push to the browser.

If we do long links, all that’s left is a partial update of the browser. It’s not that hard to do local updates. You don’t need to think too hard about local DOM updates. In the most common ajax case, we take the new content, update the innerHTML of a DOM, and the browser redraws it, and the page is rendered without refreshing the page. In webPack, too, there must be some mechanism by which a module’s DOM is finally updated.

To update from the DOM of a module, the browser needs to know which module to update, and to know which module has changed. The browser needs to pull the changed module, remove the original, and render the latest module.

So it seems that we have a plausible link: the browser and the server establish a long link -> when there is a new output, the server needs to send the updated content or logo to the browser -> the browser gets the latest module -> render the new module -> partial hot update is complete.

Ok, so there are still points in this link that we need to refine and clarify: does the server send content or what identity to the browser? Does the browser simply take the new output and execute it?

This question, I think all is ok.

In Webpack-hot-middleware, it’s processed separately. Let’s take a look at the context of this plugin:

    function webpackHotMiddleware(compiler, opts) {
        / /...
        var middleware = function (req, res, next) {
            // ...
        }
        return middleware;
    }

Copy the code

Like Webpack-dev-middleware, I passed in an instance of Webpack and returned an Express middleware that I could use in WebPack:

    const express = require('express');
    const webpackHotMiddleware = require("webpack-Hot-middleware");
    
    const hotMiddleware = webpackHotMiddleware(compiler, {
        heartbeat: 2000,
    })
    express.use(hotMiddleware);

Copy the code

Let’s continue with a detailed analysis of the important context in middleware:

    function webpackHotMiddleware(compiler, opts) {
        // Create eventStram, which is needed to communicate with the browser
        var eventStream = createEventStream(opts.heartbeat);
        var latestStats = null;
        var closed = false;
        // Also listen for hooks, which trigger the corresponding action
        if(compiler.hooks) {
            compiler.hooks.invalid.tap('webpack-hot-middleware', onInvalid);
            compiler.hooks.done.tap('webpack-hot-middleware', onDone);
        } else {
            compiler.plugin('invalid', onInvalid);
            compiler.plugin('done', onDone);
        }
        // Execute the logic when the request comes in
        var middleware = function (req, res, next) {
            eventStream.handler(req, res);
            if (latestStats) {
                // Explicitly not passing in `log` fn as we don't want to log again on
                // the server
                publishStats('sync', latestStats, eventStream); }}// Static method publish
        middleware.publish = function (payload) {
            if (closed) return;
            eventStream.publish(payload);
        };
        // Close the method
        middleware.close = function () {};return middleware;
    }

Copy the code

It’s important to look at the createEventStream method in more detail.

function createEventStream(heartbeat) {
  var clientId = 0;
  var clients = {};
  function everyClient(fn) {
    Object.keys(clients).forEach(function (id) {
      fn(clients[id]);
    });
  }
  var interval = setInterval(function heartbeatTick() {
    everyClient(function (client) {
      // Here is the 💓 symbol, more on this later
      client.write('data: \uD83D\uDC93\n\n');
    });
  }, heartbeat).unref();
  
  return {
     // Close the connection stream
    close: function () {
      clearInterval(interval);
      everyClient(function (client) {
        if(! client.finished) client.end(); }); clients = {}; },handler: function (req, res) {
      // Headers is prepared for cross-domain processing
      var headers = {
        'Access-Control-Allow-Origin': The '*'.'Content-Type': 'text/event-stream; charset=utf-8'.'Cache-Control': 'no-cache, no-transform'.// While behind nginx, event stream should not be buffered:
        // http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
        'X-Accel-Buffering': 'no'};// If it is http1, then keepAlive needs to be set manually
      varisHttp1 = ! (parseInt(req.httpVersion) >= 2);
      if (isHttp1) {
        req.socket.setKeepAlive(true);
        Object.assign(headers, {
          Connection: 'keep-alive'}); } res.writeHead(200, headers);
      res.write('\n');
      var id = clientId++;
      clients[id] = res;
      req.on('close'.function () {
        if(! res.finished) res.end();delete clients[id];
      });
    },
    // Send data to the client
    publish: function (payload) {
      everyClient(function (client) {
         // Send data to the client, more on that later
        client.write('data: ' + JSON.stringify(payload) + '\n\n'); }); }}; }Copy the code

Let’s look at the handling of the Webapck hook:

function onInvalid() {
    if (closed) return;
    latestStats = null;
    if (opts.log) opts.log('webpack building... ');
    // Send the status of the package to the browser
    eventStream.publish({ action: 'building' });
}
function onDone(statsResult) {
    if (closed) return;
    // Keep hold of latest stats so they can be propagated to new clients
    latestStats = statsResult;
    // Send module information to the browser (module name, hash, etc.)
    publishStats('built', latestStats, eventStream, opts.log);
}
Copy the code

What are these operations doing? Don’t seem to understand? In fact, this is one of the steps we discussed above: what is sent to the browser when the new result is compiled. Looking at this section, you can see that instead of sending the new output directly, you send stats, which is the changed module information, including the module name, the module’s new hash, and so on, to the client via heartbeat detection and listening for Webpack hooks.

What’s the use of all this stuff? Which brings us to the next step: browser updates.

The browser takes that data and actually requests the real module data in jSONP form, which is why access-Control-Allow-Origin was added to the cross-domain request processing we saw above.

This is where you need to take a closer look in client.js.

What does client.js do

This JS actually needs to be manually configured in the Webpack and bundled into the bundle.

module.exports = {
    entry: [
    'webpack-hot-middleware/client? noInfo=true&reload=true'.'Project entry file... '],}Copy the code

The added significance is that the first time the browser parses our package file, it will also execute the client.js in passing and establish a long connection between the browser and the server.

Take a closer look at what client.js does:

I’m running a little late, so I’ll stop there. Welcome to follow me, I will fill as soon as possible ~~