webpack-hot-middleware初探

一直感觉hot module replacement的特性挺神奇,所以这里初步探究下webpack-hot-middleware,这个模块

首先地址,https://github.com/glenjamin/...,当前版本为2.13.2,为了配合webpack2吧,肯定也做了些更新,不过这个是个非官方的库。

项目结构:

图片描述

简单使用

他的用法,大家也很熟悉,可以参考文档以及example,下面仅展示了example里核心部分

从中能看出他似乎是和webpack-dev-middleware配套使用的,具体是不是这样子? 以后有空再探究下webpack-dev-middleware喽,在此也暂时不用太关心

server.js

    var http = require('http');
    var express = require('express');
    var app = express();
    
    app.use(require('morgan')('short'));
    
    (function() {
      // Step 1: Create & configure a webpack compiler
      var webpack = require('webpack');
      var webpackConfig = require(process.env.WEBPACK_CONFIG ? process.env.WEBPACK_CONFIG : './webpack.config');
      var compiler = webpack(webpackConfig);
    
      // Step 2: Attach the dev middleware to the compiler & the server
      app.use(require("webpack-dev-middleware")(compiler, {
    noInfo: true, 
    publicPath: webpackConfig.output.publicPath
      }));
    
      // Step 3: Attach the hot middleware to the compiler & the server
      app.use(require("webpack-hot-middleware")(compiler, {
    log: console.log,
    path: '/__webpack_hmr',
    heartbeat: 10 * 1000
      }));
    })();
    
    // Do anything you like with the rest of your express application.
    app.get("/", function(req, res) {
      res.sendFile(__dirname + '/index.html');
    });
    
    if (require.main === module) {
      var server = http.createServer(app);
      server.listen(process.env.PORT || 1616, '127.0.0.1', function() {
    console.log("Listening on %j", server.address());
      });
    }

webpack.config.js

    entry: {
      index: [
        'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000',
        './src/index.js'
      ]
    }
    plugins: [
      new webpack.HotModuleReplacementPlugin()
    ]
    ...

src/index.js

    ...
    
    var timeElem = document.getElementById('timeElement');
    var timer = setInterval(updateClock, 1000);
    function updateClock() {
      timeElem.innerHTML = (new Date()).toString();
    }
    
    // ...
    
    if (module.hot) {
      // 模块自己就接收更新
      module.hot.accept();
      // dispose方法用来定义一个一次性的函数,这个函数会在当前模块被更新之前调用
      module.hot.dispose(function() {
        clearInterval(timer);
      });
    }
source code分析
  • middleware.js

webpackHotMiddleware函数

function webpackHotMiddleware(compiler, opts) {
  opts = opts || {};
  opts.log = typeof opts.log == 'undefined' ? console.log.bind(console) : opts.log;
  opts.path = opts.path || '/__webpack_hmr';
  opts.heartbeat = opts.heartbeat || 10 * 1000;
  
  var eventStream = createEventStream(opts.heartbeat);
  var latestStats = null;

  compiler.plugin("compile", function() {
    latestStats = null;
    if (opts.log) opts.log("webpack building...");
    eventStream.publish({action: "building"});
  });
  compiler.plugin("done", function(statsResult) {
    // Keep hold of latest stats so they can be propagated to new clients
    latestStats = statsResult;
    publishStats("built", latestStats, eventStream, opts.log);
  });
  var middleware = function(req, res, next) {
    if (!pathMatch(req.url, opts.path)) return 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);
    }
  };
  middleware.publish = eventStream.publish;
  return middleware;
}

这里主要使用了sse(server send event),具体协议的内容及其用法,可以文末给出的资料 1) - 4),也不算什么新东西,不过感觉还不错,可以理解为基于http协议的服务器"推送",比websocket要简便一些

稍微强调的一下的是,服务端可以发送个id字段(似乎必须作为首字段),这样连接断开时浏览器3s后会自动重连,其中服务端可以通过发送retry字段来控制这个时间,这样重连时客户端请求时会带上一个Last-Event-ID的字段,然后服务端就能知道啦(不过也看到有人说可以new EventSource("srouce?eventId=12345"),我试好像不行啊,这个我就母鸡啦)
如果你不自动想重连,那么客户端eventsource.close(),比如这里就是这样

这里就是webpack的plugin的简单写法, compile和done钩子,正常webpack一下plugin是不会运行的,要调用其run或watch方法,webpack-dev-middleware好像调用了watch方法,所以配合使用就没问题,难道这就解释上面配合使用的疑问?

这里webpack的compile的回调,为啥只在rebuild的时候触发哩?难道又被webpack-dev-middleware吸收伤害了...?

