资讯专栏INFORMATION COLUMN

webpack-hot-middleware初探

lijy91 / 901人阅读

摘要:一直感觉的特性挺神奇,所以这里初步探究下,这个模块首先地址,,当前版本为,为了配合吧,肯定也做了些更新,不过这个是个非官方的库。

一直感觉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: uD83DuDC93

");
        });
      }, 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("
");
          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) + "

");
          });
        }
      };
    }

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 == "uD83DuDC93") {
          return;
        }
        try {
          processMessage(JSON.parse(event.data));
        } catch (ex) {
          if (options.warn) {
            console.warn("Invalid HMR message: " + event.data + "
" + 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...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86630.html

相关文章

  • Wepback + koa2 配置开发环境

    摘要:把处理后的配置文件传递给服务器,不过我们在使用它之前,需要把它改造成中间件。因为通过生成的模块是写入到内存中的,所以我们需要修改开发环境中的配置项修改此配置项安装封装成中间件。 前言 webpack提供了webpack-dev-server模块来启动一个简单的web服务器,为了更大的自由度我们可以自己配置一个服务器,下面介绍如何用koa2来实现。 wepack-dev-middlewa...

    waterc 评论0 收藏0
  • Express结合Webpack的全栈自动刷新

    摘要:如果修改的是里的文件,保存后,服务器将自动重启,浏览器会在服务器重启完毕后自动刷新。从开始首先,已经想到了开发流程中的自动刷新,这就是。 在以前的一篇文章BrowserSync,迅捷从免F5开始中,我介绍了BrowserSync这样一个出色的开发工具。通过BrowserSync我感受到了这样一个理念:如果在一次ctrl + s保存后可以自动刷新,然后立即看到新的页面效果,那会是很棒的开...

    Simon 评论0 收藏0
  • 前端临床手札——webpack构建逐步解构(上)

    摘要:前言由于博主最近又闲下来了,之前觉得的官方文档比较难啃一直放到现在。文章会逐步分析每个处理的用意当然是博主自己的理解,不足之处欢迎指出沟通交流。后续将会补上构建生产的配置分析,案例参考。前端临床手札构建逐步解构下 前言 由于博主最近又闲下来了,之前觉得webpack的官方文档比较难啃一直放到现在。细心阅读多个webpack配置案例后觉得还是得自己写个手脚架,当然这个案例是基于vue的,...

    lowett 评论0 收藏0
  • 手把手深入理解 webpack dev middleware 原理與相關 plugins

    摘要:的架構設計促使第三方開發者讓核心發揮出無限的潛力。當然建置比起開發是較進階的議題,因為我們必須要理解內部的一些事件。這個編譯結果包含的訊息包含模組的狀態,編譯後的資源檔,發生異動的檔案,被觀察的相依套件等。 本文將對 webpack 周邊的 middleware 與 plugin 套件等作些介紹,若您對於 webpack 還不了解可以參考這篇彙整的翻譯。 webpack dev ser...

    gitmilk 评论0 收藏0
  • react+webpack+redux 基础配置

    摘要:此处用到跑服务器命令行输入即可,会忽略的改变,其余时候都会自动重启服务器不用的话,就用启动服务器在此处是用的做后台,并且配置了的信息,不然,在跑服务器前,要先输入命令来生成文件。并且也用到了热加载,在代码改变后,立马更新页面 package.json: 此处用到nodemon跑服务器:命令行输入:npm run serve即可,会忽略components的改变,其余时候都会自动重启服务...

    lijy91 评论0 收藏0

发表评论

0条评论

lijy91

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<