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