webpack-hot-middleware初探

一直感受hot module replacement的特性挺神奇,因此這裏初步探究下webpack-hot-middleware,這個模塊javascript

首先地址,https://github.com/glenjamin/...,當前版本爲2.13.2,爲了配合webpack2吧,確定也作了些更新,不過這個是個非官方的庫。html

項目結構:

圖片描述

簡單使用

他的用法,你們也很熟悉,能夠參考文檔以及example,下面僅展現了example裏核心部分html5

從中能看出他彷佛是和webpack-dev-middleware配套使用的,具體是否是這樣子? 之後有空再探究下webpack-dev-middleware嘍,在此也暫時不用太關心java

server.jsnode

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.jswebpack

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

src/index.jsgit

...
    
    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.jsgithub

webpackHotMiddleware函數web

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要簡便一些chrome

稍微強調的一下的是,服務端能夠發送個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...

相關文章
相關標籤/搜索