一直感受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/...