一直感受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.js
node
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
webpack
entry: { index: [ 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000', './src/index.js' ] } plugins: [ new webpack.HotModuleReplacementPlugin() ] ...
src/index.js
git
... 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); }); }
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/...