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;
},
};