createEventStream内部函数

    function createEventStream(heartbeat) {
      var clientId = 0;
      var clients = {};
      function everyClient(fn) {
        Object.keys(clients).forEach(function(id) {
          fn(clients[id]);
        });
      }
      setInterval(function heartbeatTick() {
        everyClient(function(client) {
          client.write("data: \uD83D\uDC93\n\n");
        });
      }, heartbeat).unref();
      return {
        handler: function(req, res) {
          req.socket.setKeepAlive(true);
          res.writeHead(200, {
            'Access-Control-Allow-Origin': '*',
            'Content-Type': 'text/event-stream;charset=utf-8',
            'Cache-Control': 'no-cache, no-transform',
            'Connection': 'keep-alive'
          });
          res.write('\n');
          var id = clientId++;
          clients[id] = res;
          req.on("close", function(){
            delete clients[id];
          });
        },
        publish: function(payload) {
          everyClient(function(client) {
            client.write("data: " + JSON.stringify(payload) + "\n\n");
          });
        }
      };
    }

setInterval的unref可以看资料5),我想说,我用你这模块,肯定要createServer,我肯定有event loop啊,不明白为啥还调用unref()方法

req.socket.setKeepAlive(true)可以看资料6),虽然我也没太懂,而且我看注释掉这行,好像运行也没问题啊,难道是我人品好,2333

这里呢,就是每10秒向客户端发送心跳的unicode码,chrome控制台Network里的__webpack_hmr,可以看到

图片描述

extractBundles内部函数

function extractBundles(stats) {
  // Stats has modules, single bundle
  if (stats.modules) return [stats];

  // Stats has children, multiple bundles
  if (stats.children && stats.children.length) return stats.children;

  // Not sure, assume single
  return [stats];
}

将webpack的bundle,统一成数组形式

buildModuleMap内部函数

function buildModuleMap(modules) {
  var map = {};
  modules.forEach(function(module) {
    map[module.id] = module.name;
  });
  return map;
}

转成key为module.id,value为module.name的map

publishStats内部函数

function publishStats(action, statsResult, eventStream, log) {
  // For multi-compiler, stats will be an object with a 'children' array of stats
  var bundles = extractBundles(statsResult.toJson({ errorDetails: false }));
  bundles.forEach(function(stats) {
    if (log) {
      log("webpack built " + (stats.name ? stats.name + " " : "") +
        stats.hash + " in " + stats.time + "ms");
    }
    eventStream.publish({
      name: stats.name,
      action: action,
      time: stats.time,
      hash: stats.hash,
      warnings: stats.warnings || [],
      errors: stats.errors || [],
      modules: buildModuleMap(stats.modules)
    });
  });
}

这个函数就是打印下built的信息,并调用eventStream.publish

pathMatch助手函数

    function pathMatch(url, path) {
      if (url == path) return true;
      var q = url.indexOf('?');
      if (q == -1) return false;
      return url.substring(0, q) == path;
    }

为 /__webpack_hmr 或 /__webpack_hmr?xyz=123 均返回true

  • process-update.js

这块主要是调用webpack内部hot的一些api,如module.hot.status, module.hot.check, module.hot...

作者基本也是参考webpack的hot目录下一些js文件写法以及HotModuleReplacement.runtime.js

由于是初探嘛,偷偷懒,有空补全下吧,请不要丢?

  • client.js

client.js是与你的entry开发时打包到一起的一个文件,当然它还引入了client-overlay.js就是用来展示build错误时的样式

