相关文章推荐
酷酷的煎鸡蛋  ·  Golang WebSocket Ping ...·  1 月前    · 
飘逸的荔枝  ·  WebSockets support in ...·  3 周前    · 

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-overlaydismissBuildError函数

    react-error-overlayreact-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-utilsWebpackDevServerUtils.createCompiler 方法创建编译器时,会在内部通过 webpack-dev-server 提供的 /sockjs-node 接口,与客户端通过 WebSocket 建立长连接,实现实时更新和热加载等功能。

    在开发环境中,客户端文件会被打包成多个 Chunk,当其中某个 Chunk 发生变化时,webpack-dev-server 会通过 WebSocket 推送消息到客户端。具体来说,当文件发生变化时,webpack-dev-server 会发送一个 JSON 数据到客户端,该数据包含了需要重新加载的 Chunk 名称和路径等信息。客户端接收到这个 JSON 数据后,就会使用新的 Chunk 来替换旧的 Chunk,从而实现实时更新和热加载。