如何在生产环境禁用 React Developer Tools 插件

无意间注意到,React 开发的页面在生产模式下依然会受到 React Developer Tools 浏览器插件的 “关照” ,安装此插件的浏览器可以像在开发环境下一样查看页面的组件结构和状态数据,同时还会额外加载一个 500k+ 大小的 js 文件。 但目前问题带来的影响较小,所以一些线上的网站比如:知乎、阿里云等都没有做处理。本文带你抽丝剥茧找出症节所在,并用三行代码精准解决此问题。

问题的发现与影响

某天在对线上的管理后台做性能评分的时候发现了一个 527k 的 js 文件躺在请求列表中,如下图所示:

在这里插入图片描述

从文件名称上大概能看出来这是 react dev 插件相关的一个 js 文件,同时注意到 react dev 插件正常工作;

在这里插入图片描述

但这是在生产环境下,我看就没有这个必要了吧。

而且在 LightHouse 性能打分结果分析中,它是 unused Javascript 建议移除掉的,虽然是从本地加载的 js 文件,但它的执行势必影响 HTML 的解析;

另外我的页面组件结构可以清晰的被人看到,按理说也没啥隐私数据可泄漏的,可要是组件写的不好被人嘲笑了那可就大问题了

我的诉求很简单:页面加载过程中不要请求 react_devtools_backend.js 文件,但我也不可能禁用或者卸载 React Developer Tools 插件

问题分析

先看看是谁请求了 react_devtools_backend.js 文件

在这里插入图片描述

在这里插入图片描述

很明显这个 injectGlobalHook.js 是 React Developer Tools 插件的一员,在接收到 ‘react-devtools-inject-backend’ 消息的时候就会加载 react_devtools_backend.js 文件 。

那 ‘react-devtools-inject-backend’ 这个消息是在什么地方、什么时机发出来的呢?

我在 react-devtools-extensions 源码的这个位置发现了消息源头

// https://github.com/facebook/react/blob/main/packages/react-devtools-extensions/src/main.js

// Check to see if React has loaded once per second in case React is added
// after page load
const loadCheckInterval = setInterval(function() {
  createPanelIfReactLoaded();
}, 1000);

createPanelIfReactLoaded();

// 大概意思就是看 ReactLoaded 没,如果 loaded 了 就执行 createPanelIfReactLoaded()

// 在 createPanelIfReactLoaded 方法中有这么一段逻辑,正是发出 react-devtools-inject-backend 消息的地方
function createPanelIfReactLoaded(){
  //...
  chrome.devtools.inspectedWindow.eval(
    'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
    function(pageHasReact, error) {
      if (!pageHasReact || panelCreated) {
        return;
      }
      // ...
      chrome.devtools.inspectedWindow.eval(
        `window.postMessage({ source: 'react-devtools-inject-backend' }, '*');`,
        function(response, evalError) {
          if (evalError) {
            console.error(evalError);
          }
        },
      );
    }
 )
 //...
}

如果条件 window.REACT_DEVTOOLS_GLOBAL_HOOK && window.REACT_DEVTOOLS_GLOBAL_HOOK.renderers.size > 0 满足,插件就认为当前的页面包含 React ,之后进行一系列的对应操作,包括发出消息 ‘react-devtools-inject-backend’

问题的关键点找到了,要想阻止 ‘react-devtools-inject-backend’ 消息的发出,就不能让 window.REACT_DEVTOOLS_GLOBAL_HOOK && window.REACT_DEVTOOLS_GLOBAL_HOOK.renderers.size > 0 成为现实

为了便于理解,我们简单点从 chrome 插件的运行原理说起。因为复杂的我不会。

React Developer Tools 插件运行原理

下面的配置文件是 react-devtools-extensions 的指导纲领,我从中截取了一部分来说明当页面加载的时候它都做了哪些事。

react/manifest.json at main · facebook/react · GitHub

// https://github.com/facebook/react/blob/main/packages/react-devtools-extensions/chrome/manifest.json
{
  ...
  "content_scripts": [
    {
      "matches": [
        "<all_urls>"
      ],
      "js": [
        "build/injectGlobalHook.js"
      ],
      "run_at": "document_start"
    }
  ]
  ...
}

document_start 的解释是 Scripts are injected after any files from css, but before any other DOM is constructed or any other script is run.

不难看出这个插件对所有页面都会插一脚,在 HTML 解析的时候执行 build/injectGlobalHook.js 并且执行时机要早于我们自己的代码

build/injectGlobalHook.js 主要干两件事

其一是初始化这样一个 Hook 并挂载到 window 上,这个 Hook 正是我们上面提到的,要重点关照的东西。

const hook: DevToolsHook = {
    rendererInterfaces,
    listeners,
    // Fast Refresh for web relies on this.
    renderers,
    emit,
    getFiberRoots,
    inject,
    on,
    off,
    sub,
   	...
  };

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

(可以在 React 项目中的 console 中输入 window.__REACT_DEVTOOLS_GLOBAL_HOOK__ 查看具体形态)

其二就是定义和初始化各种事件的处理逻辑,下面的一个消息回调就是要加载 react_devtools_backend.js 文件。

