上一篇中介绍了webpack-dev-server属性配置
这一篇就简单的梳理下webpack-dev-server内部实现。
由于涉及到源码解析,所以会涉及到一些比较难啃的知识,我会尽量进行简单化描述。
但如果还是具有具有难度 或 对 webpack-dev-server内部实现不感兴趣的朋友,也可以完全跳过此篇。
调试webpack-dev-server
配置调试方式
日常开发开发中,如果对代码逻辑不熟悉,最简单的方法就是调试,一步步观察流程。
学习webpack-dev-server源码,最简单的方法也就是就行调试,不过调试webpack-dev-server源码毕竟不像调试本身项目代码那样简单,必定需要做一些配置,
下面先简单介绍两种源码webpack-dev-server方式
浏览器调试
现在都知道webpack是执行于Node.js环境下,所以调试webpack也就是调试Node.js程序。
伟大的Chrome浏览器就给我们提供了调试Node.js程序的方案。
首先在package.json文件scripts属性中添加了一个debug
指令,使用这个命令启动调试Node.js程序
inspect-brk属性就是设置调试Node.js程序参数 :
5858代表启动Node.js程序的端口号
./node_modules/webpack-dev-server/bin/webpack-dev-server.js 文件代表调试的指定文件,
接下来就该设置浏览器
在Chrome浏览器地址栏输入chrome://inspect会进入一个设置页面
:whale2: 因为我使用的是新版Edge ,所以显示的为edge://inspect。
点击Open dedicated DevTools for Node 便可以进入调试面板
在调试面板中设置需要调试Node.js端口号
此时启动 yarn debug
便会被Chrome捕捉,就可以进行调试
IDE调试代码
常用IDE工具都可以调试Node.js程序。
可以使用IDE工具配置调试webpack-dev-server。
我使用的是WebStorm。可以做到零配置调试
执行点击Debug start
就可以执行当前命令的调试
至于VS Code,没用过,不太了解配置信息。
webpack-dev-server启动流程
下面简单介绍下webpack-dev-server启动流程。
在此只介绍下关键流程代码。具体每一步细节有兴趣的朋友可以自行调试查看
webpack-dev-server执行的第一个文件是 webpack库的 /bin/webpack.js 文件模块 ,也就是入口文件
原因上一篇已经介绍过:webpack-cli@4.0开始,webpack-dev-server执行命令改为了 webpack serve
。使用了webpack命令
在 webpack库的 /bin/webpack.js 文件模块 中调用了 webpack-cli库的 /bin/cli.js 文件模块 。
:whale2: 执行webpack-cli时检测webpack-cli是否安装就是在webpack库的 /bin/webpack.js 文件模块执行的,有兴趣的朋友可以自行查看。
之后在 webpack-cli的 /bin/cli.js 文件模块 中调用了 lib/bootstrap.js 启动文件模块
然后在 lib/bootstrap.js 启动文件模块中创建了 /lib/webpack-cli.js 模块实例 (WebpackCLI) 并调用 run()
之后WebpackCLI.run() 中根据命令调用对应@webpack-cli库中对应文件。
如webpack serve
,根据serve命令参数调用 @webpack-cli库 /serve/ 中模块 。
:whale2::whale2: @webpack-cli是webpack-cli@4.X版本依赖
在 @webpack-cli 中 /serve/index.js 文件模块中调用 /serve/startDevServer.js 文件模块
最后在 /serve/startDevServer.js 创建 webpack-dev-server 中 /lib/Server.js 模块实例对象。
并且调用 Server.listen() 监听启动服务器。
以上,就是webpack-dev-server服务器的启动流程。
webpack-dev-server服务器启动时跨了好几个库调用代码,
webpack ---> webpack-cli ---> @webpack-cli ---> webpack-dev-server。不过只要弄清楚了执行逻辑,理解起来并不会有多大困难
源码模块解析
express服务器
在上一篇说到过webpack-dev-server内部使用了express框架来作为服务器。
先来看一下express、
webpack-dev-server的服务器是 /lib/Server.js 文件模块。
关于express也就是在此模块中。
/lib/Server.js 模块构造函数中,初始化了许多东西,其中就有express
在 /lib/Server.js 构造函数中便初始化了express
可以看到在 Server.setupApp() 中构建了express服务器实例
然后在 Server.createServer() 中赋值给了 Server.listeningApp。
最后在 Server.listen() 进行了启动。
Server.listen() 就是在 @webpack-cli 中 /serve/startDevServer.js 文件模块调用的。
:whale2: Server.setupApp() 和 Server.createServer() 之间为 express 添加了配置的中间件。
监听代码文件更新
webpack-dev-server中,最主要的功能就是:监听代码变化触发重新编译、编译完成后通知浏览器更新数据。
其中监听代码文件变化触发重新编译是由webpack自身提供的一个功能。
webpack配置项中具有一个watch属性,此属性就代表是否要处于监听状态,监听代码文件变化
先来做一个简单的测试。
新建一个webpack.watch.js配置文件,在此配置文件设置watch属性
const { merge } = require('webpack-merge');
const common = require('./webpack.common');
// 使用node。js的导出,将配置进行导出
module.exports = merge([
common(true),
{
mode:'development',
// 监听文件变化
watch: true,
// 监听选项
watchOptions: {
// 忽略某些目录 可以为正则或者数组
ignored: /node_modules/,
// 启动轮询, 默认为false 设置true或毫秒数来启动轮询
poll: false
}
}
])
在package.json中添加watch指令,命令执行webapck
命令。
此时执行yarn watch
命令打包完成后也会一直处于wating状态,等待代码文件变化
更新代码,便会触发重新打包,
watch属性仅仅提供了watching状态等待代码文件变化,进行重新编译。
:whale2: watch属性只会监听已引用的代码模块文件,对于没有引用的代码模块文件是不会监听,有兴趣的朋友可以自行测试。
打包文件内存化
使用webpack-dev-server时,会发现并没有生成本地打包文件。
在上一篇中也介绍过webpack-dev-server默认将打包文件打在了内存流,提升访问速度
webpack-dev-server默认使用了webpack-dev-middleware中间件将打包文件内存化
webpack-dev-middleware中间件也是在 /lib/Server.js构造函数中添加的
webpack-dev-middleware的 /dist/index.js 模块中调用了 /dist/utils/setupOutputFileSystem.js 模块,此模块就是设置输出流函数
可以看到,默认情况下,webpack-dev-middleware使用了memfs作为输出流,memfs就是一个内存流库
:whale2:是一个Node.js 内存流的库,具体参考官方
:whale2:webpack-dev-middleware 项目主文件是 /dist/cjs.js ,在 /dist/cjs.js 调用了 /dist/index.js。
WebSocket
webpack-dev-server核心功能 编译完成后通知浏览器更新数据 就就是使用WebSocket完成的。
在服务器启动后,实例化WebSocket Server。浏览器访问时对浏览器(WebSocket Client)进行长链接。
当代码编译完成后,使用WebSocket Server向浏览器(WebSocket Client)推送消息
浏览器(WebSocket Client)接收到后完成对应操作。
WebSocket Server
Server 创建
WebSocket Server的实例化是在 Server.listen() 中进行的。
在成功启动服务器后,判断是否设置了 hot 或 liveReload 。如果设置就调用 Server.createSocketServer() 实例化 WebSocket Server
Server.createSocketServer() 执行时,首先会根据配置选择 WebSocket Server 类型。
然后实例化此WebSocket Server。
webpack-dev-server@4.X默认使用WebsocketServer.js类型,也就是ws
webpack-dev-server@3.X默认使用SockJSServer.js类型,也就是sockjs
:whale2::whale2:
目前在webpack-dev-server@3.11.2版本使用ws会有问题,而webpack-dev-server@4.0.0beta.0版本使用sockjs也有问题,
所以WebSocket Server直接使用默认即可
消息推送
在Server.js模块中具有一个sockWrite(),这个函数就是调用WebSocket Server来推送消息。
webpack-dev-server在三个时机中使用了WebSocket Server推送消息
客户端连接
在客户端连接(浏览器访问)
WebSocket Server会推送初始化信息。例如是否开启 热更新 、overlay配置信息、是否要输出打包编译进度、输入日志级别等。
代码编译完毕
在每次代码打包编译完毕后,WebSocket Server会将此次编译信息推送给客户端(浏览器)。
webpack内部暴露了 编译结束后(编译成功、编译失败)的一系列钩子事件。
webpack-dev-server就是使用了此钩子函数。
在Server.js构造函数中监听webpack钩子函数
编译完成后使用WebSocket Server推送消息
静态文件发生变化
webpack-dev-server提供了当静态文件变化时,使用WebSocket Server推送消息通知客户端(浏览器)自动刷新浏览器。
WebSocket Client
WebSocket client是WebSocket的客户端(浏览器)WebSocket client接收到消息时,会根据消息类型执行对应的操作,例如刷新浏览器、获取最新代码数据、输出日志等信息。
WebSocket client 代码存放在 webpack-dev-server 的 /client 目录下。WebSocket client 也是具有 SockJSClient 和 WebsocketClient 两种类型,与WebSocket Server对应
消息处理
WebSocket client 消息处理逻辑编写在 /client/default/index.js 模块文件中。
/client/default/index.js 模块文件中有一个 onSocketMessage 对象,这个对象就是消息处理对象。
将 onSocketMessage 对象复制于此。添加部分注释,以便理解, 具体就不再赘述。
其实代码还是很好理解的。
有兴趣的朋友可以看看源码。
var onSocketMessage = {
hot: function hot() {
// 如果推送hot,就代表使用HMR。在客户端连接后推送
options.hot = true;
log.info('Hot Module Replacement enabled.');
},
liveReload: function liveReload() {
// 如果推送liveReload,就代表使用liveReload。在客户端连接后推送
options.liveReload = true;
log.info('Live Reloading enabled.');
},
invalid: function invalid() {
// 监听webpack编译无效时推送。invalid与done一样,是webpack钩子函数
log.info('App updated. Recompiling...'); // fixes #1042. overlay doesn't clear if errors are fixed but warnings remain.
// 如果设置了错误页面,就将页面进行清除
if (options.useWarningOverlay || options.useErrorOverlay) {
overlay.clear();
}
sendMessage('Invalid');
},
hash: function hash(_hash) {
// 当前代码模块的hash值。
status.currentHash = _hash;
},
'still-ok': function stillOk() {
log.info('Nothing changed.');
if (options.useWarningOverlay || options.useErrorOverlay) {
overlay.clear();
}
sendMessage('StillOk');
},
logging: function logging(level) {
// 客户端显示日志级别,客户端连接后推送
// this is needed because the HMR logger operate separately from
// dev server logger
var hotCtx = require.context('webpack/hot', false, /^\.\/log$/);
if (hotCtx.keys().indexOf('./log') !== -1) {
hotCtx('./log').setLogLevel(level);
}
setLogLevel(level);
},
overlay: function overlay(value) {
// 设置编译错误时,是否显示错误页面。客户端连接后推送
if (typeof document !== 'undefined') {
if (typeof value === 'boolean') {
options.useWarningOverlay = false;
options.useErrorOverlay = value;
} else if (value) {
options.useWarningOverlay = value.warnings;
options.useErrorOverlay = value.errors;
}
}
},
progress: function progress(_progress) {
// 是否显示当前打包进度,客户端连接后推送
if (typeof document !== 'undefined') {
options.useProgress = _progress;
}
},
'progress-update': function progressUpdate(data) {
// 当前打包进度
if (options.useProgress) {
log.info("".concat(data.percent, "% - ").concat(data.msg, "."));
}
sendMessage('Progress', data);
},
ok: function ok() {
// 编译成功推送当前类型,
sendMessage('Ok');
// 如果设置了错误页面,就将页面进行清除
if (options.useWarningOverlay || options.useErrorOverlay) {
overlay.clear();
}
if (options.initial) {
return options.initial = false;
}
//使用此方法重新加载数据,此方法会处理HMR
reloadApp(options, status);
},
'content-changed': function contentChanged() {
// 静态文件改变时推送,重新刷新页面
log.info('Content base changed. Reloading...');
self.location.reload();
},
warnings: function warnings(_warnings) {
// 编译出现警告后推送
log.warn('Warnings while compiling.');
var strippedWarnings = _warnings.map(function (warning) {
return stripAnsi(warning);
});
sendMessage('Warnings', strippedWarnings);
for (var i = 0; i < strippedWarnings.length; i++) {
log.warn(strippedWarnings[i]);
}
// 如果显示错误页面设置警告提示,那么就显示
if (options.useWarningOverlay) {
overlay.showMessage(_warnings);
}
if (options.initial) {
return options.initial = false;
}
//使用此方法重新加载数据,此方法会处理HMR
reloadApp(options, status);
},
errors: function errors(_errors) {
// 编译错误后推送
log.error('Errors while compiling. Reload prevented.');
var strippedErrors = _errors.map(function (error) {
return stripAnsi(error);
});
sendMessage('Errors', strippedErrors);
for (var i = 0; i < strippedErrors.length; i++) {
log.error(strippedErrors[i]);
}
// 如果显示错误页面设置了错误提示,那么就显示
if (options.useErrorOverlay) {
overlay.showMessage(_errors);
}
options.initial = false;
},
error: function error(_error) {
log.error(_error);
},
close: function close() {
// WebSocket Server关闭前的推送
log.error('Disconnected!');
sendMessage('Close');
}
};
reloadApp
reloadApp() 代码位于 /client/default/utils/reloadApp.js 文件中。
此函数是个人感觉是一个关键函数,所以在此简单介绍下。
reloadApp() 是当代码文件发生变化后的处理逻辑。
reloadApp() 根据 hot 和 liveReload 两个属性类型做了不同处理方案。
当 hot 时,加载 webpack/hot/emitter 模块,用于触发热更新。
webpack/hot/emitter 模块是 webpack 中热更新定义的事件。
而当liveReload时,就直接刷新页面重新获取数据
也就是这两个hot和liveReload两个属性控制了能否实时更新代码
只不过hot采取热更新方式,而liveReload是直接刷新页面
并且hot优先级高于liveReload
HMR
提到webpack-dev-server,总是离不开热更新(HMR)的话题。热更新(HMR)是webpack-dev-server提供的比较重要的功能
开发时,往往只更新一小段代码,就需要在浏览器中查看效果。
而如果仅仅更新一小段代码,让浏览器刷新重新加载所有数据。那样会极大的浪费时间。
最好的结果就是打包编译时记录当前更新的模块文件,让浏览器只更新此模块文件。
这个技术就是叫作热更新(Hot Module Replacement)
webpack-dev-server中,热更新(HMR)其实是由webpack内置的一个plugin提供的:HotModuleReplacementPlugin
使用webpack-dev-server时,如果设置了hot属性,webpack-dev-server会自动添加这个plugin
:whale2:此代码在webpack-dev-server的 /lib/utils/DevServerPlugin.js
HotModuleReplacementPlugin每次编译后会生成一个 hash,对应其编译文件。WebSocket推送类型中具有一个 hash 类型,就是推送的 hash
WebSocket client处理消息时,会存储 hash。
然后在 reloadApp() 中根据当前 hash 执行 webpackHotUpdate 事件拉去最新代码。
hash: function hash(_hash) {
status.currentHash = _hash;
},
if (hot) {
log.info('App hot update...');
var hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
if (typeof self !== 'undefined' && self.window) {
// broadcast update to window
self.postMessage("webpackHotUpdate".concat(currentHash), '*');
}
} // allow refreshing the page only if liveReload isn't disabled
else if (liveReload) {
webpackHotUpdate事件是webpack中定义的一个事件,代码在 /hot/dev-server.js 。其功能是获取此次编译的代码。
if (module.hot) {
var lastHash;
var upToDate = function upToDate() {
return lastHash.indexOf(__webpack_hash__) >= 0;
};
var log = require("./log");
var check = function check() {
module.hot
.check(true)
.then(function (updatedModules) {
// HMR检查成功
if (!updatedModules) {
// 如果没有更新的modules,则刷新页面,
window.location.reload();
return;
}
if (!upToDate()) {
check();
}
// 调用HMR处理方法,只加载更新代码
require("./log-apply-result")(updatedModules, updatedModules);
if (upToDate()) {
log("info", "[HMR] App is up to date.");
}
})
.catch(function (err) {
// HMR检查失败,重新刷新页面
var status = module.hot.status();
if (["abort", "fail"].indexOf(status) >= 0) {
window.location.reload();
} else {
log("warning", "[HMR] Update failed: " + log.formatError(err));
}
});
};
var hotEmitter = require("./emitter");
// 事件定义
hotEmitter.on("webpackHotUpdate", function (currentHash) {
lastHash = currentHash;
if (!upToDate() && module.hot.status() === "idle") {
log("info", "[HMR] Checking for updates on the server...");
check();
}
});
log("info", "[HMR] Waiting for update signal from WDS...");
} else {
throw new Error("[HMR] Hot Module Replacement is disabled.");
}
总结
:whale2::whale2::whale2:
- 调试webpack-dev-server代码具有两种方法:浏览器和IDE
- webpack-dev-server内部使用了express框架作为服务器
- webpack-dev-server默认使用了内存流(memfs)存储打包文件
- webpack-dev-server使用了webpack提供的钩子函数监听打包编译
- wating状态是webpack提供的功能,其属性为:watch
- 热更新(HMR)是使用了webpack内置的HotModuleReplacementPlugin,webpack-dev-server每次在代码编译完成后将hash值推送给浏览器
如果此篇对您有所帮助,在此求一个star。项目地址: OrcasTeam/my-cli
本文参考
package.json
{
"name": "my-cli",
"version": "1.0.0",
"main": "index.js",
"author": "mowenjinzhao<yanzhangshuai@126.com>",
"license": "MIT",
"devDependencies": {
"@babel/core": "7.13.1",
"@babel/plugin-transform-runtime": "7.13.7",
"@babel/preset-env": "7.13.5",
"@babel/preset-react": "7.12.13",
"@babel/runtime-corejs3": "7.13.7",
"babel-loader": "8.2.2",
"clean-webpack-plugin": "3.0.0",
"css-loader": "5.0.2",
"file-loader": "6.2.0",
"html-webpack-plugin": "5.2.0",
"mini-css-extract-plugin": "1.3.8",
"style-loader": "2.0.0",
"webpack": "5.24.0",
"webpack-cli": "4.5.0",
"webpack-dev-server": "4.0.0-beta.0",
"webpack-merge": "5.7.3"
},
"dependencies": {
"react": "17.0.1",
"react-dom": "17.0.1"
},
"scripts": {
"start:dev": "webpack-dev-server --config build/webpack.dev.js",
"start": "webpack serve --config build/webpack.dev.js",
"build": "webpack --config build/webpack.pro.js",
"debug": "node --inspect-brk=5858 ./node_modules/webpack-dev-server/bin/webpack-dev-server.js",
"watch": "webpack --config build/webpack.watch.js"
},
"browserslist": {
"development": [
"chrome > 75"
],
"production": [
"ie 9"
]
}
}
webpack.watch.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common');
// 使用node。js的导出,将配置进行导出
module.exports = merge([
common(true),
{
mode:'development',
// 监听文件变化
watch: true,
// 监听选项
watchOptions: {
// 忽略某些目录 可以为正则或者数组
ignored: /node_modules/,
// 启动轮询, 默认为false 设置true或毫秒数来启动轮询
poll: false
}
}
])