devtools-detector 源码阅读

1,375 阅读2分钟

devtools-detector 检查控制台是否被打开

用法

import { addListener, launch } from 'devtools-detector';
const view = document.createElement('div');
document.body.appendChild(view);

// 1. add listener
addListener(
  isOpen =>
    (view.innerText = isOpen
      ? 'devtools status: open'
      : 'devtools status: close')
);
// 2. launch detect
launch();

实例化

const defaultDetector = new DevtoolsDetector({
  // 会按照 checker 的顺序执行检查
  checkers: [
    checkers.elementIdChecker,
    checkers.regToStringChecker,
    checkers.functionToStringChecker,
    checkers.depRegToStringChecker,
    checkers.debuggerChecker,
  ],
});

使用5种方法检查控制台是否打开

addListener

把回调函数加入队列

addListener(listener: DevtoolsDetectorListener) {
    this._listeners.push(listener);
  }

launch

启动监听

launch() {
    if (this._detectLoopDelay <= 0) {
      this.setDetectDelay(500);
    }
    if (this._detectLoopStopped) {
      this._detectLoopStopped = false;
      this._detectLoop();
    }
  }

_detectLoopDelay: 检查间隔

_detectLoopStopped: 是否停止检查, 多次调用 launch 还是触发一次定时器

stop

stop() {
    if (!this._detectLoopStopped) {
      this._detectLoopStopped = true;
      clearTimeout(this._timer);
    }
  }

根据 _detectLoopStopped 清除定时器

_detectLoop

private async _detectLoop() {
  let isOpen = false;
  let checkerName = '';

  for (const checker of this._checkers) {
    // 检查浏览器兼容性
    const isEnable = await checker.isEnable();
    if (isEnable) {
      checkerName = checker.name;
      isOpen = await checker.isOpen();
    }

    // 任意一个 checker 返回 true 就视为 devtools 已打开
    if (isOpen) {
      break;
    }
  }

  if (isOpen != this._isOpen) {
    this._isOpen = isOpen;
    this._broadcast({
      isOpen,
      checkerName,
    });
  }

  if (this._detectLoopDelay > 0) {
    this._timer = setTimeout(() => this._detectLoop(), this._detectLoopDelay);
  } else {
    this.stop();
  }
}

5种检查方式分析

checkers: [
  checkers.elementIdChecker,
  checkers.regToStringChecker,
  checkers.functionToStringChecker,
  checkers.depRegToStringChecker,
  checkers.debuggerChecker,
],

elementIdChecker

原理: 使用 defineProperty 劫持元素的 get 方法检查控制台是否打开。每次控制台打开,会触发 get 方法

const ele = createElement('div');
let isOpen = false;

Object.defineProperty(ele, 'id', {
  get() {
    isOpen = true;
    return elementIdChecker.name;
  },
  configurable: true,
});

// 浏览器兼容性
match({
  /** 匹配所有浏览器 */
  includes: [true],
  excludes: [isIE, isEdge, isFirefox],
});

regToStringChecker

原理: 重写正则表达式的 toString 方法实现。每次控制台打开,会触发正则表达式的 toString 方法

const reg = / /;
let isOpen = false;

reg.toString = () => {
  isOpen = true;
  return regToStringChecker.name;
};
// 浏览器兼容性
match({
  /** 匹配所有浏览器 */
  includes: [true],
  /** 排除 webkit */
  excludes: [isWebkit],
});

functionToStringChecker

原理: 重写函数的 toString 方法实现。每次控制台打开,会触发 toString 方法


function devtoolsTestFunction() {
  // nothing todo
}

let count = 0;

devtoolsTestFunction.toString = () => {
  count++;

  return '';
};

export const functionToStringChecker: DevtoolsStatusChecker = {
  name: 'function-to-string',
  async isOpen(): Promise<boolean> {
    count = 0;

    log(devtoolsTestFunction);
    clear();

    return count === 2;
  },
  async isEnable(): Promise<boolean> {
    return match({
      /** 匹配所有浏览器 */
      includes: [true],
      /** 排除 firefox 和  ipad 或 iphone 上的 chrome */
      excludes: [
        isFirefox,
        // ipad 或 iphone 上的 chrome
        (isIpad || isIphone) && isChrome,
      ],
    });
  },
};

不知道为什么 return count === 2; 而不是 return count === 1;

depRegToStringChecker

depRegToStringChecker 和 regToStringChecker 一样,没看出区别

就是判断的浏览器

debuggerChecker

利用打开控制台,触发一个立即执行函数,产生 debugger 断点效果。判断执行时间是否超过 100ms,如果超过 100ms,则认为打开控制台

function now() {
  if (performance) {
    return performance.now();
  } else {
    return Date.now();
  }
}

export const debuggerChecker: DevtoolsStatusChecker = {
  name: 'debugger-checker',
  async isOpen(): Promise<boolean> {
    const startTime = now();

    // tslint:disable-next-line:no-empty only-arrow-functions
    (function () {}.constructor('debugger')());

    return now() - startTime > 100;
  },
  async isEnable(): Promise<boolean> {
    return true;
  },
};