React-dev-utils
主要用于 React 开发过程中的开发工具,例如创建开发服务器、热更新、错误处理等功能。
在开发时遇到了一个问题,在项目第一次热更新时,即使项目编译成功也会在document.body添加一个iframe覆盖
react-dev-utils: 9.0.3
观察ifame元素查找关键字,并打好debugger。查看调用栈发现问题,便开始了研究之路
react-dev-utils
注册
websocket
var connection = new SockJS(
url.format({
protocol: window.location.protocol,
hostname: window.location.hostname,
port: window.location.port,
// Hardcoded in WebpackDevServer
pathname: '/sockjs-node',
onmessage监听消息。并根据相应消息进行对应的逻辑处理
比如编译结果成功时会接收到'{"type":"ok"}'的消息并调用handleSuccess
函数
function handleSuccess() {
clearOutdatedErrors();
var isHotUpdate = !isFirstCompilation;
isFirstCompilation = false;
hasCompileErrors = false;
// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(function onHotUpdateSuccess() {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
tryDismissErrorOverlay();
调用tryDismissErrorOverlay
函数,底层是调用react-error-overlay
的dismissBuildError
函数
react-error-overlay
的react-error-overlay
函数
export function dismissBuildError() {
currentBuildError = null;
update();
update中的内容
function update() {
// Loading iframe can be either sync or async depending on the browser.
if (isLoadingIframe) {
// Iframe is loading.
// First render will happen soon--don't need to do anything.
return;
if (isIframeReady) {
// Iframe is ready.
// Just update it.
updateIframeContent();
return;
// We need to schedule the first render.
isLoadingIframe = true;
const loadingIframe = window.document.createElement('iframe');
applyStyles(loadingIframe, iframeStyle);
loadingIframe.onload = function () {
const iframeDocument = loadingIframe.contentDocument;
if (iframeDocument != null && iframeDocument.body != null) {
iframe = loadingIframe;
const script =
loadingIframe.contentWindow.document.createElement('script');
script.type = 'text/javascript';
script.innerHTML = iframeScript;
iframeDocument.body.appendChild(script);
const appDocument = window.document;
appDocument.body.appendChild(loadingIframe);
到这里我们可以发现iframe中的弹窗是在这里添加的,但是当第一次编译成功时,react-error-overlay中的全局变量isLoadingIframe和isIframeReady都为false。那么当第一次热更新时就会走创建iframe的逻辑,当手动删除后,而后的更新都不会增加。这是因为其添加的全局hookwindow.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__
提供的方法iframeReady
重置了变量,不会走创建iframe的逻辑
出于好奇,react-dev-utils
的websocket是和谁建立的,决定研究下去
全局搜索sockjs-node
。搜索出来的结果还是挺多的有53个结果。仔细一想,我们启动服务时使用了webpack-dev-server
启动了一恶搞服务器。客户端建立websocket需要和服务器建立连接,便先以webpack-dev-server
路径下的资源这里开始查找
点击去一看,这里果然是用于创建socket
但仔细看下来后发现这里的websocket也是客户端用户接收消息的,并未查看到推送消息相关的😭
按照之前查找的结果,重新查找,最终在node_modules/webpack-dev-server/lib/Server.js路径下找到了
整理的流程图
webpack-dev-server注册websocket主要流程代码自我理解
使用 sockjs.createServer
创建 WebSocket 服务器实例,其中 sockjs_url
指定了客户端 JavaScript 库的 URL,该库用于创建 WebSocket 连接。其中sockjs_url
指定了用于创建 WebSocket 服务的 sockjs-client 的 URL。这个 URL 是由 webpack-dev-server
通过 html-webpack-plugin
插件生成的,会注入到 HTML 模板中
const socket = sockjs.createServer({
// Use provided up-to-date sockjs-client
sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
// Limit useless logs
log: (severity, line) => {
if (severity === 'error') {
this.log.error(line);
} else {
this.log.debug(line);
监听 WebSocket 服务器的 connection
事件。这里会检查连接的请求头是否符合要求,如果不符合则关闭连接
socket.on('connection', (connection) => {
if (!connection) {
return;
!this.checkHost(connection.headers) ||
!this.checkOrigin(connection.headers)
this.sockWrite([connection], 'error', 'Invalid Host/Origin header');
connection.close();
return;
this.sockets.push(connection);
connection.on('close', () => {
const idx = this.sockets.indexOf(connection);
if (idx >= 0) {
this.sockets.splice(idx, 1);
// ...
启用配置发送消息
return this.listeningApp.listen(port, hostname, (err) => {
// ...
if (this.hot) {
this.sockWrite([connection], 'hot');
if (this.progress) {
this.sockWrite([connection], 'progress', this.progress);
if (this.clientOverlay) {
this.sockWrite([connection], 'overlay', this.clientOverlay);
if (this.clientLogLevel) {
this.sockWrite([connection], 'log-level', this.clientLogLevel);
if (!this._stats) {
return;
this._sendStats([connection], this._stats.toJson(STATS), true);
// ...
如果已经有了编译结果,向客户端发送编译结果
this._sendStats([connection], this._stats.toJson(STATS), true);
其中_sendStats的作用:
_sendStats
方法,该方法是其内部实现的一个方法,它的作用是将编译结果通过WebSocket发送给客户端,更新客户端的界面。
_sendStats
方法接受两个参数,第一个参数是发送目标,这里传入了this.sockets
,也就是所有与服务器建立了WebSocket连接的客户端;第二个参数是编译结果,这里调用了stats.toJson(STATS)
方法,将webpack的编译结果转换成JSON格式
接着,这个函数将编译结果保存在this._stats
属性中,方便在其他地方使用。由于webpack-dev-server使用了WebSocket实现了热更新功能,所以在每次编译完成后,需要将编译结果发送到客户端,更新客户端的界面
如果没编译好,会在addHooks处理
const addHooks = (compiler) => {
const { compile, invalid, done } = compiler.hooks;
compile.tap('webpack-dev-server', invalidPlugin);
invalid.tap('webpack-dev-server', invalidPlugin);
done.tap('webpack-dev-server', (stats) => {
this._sendStats(this.sockets, stats.toJson(STATS));
this._stats = stats;
通过调用 compile.tap
方法,为 compile 和 invalid 这两个钩子函数注册一个名为 "webpack-dev-server" 的插件(plugin),并指定执行 invalidPlugin 函数作为回调函数。这里用了 tap 方法,表示添加一个新的回调函数到钩子的回调队列中。
最后,调用 done.tap
方法,为 done 钩子函数也注册一个名为 "webpack-dev-server" 的插件,并指定一个回调函数。当编译完成后,会调用这个回调函数,将编译生成的统计信息(stats)转换成 JSON 格式,然后通过 _sendStats
方法发送给当前正在监听的所有 WebSocket 连接。同时,也将这个统计信息保存在 this._stats
变量中,以便后续使用
使用 socket.installHandlers
方法将 WebSocket 服务器安装到 HTTP 服务器中。大概查了一下这里的作用是: WebSocket 服务器的处理逻辑绑定到 HTTP 服务器中,从而将 HTTP 请求转发到 WebSocket 服务器中处理。
socket.installHandlers(this.listeningApp, {
prefix: this.sockPath,
如果传入了回调函数 fn
,则在 HTTP 服务器启动完成时调用该函数
if (fn) {
fn.call(this.listeningApp, err);
const addHooks = (compiler) => {
const { compile, invalid, done } = compiler.hooks;
compile.tap('webpack-dev-server', invalidPlugin);
invalid.tap('webpack-dev-server', invalidPlugin);
done.tap('webpack-dev-server', (stats) => {
this._sendStats(this.sockets, stats.toJson(STATS));
this._stats = stats;
_sendStats
方法,该方法是webpack-dev-server内部实现的一个方法,它的作用是将编译结果通过WebSocket发送给客户端,更新客户端的界面
_sendStats(sockets, stats, force) {
!force &&
stats &&
(!stats.errors || stats.errors.length === 0) &&
stats.assets &&
stats.assets.every((asset) => !asset.emitted)
return this.sockWrite(sockets, 'still-ok');
this.sockWrite(sockets, 'hash', stats.hash);
if (stats.errors.length > 0) {
this.sockWrite(sockets, 'errors', stats.errors);
} else if (stats.warnings.length > 0) {
this.sockWrite(sockets, 'warnings', stats.warnings);
} else {
this.sockWrite(sockets, 'ok');
/sockjs-node
是由 webpack-dev-server
提供的,它是一个用于开发环境的静态资源服务器,可以为前端项目提供本地开发环境。在使用 react-dev-utils
的 WebpackDevServerUtils.createCompiler
方法创建编译器时,会在内部通过 webpack-dev-server
提供的 /sockjs-node
接口,与客户端通过 WebSocket 建立长连接,实现实时更新和热加载等功能。
在开发环境中,客户端文件会被打包成多个 Chunk,当其中某个 Chunk 发生变化时,webpack-dev-server
会通过 WebSocket 推送消息到客户端。具体来说,当文件发生变化时,webpack-dev-server
会发送一个 JSON 数据到客户端,该数据包含了需要重新加载的 Chunk 名称和路径等信息。客户端接收到这个 JSON 数据后,就会使用新的 Chunk 来替换旧的 Chunk,从而实现实时更新和热加载。