手动搭建webpack-dev-server
- Published on
- 发布于·预估阅读15分钟
- 浏览器是怎么知道webpack重新编译了代码?
- 浏览器又是怎样拿到webpack编译的代码?
- 浏览器拿到webpack编译的代码之后又是怎么执行的及触发依赖回调?
- webpack-dev-middleware扮演什么角色?
- webpack-hot-middleware又是扮演什么角色?
- webpack.HotModuleReplacementPlugin插件的作用又是什么?
- webpack-dev-server与webpack-dev-middleware、webpack-hot-middleware有什么异同?
- webpack-dev-middleware源码分析,只保留关键代码
- webpack-hot-middleware源码分析,只留关键代码
- webpakc-dev-server源码,只保留关键代码
- webpakc-dev-server的例子
- webpack-dev-middleware + webpack-hot-middleware的例子
- 总结
- Authors
- Name
- willson-wang
Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running, without a full reload. This can significantly speed up development in a few ways:
Retain application state which is lost during a full reload. Save valuable development time by only updating what's changed. Instantly update the browser when modifications are made to CSS/JS in the source code, which is almost comparable to changing styles directly in the browser's dev tools.
浏览器是怎么知道webpack重新编译了代码?
因为webpack-hot-middleware通过sse将重新编译产生的hash值及变化的模块信息推送到了客户端
浏览器又是怎样拿到webpack编译的代码?
webpack-hot-middleware提供了sse的客户端、当接收到服务端发送来的消息时,会通过process-update内调用module.hot.check方法及module.hot.apply方法促使客户端下载更新的模块及hot-update.json
浏览器拿到webpack编译的代码之后又是怎么执行的及触发依赖回调?
webpack.HotModuleReplacementPlugin提供了一系列check、update、apply方法,帮助我们去执行代码及触发回调
webpack-dev-middleware扮演什么角色?
通过源码分析,我们可以知道webpack-dev-middleware帮我们做了如下事情
注册了done、run、invalid、watchRun几个钩子,在钩子内做一些打印or校验的功能
手动执行了webpack.watch帮助我们进行监控webpack重新编译
使用memory-fs读写文件
返回一个middleware方法,该方法包含close、invalid等关闭与校验的方法
总结起来就是帮助我们监控了文件变化
webpack-hot-middleware又是扮演什么角色?
使用Server-Sent Events(以下简称 SSE)技术与客户端推送消息
客户端的逻辑放在clinet.js内
服务端的逻辑放在middleware.js内
把更改的模块、hash值传递到客户端
通过process-update内调用module.hot.check方法及module.hot.apply方法促使客户端下载更新的模块及hot-update.json
总结起来就是把webpack变化了的模块及重新包生产的hash值传到客户端(也就是浏览器端) ,最终替换模块及执行回调
webpack.HotModuleReplacementPlugin插件的作用又是什么?
提供module.hot属性,包含一系列下载模块,更新模块,执行回调的方法
webpack-dev-server与webpack-dev-middleware、webpack-hot-middleware有什么异同?
webpack-dev-server是一个完整的即提供了node服务又提供了热更新功能包
webpack-dev-middleware主要提供了监控webpack编译的功能及使用memory-fs读写文件
webpack-hot-middleware主要提供Server-Sent Events(以下简称 SSE)技术用于热更新
webpack-dev-middleware源码分析,只保留关键代码
index.js
module.exports = function wdm(compiler, opts) {
// 初始化监听一些钩子
const context = createContext(compiler, options);
// start watching
context.watching = compiler.watch(options.watchOptions, (err) => {
if (err) {
context.log.error(err.stack || err);
if (err.details) {
context.log.error(err.details);
}
}
});
// 是否通过磁盘读写
if (options.writeToDisk) {
toDisk(context);
}
// 设置读写文件通过memory-fs
setFs(context, compiler);
const temp = middleware(context)
// 返回一个middleware函数,包含close、context等静态属性与方法
const tempObj = Object.assign(temp, {
close(callback) {
// eslint-disable-next-line no-param-reassign
callback = callback || noop;
if (context.watching) {
context.watching.close(callback);
} else {
callback();
}
},
context,
fileSystem: context.fs,
});
return tempObj
};
content.js
module.exports = function ctx(compiler, options) {
const context = {
state: false,
webpackStats: null,
callbacks: [],
options,
compiler,
watching: null,
forceRebuild: false,
};
context.rebuild = rebuild;
// 注册插件的钩子函数,主要作用就是打印消息
context.compiler.hooks.invalid.tap('WebpackDevMiddleware', invalid);
context.compiler.hooks.run.tap('WebpackDevMiddleware', invalid);
context.compiler.hooks.done.tap('WebpackDevMiddleware', done);
context.compiler.hooks.watchRun.tap(
'WebpackDevMiddleware',
(comp, callback) => {
invalid(callback);
}
);
return context;
};
webpack-hot-middleware源码分析,只留关键代码
middleware.js 提供sse的服务端代码
function webpackHotMiddleware(compiler, opts) {
// 通过createEventStream方法创建eventStream对象,该对象上包含publish向客户端发布消息的方法
var eventStream = createEventStream(opts.heartbeat);
// 插件注册钩子,兼容webpack的写法
if (compiler.hooks) {
compiler.hooks.invalid.tap('webpack-hot-middleware', onInvalid);
compiler.hooks.done.tap('webpack-hot-middleware', onDone);
} else {
compiler.plugin('invalid', onInvalid);
compiler.plugin('done', onDone);
}
function onInvalid() {
// 发送 action: 'building'
eventStream.publish({ action: 'building' });
}
function onDone(statsResult) {
// 还是调用eventStream.publish向客户端发送消息
publishStats('built', latestStats, eventStream, opts.log);
}
var middleware = function(req, res, next) {
if (closed) return 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 = function(payload) {
if (closed) return;
eventStream.publish(payload);
};
middleware.close = function() {
if (closed) return;
// Can't remove compiler plugins, so we just set a flag and noop if closed
// https://github.com/webpack/tapable/issues/32#issuecomment-350644466
closed = true;
eventStream.close();
eventStream = null;
};
return middleware;
}
function createEventStream(heartbeat) {
var clientId = 0;
var clients = {};
// 提取公共方法
function everyClient(fn) {
Object.keys(clients).forEach(function(id) {
fn(clients[id]);
});
}
// 心跳检测判断sse是否正常连接
var interval = setInterval(function heartbeatTick() {
everyClient(function(client) {
client.write('data: \uD83D\uDC93\n\n');
});
}, heartbeat).unref();
return {
close: function() {
// 关闭服务端sse连接
clearInterval(interval);
everyClient(function(client) {
if (!client.finished) client.end();
});
clients = {};
},
handler: function(req, res) {
// sse必要的header设置
var headers = {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/event-stream;charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
// While behind nginx, event stream should not be buffered:
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
'X-Accel-Buffering': 'no',
};
var isHttp1 = !(parseInt(req.httpVersion) >= 2);
if (isHttp1) {
req.socket.setKeepAlive(true);
Object.assign(headers, {
Connection: 'keep-alive',
});
}
res.writeHead(200, headers);
res.write('\n');
var id = clientId++;
// 保存当前连接的response
clients[id] = res;
req.on('close', function() {
if (!res.finished) res.end();
delete clients[id];
});
},
publish: function(payload) {
everyClient(function(client) {
// 通过当前保存的response向客户端发送消息,这里是hash值、修改的模块名等
client.write('data: ' + JSON.stringify(payload) + '\n\n');
});
},
};
}
function publishStats(action, statsResult, eventStream, log) {
var bundles = extractBundles(stats);
bundles.forEach(function(stats) {
var name = stats.name || '';
eventStream.publish({
name: name,
action: action,
time: stats.time,
hash: stats.hash,
warnings: stats.warnings || [],
errors: stats.errors || [],
modules: buildModuleMap(stats.modules),
});
});
}
client.js 提供客户端发起sse的文件
// 判断EventSource是否存在,EventSource对象是发起sse的对象
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 {
if (options.autoConnect) {
connect();
}
}
function EventSourceWrapper() {
var source;
var lastActivity = new Date();
var listeners = [];
// 初始化发起一个sse连接
init();
// 心跳检测,如果连接时间超过超时时间则断开重新连接
var timer = setInterval(function() {
if (new Date() - lastActivity > options.timeout) {
handleDisconnect();
}
}, options.timeout / 2);
// 初始化EventSource对象,并监听开启、错误、收到消息事件
function init() {
source = new window.EventSource(options.path);
source.onopen = handleOnline;
source.onerror = handleDisconnect;
source.onmessage = handleMessage;
}
function handleOnline() {
if (options.log) console.log('[HMR] connected');
lastActivity = new Date();
}
function handleMessage(event) {
lastActivity = new Date();
for (var i = 0; i < listeners.length; i++) {
listeners[i](event);
}
}
function handleDisconnect() {
clearInterval(timer);
source.close();
setTimeout(init, options.timeout);
}
// 添加一个处理接收消息时的回调
return {
addMessageListener: function(fn) {
listeners.push(fn);
},
};
}
function getEventSourceWrapper() {
if (!window.__whmEventSourceWrapper) {
window.__whmEventSourceWrapper = {};
}
if (!window.__whmEventSourceWrapper[options.path]) {
// cache the wrapper for other entries loaded on
// the same page with the same options.path
window.__whmEventSourceWrapper[options.path] = EventSourceWrapper();
}
return window.__whmEventSourceWrapper[options.path];
}
function connect() {
// 发起一个连接,并通过addMessageListener方法添加处理message的回调
getEventSourceWrapper().addMessageListener(handleMessage);
function handleMessage(event) {
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);
}
}
}
}
// 引入跟webpack交互的模块
var processUpdate = require('./process-update');
var customHandler;
var subscribeAllHandler;
function processMessage(obj) {
switch (obj.action) {
case 'building':
if (options.log) {
console.log(
'[HMR] bundle ' +
(obj.name ? "'" + obj.name + "' " : '') +
'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.name && options.name && obj.name !== options.name) {
return;
}
var applyUpdate = true;
if (applyUpdate) {
// 与webpack交互的方法
processUpdate(obj.hash, obj.modules, options);
}
break;
default:
if (customHandler) {
customHandler(obj);
}
}
if (subscribeAllHandler) {
subscribeAllHandler(obj);
}
}
process-update.js
var lastHash;
var failureStatuses = { abort: 1, fail: 1 };
// 支持module.hot.apply传入的参数
var applyOptions = {
ignoreUnaccepted: true,
ignoreDeclined: true,
ignoreErrored: true,
onUnaccepted: function(data) {
console.warn(
'Ignored an update to unaccepted module ' + data.chain.join(' -> ')
);
},
onDeclined: function(data) {
console.warn(
'Ignored an update to declined module ' + data.chain.join(' -> ')
);
},
onErrored: function(data) {
console.error(data.error);
console.warn(
'Ignored an error while updating module ' +
data.moduleId +
' (' +
data.type +
')'
);
},
};
function upToDate(hash) {
if (hash) lastHash = hash;
return lastHash == __webpack_hash__;
}
module.exports = function(hash, moduleMap, options) {
var reload = options.reload;
// 是否需要触发热更新
if (!upToDate(hash) && module.hot.status() == 'idle') {
if (options.log) console.log('[HMR] Checking for updates on the server...');
check();
}
function check() {
// 传入需要更改的模块updatedModules
var cb = function(err, updatedModules) {
if (err) return handleError(err);
// 如果没有即我们没有改动文件,直接再一次保存
if (!updatedModules) {
if (options.warn) {
console.warn('[HMR] Cannot find update (Full reload needed)');
console.warn('[HMR] (Probably because of restarting the server)');
}
performReload();
return null;
}
var applyCallback = function(applyErr, renewedModules) {
if (applyErr) return handleError(applyErr);
if (!upToDate()) check();
logUpdates(updatedModules, renewedModules);
};
// 调用module.hot.apply方法,执行变更的模块代码及accept注册的回调函数
var applyResult = module.hot.apply(applyOptions, applyCallback);
// webpack 2 promise
if (applyResult && applyResult.then) {
// HotModuleReplacement.runtime.js refers to the result as `outdatedModules`
applyResult.then(function(outdatedModules) {
applyCallback(null, outdatedModules);
});
applyResult.catch(applyCallback);
}
};
// 调用module.hot.check检测需要更改的模块,并下载变更了的模块与当前的hash值hash.hot-update.json、hash.hot-update.js,module.hot是HotModuleReplacementPlugin插件提供的属性及方法
var result = module.hot.check(false, cb);
// webpack 2 promise
if (result && result.then) {
result.then(function(updatedModules) {
cb(null, updatedModules);
});
result.catch(cb);
}
}
// 浏览器端打印热更新相关的消息
function logUpdates(updatedModules, renewedModules) {
var unacceptedModules = updatedModules.filter(function(moduleId) {
return renewedModules && renewedModules.indexOf(moduleId) < 0;
});
if (unacceptedModules.length > 0) {
if (options.warn) {
unacceptedModules.forEach(function(moduleId) {
console.warn('[HMR] - ' + (moduleMap[moduleId] || moduleId));
});
}
performReload();
return;
}
if (options.log) {
if (!renewedModules || renewedModules.length === 0) {
console.log('[HMR] Nothing hot updated.');
} else {
console.log('[HMR] Updated modules:');
renewedModules.forEach(function(moduleId) {
console.log('[HMR] - ' + (moduleMap[moduleId] || moduleId));
});
}
if (upToDate()) {
console.log('[HMR] App is up to date.');
}
}
}
function handleError(err) {
if (module.hot.status() in failureStatuses) {
if (options.warn) {
console.warn('[HMR] Cannot check for update (Full reload needed)');
console.warn('[HMR] ' + (err.stack || err.message));
}
performReload();
return;
}
if (options.warn) {
console.warn('[HMR] Update check failed: ' + (err.stack || err.message));
}
}
// 是否需要重新刷新整个页面
function performReload() {
if (reload) {
if (options.warn) console.warn('[HMR] Reloading page');
window.location.reload();
}
}
};
webpakc-dev-server源码,只保留关键代码
class Server {
// 更改webpack配置,如添加websoket的客户端代码、webpack热更新的代码等
updateCompiler(this.compiler, this.options);
// new express
this.setupApp();
// 调用webpack-dev-middleware,监听webpack编译文件
this.setupDevMiddleware();
// 创建服务
this.createServer();
setupApp() {
this.app = new express();
}
setupDevMiddleware() {
// middleware for serving webpack bundle
this.middleware = webpackDevMiddleware(
this.compiler,
Object.assign({}, this.options, { logLevel: this.log.options.level })
);
}
createServer() {
this.listeningApp = http.createServer(this.app);
}
// 启动服务并监听对应的端口
listen(port, hostname, fn) {
return this.listeningApp.listen(port, hostname, (err) => {
// 创建socket服务、具体的socket通信就不展开了
this.createSocketServer();
});
}
}
addEntries.js
function addEntries(config, options, server) {
if (options.inline !== false) {
const app = server || {
address() {
return { port: options.port };
},
};
// 拼接socket的入口文件路径,实现与服务端的socket通信
const clientEntry = `${require.resolve(
'../../client/'
)}?${domain}${sockHost}${sockPath}${sockPort}`;
// 获取webpack处理热更新文件的代码
if (options.hotOnly) {
hotEntry = require.resolve('webpack/hot/only-dev-server');
} else if (options.hot) {
hotEntry = require.resolve('webpack/hot/dev-server');
}
// eslint-disable-next-line no-shadow
[].concat(config).forEach((config) => {
// 将clientEntry添加到entry入口
const additionalEntries = checkInject(
options.injectClient,
config,
webTarget
)
? [clientEntry]
: [];
// 将hotEntry添加到entry入口
if (hotEntry && checkInject(options.injectHot, config, true)) {
additionalEntries.push(hotEntry);
}
// 将additionalEntries添加实际的webpack entry
config.entry = prependEntry(config.entry || './src', additionalEntries);
if (options.hot || options.hotOnly) {
config.plugins = config.plugins || [];
// 如果用户没有手动使用HotModuleReplacementPlugin插件,则自动注入
if (
!config.plugins.find(
(plugin) => plugin.constructor.name === 'HotModuleReplacementPlugin'
)
) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
}
});
}
}
./bin/webpack-dev-server
function startDevServer(config, options) {
let compiler;
compiler = webpack(config);
// 创建一个Server实例
server = new Server(compiler, options, log);
serverData.server = server;
// 调用server的listen方法,启动app并监听对应的端口
server.listen(options.port, options.host, (err) => {
if (err) {
throw err;
}
});
}
webpakc-dev-server的例子
只需要将hot || hotOnly属性设置为true即可,其它任何东西都不需要设置
webpack.config.js
module.exports = {
mode: 'development',
entry: [
'./src/app.js'
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: './index.html'
})
],
devServer: {
contentBase: './dist',
hot: true
}
}
webpack-dev-middleware + webpack-hot-middleware的例子
webpack.config.js
module.exports = {
mode: 'development',
entry: [
'webpack-hot-middleware/client',
'./src/app.js'
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
inject: true,
template: './index.html'
})
]
}
server.js
const middleware = require('webpack-dev-middleware')
const hotMiddleware = require('webpack-hot-middleware')
const express = require('express')
const compiler = webpack(config)
const app = express()
app.use(middleware(compiler, {
publicPath: config.output.publicPath,
stats: {
modules: false,
children: false,
chunks: true,
chunkModules: false,
colors: true
}
}))
app.use(hotMiddleware(compiler))
app.listen(3001, function (err) {
console.log('listen 3001')
})
总结
不论是webpack-dev-server还是webpack-dev-middleware + webpack-hot-middleware的组合,都是利用webpack.wacth来编译文件及检测webpack文件变化,然后给浏览器注入webpack热更新的js代码,以及与服务端通信的客户端代码来最终实现代码的热更新,形式在变,但是思路不变
具体例子可参考webpack-tiny-server
参考链接:
https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html https://github.com/webpack/webpack/blob/v4.41.4/hot/only-dev-server.js