__resourceQuery是webpack的一个变量,这里其值为?path=/__webpack_hmr&timeout=20000

    // 选项,参数
    var options = {
      path: "/__webpack_hmr",
      timeout: 20 * 1000,
      overlay: true,
      reload: false,
      log: true,
      warn: true
    };
    
    if (__resourceQuery) {
      var querystring = require('querystring');
      var overrides = querystring.parse(__resourceQuery.slice(1));
      if (overrides.path) options.path = overrides.path;
      if (overrides.timeout) options.timeout = overrides.timeout;
      if (overrides.overlay) options.overlay = overrides.overlay !== 'false';
      if (overrides.reload) options.reload = overrides.reload !== 'false';
      if (overrides.noInfo && overrides.noInfo !== 'false') {
        options.log = false;
      }
      if (overrides.quiet && overrides.quiet !== 'false') {
        options.log = false;
        options.warn = false;
      }
      if (overrides.dynamicPublicPath) {
        options.path = __webpack_public_path__ + options.path;
      }
    }
    
    // 主要部分
    if (typeof window === 'undefined') {
      // do nothing
    } else if (typeof window.EventSource === 'undefined') {
      console.warn(
        "webpack-hot-middleware's client requires EventSource to work. " +
        "You should include a polyfill if you want to support this browser: " +
        "https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events#Tools"
      );
    } else {
      connect(window.EventSource);
    }
    
    function connect(EventSource) {
      var source = new EventSource(options.path);
      var lastActivity = new Date();
      
      source.onopen = handleOnline;
      source.onmessage = handleMessage;
      source.onerror = handleDisconnect;
    
      var timer = setInterval(function() {
        if ((new Date() - lastActivity) > options.timeout) {
          handleDisconnect();
        }
      }, options.timeout / 2);
    
      function handleOnline() {
        if (options.log) console.log("[HMR] connected");
        lastActivity = new Date();
      }
    
      function handleMessage(event) {
        lastActivity = new Date();
        if (event.data == "\uD83D\uDC93") {
          return;
        }
        try {
          processMessage(JSON.parse(event.data));
        } catch (ex) {
          if (options.warn) {
            console.warn("Invalid HMR message: " + event.data + "\n" + ex);
          }
        }
      }
      
      function handleDisconnect() {
        clearInterval(timer);
        source.close();
        setTimeout(function() { connect(EventSource); }, options.timeout);
      }
    }
    
    // 导出一些方法
    if (module) {
      module.exports = {
        subscribeAll: function subscribeAll(handler) {
          subscribeAllHandler = handler;
        },
        subscribe: function subscribe(handler) {
          customHandler = handler;
        },
        useCustomOverlay: function useCustomOverlay(customOverlay) {
          if (reporter) reporter.useCustomOverlay(customOverlay);
        }
      };
    }

这里,每10s钟检查当前时间和上次活跃(onopen, on message)的时间的间隔是否超过20s,超过20s则认为失去连接,则调用handleDisconnect

eventsource主要监听3个方法:
onopen,记录下当前时间
onmessage,记录下当前时间,发现心跳就直接返回,否则尝试processMessage(JSON.parse(event.data))
onerror,调用handleDisconnect,停止定时器,eventsource.close,手动20s后重连

module.exports的方法,主要给自定义用的

其中useCustomeOverlay,就是自定义报错的那层dom层

createReporter函数

    var reporter;
    // the reporter needs to be a singleton on the page
    // in case the client is being used by mutliple bundles
    // we only want to report once.
    // all the errors will go to all clients
    var singletonKey = '__webpack_hot_middleware_reporter__';
    if (typeof window !== 'undefined' && !window[singletonKey]) {
      reporter = window[singletonKey] = createReporter();
    }
    
    function createReporter() {
      var strip = require('strip-ansi');
    
      var overlay;
      if (typeof document !== 'undefined' && options.overlay) {
        overlay = require('./client-overlay');
      }
    
      return {
        problems: function(type, obj) {
          if (options.warn) {
            console.warn("[HMR] bundle has " + type + ":");
            obj[type].forEach(function(msg) {
              console.warn("[HMR] " + strip(msg));
            });
          }
          if (overlay && type !== 'warnings') overlay.showProblems(type, obj[type]);
        },
        success: function() {
          if (overlay) overlay.clear();
        },
        useCustomOverlay: function(customOverlay) {
          overlay = customOverlay;
        }
      };
    }

createReport就是有stats有warning或error的时候,让overlay显示出来

如果build succes那么在有overlay的情况下,将其clear掉

如下图,故意在src/index.js弄个语法错误,让其编译不通过

图片描述

processMessage函数

    var processUpdate = require('./process-update');
    
    var customHandler;
    var subscribeAllHandler;
    function processMessage(obj) {
      switch(obj.action) {
        case "building":
          if (options.log) console.log("[HMR] bundle rebuilding");
          break;
        case "built":
          if (options.log) {
            console.log(
              "[HMR] bundle " + (obj.name ? obj.name + " " : "") +
              "rebuilt in " + obj.time + "ms"
            );
          }
          // fall through
        case "sync":
          if (obj.errors.length > 0) {
            if (reporter) reporter.problems('errors', obj);
          } else {
            if (reporter) {
              if (obj.warnings.length > 0) reporter.problems('warnings', obj);
              reporter.success();
            }
            processUpdate(obj.hash, obj.modules, options);
          }
          break;
        default:
          if (customHandler) {
            customHandler(obj);
          }
      }
    
      if (subscribeAllHandler) {
        subscribeAllHandler(obj);
      }
    }

参数obj其实就是后端传过来的data,JSON.parse里一下

action分为"building", built", "sync",均为middleware.js服务端传过来的

至于其他,应该是用户自定义处理的

资料:

1) http://cjihrig.com/blog/the-s...

2) https://www.html5rocks.com/en...

3) http://cjihrig.com/blog/serve...

4) http://www.howopensource.com/...

5) https://cnodejs.org/topic/570...

6) http://tldp.org/HOWTO/TCP-Kee...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值