Chrome 插件使用 React Devtools 调试问题

3,980 阅读3分钟

Author:Hongying Guo
Editor:Xiaoli Tan

背景

我们开发了一个浏览器插件,用xigua~代称,用于运营同学等便捷的查看视频的一些数据,该插件目前存在很多重复渲染的问题,最近打算对 xigua~ 项目进行进行一下组件渲染优化,想先用 React Devtools 插件 对当前的该插件进行性能检测和代码调试,便于总结优化前后的收益。 但是,打开 Components 调试工具会发现 Dom 树只有一个原页面的根结点,无法检测到插件的 Dom 树。

补充一下React Devtools 的 icon 的颜色含义:

蓝色:生成环境 红色:开发环境 灰色:非 React 项目 并且,在西瓜 PC 站是,React Devtools 的 icon 是蓝色的。

在 Youtube 页面时,React Devtools 的 icon 是灰色的。

这说明,React Devtools 插件并没有检测到插件的上下文。

为什么不能对插件进行调试?

Devtools 插件 API

Devtools 插件可以通过以下的几组 API 分别对面板、被审查窗口、网络请求进行操作或监听:

  • chrome.devtools.panels:面板相关;
  • chrome.devtools.inspectedWindow:获取被审查窗口的有关信息;
  • chrome.devtools.network:获取有关网络请求的信息;

React Devtools 插件

在页面加载的时候,React Devtools插件会设置一个名为__REACT_DEVTOOLS_GLOBAL_HOOK__ 全局变量 hook,然后 React 初始化的时候就是通过这个这个 hook 与插件进行通信的。

如下面的源码,如果检测到当前页面的类型是 html 就会插入一个包含 script 标签,包含了初始化__REACT_DEVTOOLS_GLOBAL_HOOK__的代码。

if ('text/html' === document.contentType) {
  injectCode( // injectCode 即 插入 script 标签的逻辑
    ';(' +
      installHook.toString() + // installHook中会定__REACT_DEVTOOLS_GLOBAL_HOOK__
      '(window))' +
      saveNativeValues +
      detectReact,
  );
}

Object.defineProperty(
    target,
    '__REACT_DEVTOOLS_GLOBAL_HOOK__',
    ({
      configurable: __DEV__,
      enumerable: false,
      get() {
        return hook;
      },
    }: Object),
  );

源码中,是通过检查全局变量__REACT_DEVTOOLS_GLOBAL_HOOK__是否存在以及是否有 render 实例来判断当前页面是否包含 react。

chrome.devtools.inspectedWindow.eval(
    'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
    function(pageHasReact, error) {
      if (!pageHasReact || panelCreated) {
        return;
      }
    }
    // ....
}

从上面的逻辑中可以看到,Devtools 始终是与 window 进行交互,window 即当前页面的命名空间,而 Devtools 是无法与 Buddy 插件的命名空间进行通信的。

图中可以看到 Devtools 是可以和 Inspected Window 和 Background Page 进行通信的,无法与 Content Script 进行通信,这也是为什么 React Devtools 无法对 Buddy 插件的页面进行调试与性能检测的原因。

Content Script 插件如何调试?

  1. 使用开发者工具的 Performance,React 在开发环境会自主上报每个组件的渲染耗时。这种方案比较适合对项目的整体性能查看,当需要对某一个具体的组件进行渲染检测的时候不是很方便。
  2. 使用 React 的实验性 API Profiler,如下,通过使用 Profiler 组件包裹并定义 onRender 的回调函数,自主的打印组件的渲染时间,来测试组件的渲染耗时。
import React, { unstable_Profiler as Profiler  } from 'react';

export default function TestComp () {
    function onRenderCallback (
        id, // 发生提交的 Profiler 树的 “id”
        phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一
        actualDuration, // 本次更新 committed 花费的渲染时间
        baseDuration, // 估计不使用 memoization 的情况下渲染整颗子树需要的时间
        startTime, // 本次更新中 React 开始渲染的时间
        commitTime, // 本次更新中 React committed 的时间
        interactions // 属于本次更新的 interactions 的集合
    ) {
        console.log(`${id}组件渲染时间:`, actualDuration);
        console.log(`${id}组件不使用Memo估计渲染时间:`, baseDuration);
    }
    
    return (
        <Profiler id="XgVideo" onRender={onRenderCallback}>
            <div>
                // 组件内容
            </div>
        </Profiler>
    )
}

结果如图:

Profiler 的原理是使用 Performance.measure() API 来实现上报标记的。 例如,通过下面的代码,我们也可以自主上报信息到 开发者工具的 Performance:

<!DOCTYPE html>
<html>
<head>
        <title></title>
</head>
<body>
        <script type="text/javascript">
                performance.mark("测试上报Performance-start");
                // 等待一些时间。
                setTimeout(function() {
                  // 标志时间的结束。
                  performance.mark("测试上报Performance-end");

                  // 测量两个不同的标志。
                  performance.measure(
                    "测试上报Performance",
                    "测试上报Performance-start",
                    "测试上报Performance-end"
                  );

                  // 清除存储的标志位
                  performance.clearMarks();
                  performance.clearMeasures();
                }, 1000);
        </script>
</body>
</html>

补充:禁止 React Devtools 改变 state 和 props

在生产环境也可以通过 React Devtools 插件对 state 和 props 进行更改并且生效,这里可能会存在一定的安全问题,可以通过在使用 React 之前运行下面的代码来实现在生成环境一定程度上禁止修改 state 和 props。

if (process.env.NODE_ENV == 'production' && typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') {
    __REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function() {};
}

参考链接