// build/injectGlobalHook.js: ;
// build/injectGlobalHook.js ;
window.addEventListener('message', function onMessage({ data,source }) {
  switch (data.source) {
   	...
    case 'react-devtools-inject-backend':
      const script = document.createElement('script');
      script.src = chrome.runtime.getURL('build/react_devtools_backend.js');
      document.documentElement.appendChild(script);
      script.parentNode.removeChild(script);
      break;
  }
});

好了,做完这两件事以后 ,插件就开始每一秒钟检查一次当前页面是否包含 React ,我把上面展示消息源头的代码再粘过来。

// after page load 每一秒钟检查一次当前页面是否包含 React
const loadCheckInterval = setInterval(function() {
  createPanelIfReactLoaded();
}, 1000);

createPanelIfReactLoaded();

// 在 createPanelIfReactLoaded 方法中有这么一段逻辑,正是发出 react-devtools-inject-backend 消息的地方
function createPanelIfReactLoaded(){
  //...
  chrome.devtools.inspectedWindow.eval(
    'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
    function(pageHasReact, error) {
      if (!pageHasReact || panelCreated) {
        return;
      }
      // ...
      chrome.devtools.inspectedWindow.eval(
        `window.postMessage({ source: 'react-devtools-inject-backend' }, '*');`,
        function(response, evalError) {
          if (evalError) {
            console.error(evalError);
          }
        },
      );
    }
 )
 //...
}

// 只要条件 window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0 得到满足,插件就会认为当前页面包含 React ,进而发送消息 'react-devtools-inject-backend',加载对应的 js 文件。

条件 window.REACT_DEVTOOLS_GLOBAL_HOOK && window.REACT_DEVTOOLS_GLOBAL_HOOK.renderers.size > 0 是如何成为现实的?

window.REACT_DEVTOOLS_GLOBAL_HOOK 是插件通过 injectGlobalHook.js 文件的执行挂载到全局,它肯定不是 undefined;

问题是**这个 Hook 下面的 renderers 中的成员是由谁往里‘充值’**呢?

React 做助攻

在 React 源码中我又找到了这么一段代码,生动展示了它是如何往 window.__REACT_DEVTOOLS_GLOBAL_HOOK__下面的 renderers 中填充成员的过程。

// https://github.com/facebook/react/blob/65e32e58b6057db1fdfed95a942fad4fc96da191/packages/react-reconciler/src/ReactFiberDevToolsHook.new.js#L92

export const isDevToolsPresent =
  typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined';

export function injectInternals(internals: Object): boolean {
  if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
    // No DevTools
    return false;
  }
  const hook = __REACT_DEVTOOLS_GLOBAL_HOOK__;
  try {
    if (enableSchedulingProfiler) {
      // Conditionally inject these hooks only if Timeline profiler is supported by this build.
      // This gives DevTools a way to feature detect that isn't tied to version number
      // (since profiling and timeline are controlled by different feature flags).
      internals = {
        ...internals,
        getLaneLabelMap,
        injectProfilingHooks,
      };
    }

    rendererID = hook.inject(internals);

    // We have successfully injected, so now it is safe to set up hooks.
    injectedHook = hook;
  } catch (err) {
    // Catch all errors because it is unsafe to throw during initialization.
    if (__DEV__) {
      console.error('React instrumentation encountered an error: %s.', err);
    }
  }
  if (hook.checkDCE) {
    // This is the real DevTools.
    return true;
  } else {
    // This is likely a hook installed by Fast Refresh runtime.
    return false;
  }
}

只要 window.REACT_DEVTOOLS_GLOBAL_HOOK 不是 undefined ,它就会调用该 Hook 中的 inject 方法,inject 方法中正是 通过renderers.set(id, renderer); 这句往 renderers 中填充了成员。

那边只要发现 renderers 的 size 不为 0 就会认为当前页面包含 React ,进而发送消息 ‘react-devtools-inject-backend’,加载对应的 js 文件。

这就全部对上了,整体的流程简述如下:

插件首先在每个页面加载前向 window 上挂载一个 REACT_DEVTOOLS_GLOBAL_HOOK,之后便不停的查看 Hook 中的 renderers 中有没有东西,一时发现 renderers 中有了内容就判定当前页面包含 React;

而这边 React 在初始化过程中会检查 window 上有没有 REACT_DEVTOOLS_GLOBAL_HOOK 如何有的话就调用其内部的 inject 方法向renderers 中填充内容。

解决方案

不能让 window.REACT_DEVTOOLS_GLOBAL_HOOK && window.REACT_DEVTOOLS_GLOBAL_HOOK.renderers.size > 0 成为现实

很简单,往 head 中添加 script 标签,设置 window.REACT_DEVTOOLS_GLOBAL_HOOK = undefined

发现并不可行。

因为插件往 window 上挂载 REACT_DEVTOOLS_GLOBAL_HOOK 的同时,设置了它的 configurable:false

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

那就破坏它的 inject 方法(它向 React 提供 inject 方法, 后者调用 inject 往 renderers 中添加成员)

<script>
   var devToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
    if(!!devToolsHook &&  devToolsHook['inject']){
      // 打不过你,还打不过你的狗么?  
      devToolsHook['inject'] = Function.prototype
    }
</script>

问题得到完美解决

在这里插入图片描述

参考:chrome 插件开发文档 - Chrome Developers

Chrome Extension插件开发概述 - 墨天轮 (modb.pro)

react/injectGlobalHook.js at main · facebook/react · GitHub

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值