vue-debug-helper

Vue components debug helper

Version au 27/04/2022. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         vue-debug-helper
// @name:en      vue-debug-helper
// @name:zh      Vue调试分析助手
// @name:zh-TW   Vue調試分析助手
// @name:ja      Vueデバッグ分析アシスタント
// @namespace    https://github.com/xxxily/vue-debug-helper
// @homepage     https://github.com/xxxily/vue-debug-helper
// @version      0.0.2
// @description  Vue components debug helper
// @description:en  Vue components debug helper
// @description:zh  Vue组件探测、统计、分析辅助脚本
// @description:zh-TW  Vue組件探測、統計、分析輔助腳本
// @description:ja  Vueコンポーネントの検出、統計、分析補助スクリプト
// @author       ankvps
// @icon         https://cdn.jsdelivr.net/gh/xxxily/vue-debug-helper@main/logo.png
// @match        http://*/*
// @match        https://*/*
// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_getTab
// @grant        GM_saveTab
// @grant        GM_getTabs
// @grant        GM_openInTab
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @run-at       document-start
// @connect      127.0.0.1
// @license      GPL
// ==/UserScript==
(function (w) { if (w) { w._vueDebugHelper_ = 'https://github.com/xxxily/vue-debug-helper'; } })();

class Debug {
  constructor (msg) {
    const t = this;
    msg = msg || 'debug message:';
    t.log = t.createDebugMethod('log', null, msg);
    t.error = t.createDebugMethod('error', null, msg);
    t.info = t.createDebugMethod('info', null, msg);
    t.warn = t.createDebugMethod('warn', null, msg);
  }

  create (msg) {
    return new Debug(msg)
  }

  createDebugMethod (name, color, tipsMsg) {
    name = name || 'info';

    const bgColorMap = {
      info: '#2274A5',
      log: '#95B46A',
      error: '#D33F49'
    };

    return function () {
      if (!window._debugMode_) {
        return false
      }

      const curTime = new Date();
      const H = curTime.getHours();
      const M = curTime.getMinutes();
      const S = curTime.getSeconds();
      const msg = tipsMsg || 'debug message:';

      const arg = Array.from(arguments);
      arg.unshift(`color: white; background-color: ${color || bgColorMap[name] || '#95B46A'}`);
      arg.unshift(`%c [${H}:${M}:${S}] ${msg} `);
      window.console[name].apply(window.console, arg);
    }
  }

  isDebugMode () {
    return Boolean(window._debugMode_)
  }
}

var Debug$1 = new Debug();

var debug = Debug$1.create('vue-debug-helper message:');

/**
 * 对特定数据结构的对象进行排序
 * @param {object} obj 一个对象,其结构应该类似于:{key1: [], key2: []}
 * @param {boolean} reverse -可选 是否反转、降序排列,默认为false
 * @param {object} opts -可选 指定数组的配置项,默认为{key: 'key', value: 'value'}
 * @param {object} opts.key -可选 指定对象键名的别名,默认为'key'
 * @param {object} opts.value -可选 指定对象值的别名,默认为'value'
 * @returns {array} 返回一个数组,其结构应该类似于:[{key: key1, value: []}, {key: key2, value: []}]
 */
const objSort = (obj, reverse, opts = { key: 'key', value: 'value' }) => {
  const arr = [];
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key) && Array.isArray(obj[key])) {
      const tmpObj = {};
      tmpObj[opts.key] = key;
      tmpObj[opts.value] = obj[key];
      arr.push(tmpObj);
    }
  }

  arr.sort((a, b) => {
    return a[opts.value].length - b[opts.value].length
  });

  reverse && arr.reverse();
  return arr
};

/**
 * 根据指定长度创建空白数据
 * @param {number} size -可选 指定数据长度,默认为1024
 * @param {string} str - 可选 指定数据的字符串,默认为'd'
 */
function createEmptyData (size = 1024, str = 'd') {
  const arr = [];
  arr.length = size + 1;
  return arr.join(str)
}

window.vueDebugHelper = {
  /* 存储全部未被销毁的组件对象 */
  components: {},
  /* 存储全部创建过的组件的概要信息,即使销毁了概要信息依然存在 */
  componentsSummary: {},
  /* 基于componentsSummary的组件情况统计 */
  componentsSummaryStatistics: {},
  /* 已销毁的组件概要信息列表 */
  destroyList: [],
  /* 基于destroyList的组件情况统计 */
  destroyStatistics: {},
  /* 给组件注入空白数据的配置信息 */
  ddConfig: {
    enabled: false,
    filters: [],
    size: 1024
  }
};

const helper = window.vueDebugHelper;

const methods = {
  objSort,
  createEmptyData,
  /* 清除全部helper的全部记录数据,以便重新统计 */
  clearAll () {
    helper.components = {};
    helper.componentsSummary = {};
    helper.componentsSummaryStatistics = {};
    helper.destroyList = [];
    helper.destroyStatistics = {};
  },

  /**
   * 对当前的helper.components进行统计与排序
   * 如果一直没运行过清理函数,则表示统计页面创建至今依然存活的组件对象
   * 运行过清理函数,则表示统计清理后新创建且至今依然存活的组件对象
   */
  componentsStatistics (reverse = true) {
    const tmpObj = {};

    Object.keys(helper.components).forEach(key => {
      const component = helper.components[key];

      tmpObj[component._componentName]
        ? tmpObj[component._componentName].push(component)
        : (tmpObj[component._componentName] = [component]);
    });

    return objSort(tmpObj, reverse, {
      key: 'componentName',
      value: 'componentInstance'
    })
  },

  /**
   * 对componentsSummaryStatistics进行排序输出,以便可以直观查看组件的创建情况
   */
  componentsSummaryStatisticsSort (reverse = true) {
    return objSort(helper.componentsSummaryStatistics, reverse, {
      key: 'componentName',
      value: 'componentsSummary'
    })
  },

  /**
   * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况
   */
  destroyStatisticsSort (reverse = true) {
    return objSort(helper.destroyStatistics, reverse, {
      key: 'componentName',
      value: 'destroyList'
    })
  },

  /**
   * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况
   */
  getDestroyByDuration (duration = 1000) {
    const destroyList = helper.destroyList;
    const destroyListLength = destroyList.length;
    const destroyListDuration = destroyList.map(item => item.duration).sort();
    const maxDuration = Math.max(...destroyListDuration);
    const minDuration = Math.min(...destroyListDuration);
    const avgDuration =
      destroyListDuration.reduce((a, b) => a + b, 0) / destroyListLength;
    const durationRange = maxDuration - minDuration;
    const durationRangePercent = (duration - minDuration) / durationRange;

    return {
      destroyList,
      destroyListLength,
      destroyListDuration,
      maxDuration,
      minDuration,
      avgDuration,
      durationRange,
      durationRangePercent
    }
  },

  /**
   * 获取组件的调用链信息
   */
  getComponentChain (component, moreDetail = false) {
    const result = [];
    let current = component;
    let deep = 0;

    while (current && deep < 50) {
      deep++;

      if (moreDetail) {
        result.push({
          name: current._componentName,
          componentsSummary: helper.componentsSummary[current._uid] || null
        });
      } else {
        result.push(current._componentName);
      }

      current = current.$parent;
    }

    if (moreDetail) {
      return result
    } else {
      return result.join(' -> ')
    }
  },

  /**
   * 给指定组件注入大量空数据,以便观察组件的内存泄露情况
   * @param {Array|string} filter -必选 指定组件的名称,如果为空则表示注入所有组件
   * @param {number} size -可选 指定注入空数据的大小,单位Kb,默认为1024Kb,即1Mb
   * @returns
   */
  dd (filter, size = 1024) {
    filter = filter || [];

    /* 如果是字符串,则支持通过, | 两个符号来指定多个组件名称的过滤器 */
    if (typeof filter === 'string') {
      /* 移除前后的, |分隔符,防止出现空字符的过滤规则 */
      filter.replace(/^(,|\|)/, '').replace(/(,|\|)$/, '');

      if (/\|/.test(filter)) {
        filter = filter.split('|');
      } else {
        filter = filter.split(',');
      }
    }

    helper.ddConfig = {
      enabled: true,
      filters: filter,
      size
    };
  },
  /* 禁止给组件注入空数据 */
  undd () {
    helper.ddConfig = {
      enabled: false,
      filters: [],
      size: 1024
    };
  }
};

helper.methods = methods;

function mixinRegister (Vue) {
  if (!Vue || !Vue.mixin) {
    debug.error('未检查到VUE对象,请检查是否引入了VUE,且将VUE对象挂载到全局变量window.Vue上');
    return false
  }

  Vue.mixin({
    beforeCreate: function () {
      const tag = this.$options?._componentTag || this.$vnode?.tag || this._uid;
      const chain = helper.methods.getComponentChain(this);
      this._componentTag = tag;
      this._componentChain = chain;
      this._componentName = isNaN(Number(tag)) ? tag.replace(/^vue\-component\-\d+\-/, '') : 'anonymous-component';
      this._createdTime = Date.now();

      /* 判断是否为函数式组件,函数式组件无状态 (没有响应式数据),也没有实例,也没生命周期概念 */
      if (this._componentName === 'anonymous-component' && !this.$parent && !this.$vnode) {
        this._componentName = 'functional-component';
      }

      helper.components[this._uid] = this;

      /**
       * 收集所有创建过的组件信息,此处只存储组件的基础信息,没销毁的组件会包含组件实例
       * 严禁对组件内其它对象进行引用,否则会导致组件实列无法被正常回收
       */
      const componentSummary = {
        uid: this._uid,
        name: this._componentName,
        tag: this._componentTag,
        createdTime: this._createdTime,
        // 0 表示还没被销毁
        destroyTime: 0,
        // 0 表示还没被销毁,duration可持续当当前查看时间
        duration: 0,
        component: this,
        chain
      };
      helper.componentsSummary[this._uid] = componentSummary;

      /* 添加到componentsSummaryStatistics里,生成统计信息 */
      Array.isArray(helper.componentsSummaryStatistics[this._componentName])
        ? helper.componentsSummaryStatistics[this._componentName].push(componentSummary)
        : (helper.componentsSummaryStatistics[this._componentName] = [componentSummary]);
    },
    created: function () {
      /* 增加空白数据,方便观察内存泄露情况 */
      if (helper.ddConfig.enabled) {
        let needDd = false;

        if (helper.ddConfig.filters.length === 0) {
          needDd = true;
        } else {
          for (let index = 0; index < helper.ddConfig.filters.length; index++) {
            const filter = helper.ddConfig.filters[index];
            if (filter === this._componentName || String(this._componentName).endsWith(filter)) {
              needDd = true;
              break
            }
          }
        }

        if (needDd) {
          const size = helper.ddConfig.size * 1024;
          const componentInfo = `tag: ${this._componentTag}, uid: ${this._uid}, createdTime: ${this._createdTime}`;
          /* 此处必须使用JSON.stringify对产生的字符串进行消费,否则没法将内存占用上去 */
          this.$data.__dd__ = componentInfo + ' ' + JSON.stringify(helper.methods.createEmptyData(size, 'd'));
          console.log(`[dd success] ${componentInfo} componentChain: ${this._componentChain}`);
        }
      }
    },
    destroyed: function () {
      if (this._componentTag) {
        const uid = this._uid;
        const name = this._componentName;
        const destroyTime = Date.now();

        /* helper里的componentSummary有可能通过调用clear函数而被清除掉,所以需进行判断再更新赋值 */
        const componentSummary = helper.componentsSummary[this._uid];
        if (componentSummary) {
          /* 补充/更新组件信息 */
          componentSummary.destroyTime = destroyTime;
          componentSummary.duration = destroyTime - this._createdTime;

          helper.destroyList.push(componentSummary);

          /* 统计被销毁的组件信息 */
          Array.isArray(helper.destroyStatistics[name])
            ? helper.destroyStatistics[name].push(componentSummary)
            : (helper.destroyStatistics[name] = [componentSummary]);

          /* 删除已销毁的组件实例 */
          delete componentSummary.component;
        }

        // 解除引用关系
        delete this._componentTag;
        delete this._componentChain;
        delete this._componentName;
        delete this._createdTime;
        delete helper.components[uid];
      } else {
        console.error('存在未被正常标记的组件,请检查组件采集逻辑是否需完善', this);
      }
    }
  });
}

/*!
 * @name      menuCommand.js
 * @version   0.0.1
 * @author    Blaze
 * @date      2019/9/21 14:22
 */

const monkeyMenu = {
  on (title, fn, accessKey) {
    return window.GM_registerMenuCommand && window.GM_registerMenuCommand(title, fn, accessKey)
  },
  off (id) {
    return window.GM_unregisterMenuCommand && window.GM_unregisterMenuCommand(id)
  },
  /* 切换类型的菜单功能 */
  switch (title, fn, defVal) {
    const t = this;
    t.on(title, fn);
  }
};

/**
 * 简单的i18n库
 */

class I18n {
  constructor (config) {
    this._languages = {};
    this._locale = this.getClientLang();
    this._defaultLanguage = '';
    this.init(config);
  }

  init (config) {
    if (!config) return false

    const t = this;
    t._locale = config.locale || t._locale;
    /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
    t._languages = config.languages || t._languages;
    t._defaultLanguage = config.defaultLanguage || t._defaultLanguage;
  }

  use () {}

  t (path) {
    const t = this;
    let result = t.getValByPath(t._languages[t._locale] || {}, path);

    /* 版本回退 */
    if (!result && t._locale !== t._defaultLanguage) {
      result = t.getValByPath(t._languages[t._defaultLanguage] || {}, path);
    }

    return result || ''
  }

  /* 当前语言值 */
  language () {
    return this._locale
  }

  languages () {
    return this._languages
  }

  changeLanguage (locale) {
    if (this._languages[locale]) {
      this._languages = locale;
      return locale
    } else {
      return false
    }
  }

  /**
   * 根据文本路径获取对象里面的值
   * @param obj {Object} -必选 要操作的对象
   * @param path {String} -必选 路径信息
   * @returns {*}
   */
  getValByPath (obj, path) {
    path = path || '';
    const pathArr = path.split('.');
    let result = obj;

    /* 递归提取结果值 */
    for (let i = 0; i < pathArr.length; i++) {
      if (!result) break
      result = result[pathArr[i]];
    }

    return result
  }

  /* 获取客户端当前的语言环境 */
  getClientLang () {
    return navigator.languages ? navigator.languages[0] : navigator.language
  }
}

var zhCN = {
  about: '关于',
  issues: '反馈',
  setting: '设置',
  hotkeys: '快捷键',
  donate: '赞赏',
};

var enUS = {
  about: 'about',
  issues: 'issues',
  setting: 'setting',
  hotkeys: 'hotkeys',
  donate: 'donate'
};

var zhTW = {
  about: '關於',
  issues: '反饋',
  setting: '設置',
  hotkeys: '快捷鍵',
  donate: '讚賞',
};

const messages = {
  'zh-CN': zhCN,
  zh: zhCN,
  'zh-HK': zhTW,
  'zh-TW': zhTW,
  'en-US': enUS,
  en: enUS,
};

/*!
 * @name         i18n.js
 * @description  vue-debug-helper的国际化配置
 * @version      0.0.1
 * @author       xxxily
 * @date         2022/04/26 14:56
 * @github       https://github.com/xxxily
 */

const i18n = new I18n({
  defaultLanguage: 'en',
  /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  // locale: 'zh-TW',
  languages: messages
});

/*!
 * @name         menu.js
 * @description  vue-debug-helper的菜单配置
 * @version      0.0.1
 * @author       xxxily
 * @date         2022/04/25 22:28
 * @github       https://github.com/xxxily
 */

function menuRegister () {
  monkeyMenu.on('查看vueDebugHelper对象', () => {
    debug.log('vueDebugHelper对象', helper);
  });

  monkeyMenu.on('当前存活组件统计', () => {
    debug.log('当前存活组件统计', helper.methods.componentsStatistics());
  });

  monkeyMenu.on('已销毁组件统计', () => {
    debug.log('已销毁组件统计', helper.methods.destroyStatisticsSort());
  });

  monkeyMenu.on('全部组件混合统计', () => {
    debug.log('全部组件混合统计', helper.methods.componentsSummaryStatisticsSort());
  });

  monkeyMenu.on('组件存活时间信息', () => {
    debug.log('组件存活时间信息', helper.methods.getDestroyByDuration());
  });

  monkeyMenu.on('清空统计信息', () => {
    helper.methods.clearAll();
    debug.log('清空统计信息');
  });

  monkeyMenu.on('数据注入(dd)', () => {
    const filter = window.prompt('组件过滤器(如果为空,则对所有组件注入)', '');
    const size = window.prompt('指定注入数据的大小值(默认1Mb)', 1024);
    debug.log('数据注入(dd)');
    helper.methods.dd(filter, Number(size));
  });

  monkeyMenu.on('取消数据注入(undd)', () => {
    debug.log('取消数据注入(undd)');
    helper.methods.undd();
  });

  // monkeyMenu.on('i18n.t('setting')', () => {
  //   window.alert('功能开发中,敬请期待...')
  // })

  monkeyMenu.on(i18n.t('issues'), () => {
    window.GM_openInTab('https://github.com/xxxily/vue-debug-helper/issues', {
      active: true,
      insert: true,
      setParent: true
    });
  });

  monkeyMenu.on(i18n.t('donate'), () => {
    window.GM_openInTab('https://cdn.jsdelivr.net/gh/xxxily/h5player@master/donate.png', {
      active: true,
      insert: true,
      setParent: true
    });
  });
}

const isff = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase().indexOf('firefox') > 0 : false;

// 绑定事件
function addEvent (object, event, method) {
  if (object.addEventListener) {
    object.addEventListener(event, method, false);
  } else if (object.attachEvent) {
    object.attachEvent(`on${event}`, () => { method(window.event); });
  }
}

// 修饰键转换成对应的键码
function getMods (modifier, key) {
  const mods = key.slice(0, key.length - 1);
  for (let i = 0; i < mods.length; i++) mods[i] = modifier[mods[i].toLowerCase()];
  return mods
}

// 处理传的key字符串转换成数组
function getKeys (key) {
  if (typeof key !== 'string') key = '';
  key = key.replace(/\s/g, ''); // 匹配任何空白字符,包括空格、制表符、换页符等等
  const keys = key.split(','); // 同时设置多个快捷键,以','分割
  let index = keys.lastIndexOf('');

  // 快捷键可能包含',',需特殊处理
  for (; index >= 0;) {
    keys[index - 1] += ',';
    keys.splice(index, 1);
    index = keys.lastIndexOf('');
  }

  return keys
}

// 比较修饰键的数组
function compareArray (a1, a2) {
  const arr1 = a1.length >= a2.length ? a1 : a2;
  const arr2 = a1.length >= a2.length ? a2 : a1;
  let isIndex = true;

  for (let i = 0; i < arr1.length; i++) {
    if (arr2.indexOf(arr1[i]) === -1) isIndex = false;
  }
  return isIndex
}

// Special Keys
const _keyMap = {
  backspace: 8,
  tab: 9,
  clear: 12,
  enter: 13,
  return: 13,
  esc: 27,
  escape: 27,
  space: 32,
  left: 37,
  up: 38,
  right: 39,
  down: 40,
  del: 46,
  delete: 46,
  ins: 45,
  insert: 45,
  home: 36,
  end: 35,
  pageup: 33,
  pagedown: 34,
  capslock: 20,
  num_0: 96,
  num_1: 97,
  num_2: 98,
  num_3: 99,
  num_4: 100,
  num_5: 101,
  num_6: 102,
  num_7: 103,
  num_8: 104,
  num_9: 105,
  num_multiply: 106,
  num_add: 107,
  num_enter: 108,
  num_subtract: 109,
  num_decimal: 110,
  num_divide: 111,
  '⇪': 20,
  ',': 188,
  '.': 190,
  '/': 191,
  '`': 192,
  '-': isff ? 173 : 189,
  '=': isff ? 61 : 187,
  ';': isff ? 59 : 186,
  '\'': 222,
  '[': 219,
  ']': 221,
  '\\': 220
};

// Modifier Keys
const _modifier = {
  // shiftKey
  '⇧': 16,
  shift: 16,
  // altKey
  '⌥': 18,
  alt: 18,
  option: 18,
  // ctrlKey
  '⌃': 17,
  ctrl: 17,
  control: 17,
  // metaKey
  '⌘': 91,
  cmd: 91,
  command: 91
};
const modifierMap = {
  16: 'shiftKey',
  18: 'altKey',
  17: 'ctrlKey',
  91: 'metaKey',

  shiftKey: 16,
  ctrlKey: 17,
  altKey: 18,
  metaKey: 91
};
const _mods = {
  16: false,
  18: false,
  17: false,
  91: false
};
const _handlers = {};

// F1~F12 special key
for (let k = 1; k < 20; k++) {
  _keyMap[`f${k}`] = 111 + k;
}

// https://github.com/jaywcjlove/hotkeys

let _downKeys = []; // 记录摁下的绑定键
let winListendFocus = false; // window是否已经监听了focus事件
let _scope = 'all'; // 默认热键范围
const elementHasBindEvent = []; // 已绑定事件的节点记录

// 返回键码
const code = (x) => _keyMap[x.toLowerCase()] ||
  _modifier[x.toLowerCase()] ||
  x.toUpperCase().charCodeAt(0);

// 设置获取当前范围(默认为'所有')
function setScope (scope) {
  _scope = scope || 'all';
}
// 获取当前范围
function getScope () {
  return _scope || 'all'
}
// 获取摁下绑定键的键值
function getPressedKeyCodes () {
  return _downKeys.slice(0)
}

// 表单控件控件判断 返回 Boolean
// hotkey is effective only when filter return true
function filter (event) {
  const target = event.target || event.srcElement;
  const { tagName } = target;
  let flag = true;
  // ignore: isContentEditable === 'true', <input> and <textarea> when readOnly state is false, <select>
  if (
    target.isContentEditable ||
    ((tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') && !target.readOnly)
  ) {
    flag = false;
  }
  return flag
}

// 判断摁下的键是否为某个键,返回true或者false
function isPressed (keyCode) {
  if (typeof keyCode === 'string') {
    keyCode = code(keyCode); // 转换成键码
  }
  return _downKeys.indexOf(keyCode) !== -1
}

// 循环删除handlers中的所有 scope(范围)
function deleteScope (scope, newScope) {
  let handlers;
  let i;

  // 没有指定scope,获取scope
  if (!scope) scope = getScope();

  for (const key in _handlers) {
    if (Object.prototype.hasOwnProperty.call(_handlers, key)) {
      handlers = _handlers[key];
      for (i = 0; i < handlers.length;) {
        if (handlers[i].scope === scope) handlers.splice(i, 1);
        else i++;
      }
    }
  }

  // 如果scope被删除,将scope重置为all
  if (getScope() === scope) setScope(newScope || 'all');
}

// 清除修饰键
function clearModifier (event) {
  let key = event.keyCode || event.which || event.charCode;
  const i = _downKeys.indexOf(key);

  // 从列表中清除按压过的键
  if (i >= 0) {
    _downKeys.splice(i, 1);
  }
  // 特殊处理 cmmand 键,在 cmmand 组合快捷键 keyup 只执行一次的问题
  if (event.key && event.key.toLowerCase() === 'meta') {
    _downKeys.splice(0, _downKeys.length);
  }

  // 修饰键 shiftKey altKey ctrlKey (command||metaKey) 清除
  if (key === 93 || key === 224) key = 91;
  if (key in _mods) {
    _mods[key] = false;

    // 将修饰键重置为false
    for (const k in _modifier) if (_modifier[k] === key) hotkeys[k] = false;
  }
}

function unbind (keysInfo, ...args) {
  // unbind(), unbind all keys
  if (!keysInfo) {
    Object.keys(_handlers).forEach((key) => delete _handlers[key]);
  } else if (Array.isArray(keysInfo)) {
    // support like : unbind([{key: 'ctrl+a', scope: 's1'}, {key: 'ctrl-a', scope: 's2', splitKey: '-'}])
    keysInfo.forEach((info) => {
      if (info.key) eachUnbind(info);
    });
  } else if (typeof keysInfo === 'object') {
    // support like unbind({key: 'ctrl+a, ctrl+b', scope:'abc'})
    if (keysInfo.key) eachUnbind(keysInfo);
  } else if (typeof keysInfo === 'string') {
    // support old method
    // eslint-disable-line
    let [scope, method] = args;
    if (typeof scope === 'function') {
      method = scope;
      scope = '';
    }
    eachUnbind({
      key: keysInfo,
      scope,
      method,
      splitKey: '+'
    });
  }
}

// 解除绑定某个范围的快捷键
const eachUnbind = ({
  key, scope, method, splitKey = '+'
}) => {
  const multipleKeys = getKeys(key);
  multipleKeys.forEach((originKey) => {
    const unbindKeys = originKey.split(splitKey);
    const len = unbindKeys.length;
    const lastKey = unbindKeys[len - 1];
    const keyCode = lastKey === '*' ? '*' : code(lastKey);
    if (!_handlers[keyCode]) return
    // 判断是否传入范围,没有就获取范围
    if (!scope) scope = getScope();
    const mods = len > 1 ? getMods(_modifier, unbindKeys) : [];
    _handlers[keyCode] = _handlers[keyCode].filter((record) => {
      // 通过函数判断,是否解除绑定,函数相等直接返回
      const isMatchingMethod = method ? record.method === method : true;
      return !(
        isMatchingMethod &&
        record.scope === scope &&
        compareArray(record.mods, mods)
      )
    });
  });
};

// 对监听对应快捷键的回调函数进行处理
function eventHandler (event, handler, scope, element) {
  if (handler.element !== element) {
    return
  }
  let modifiersMatch;

  // 看它是否在当前范围
  if (handler.scope === scope || handler.scope === 'all') {
    // 检查是否匹配修饰符(如果有返回true)
    modifiersMatch = handler.mods.length > 0;

    for (const y in _mods) {
      if (Object.prototype.hasOwnProperty.call(_mods, y)) {
        if (
          (!_mods[y] && handler.mods.indexOf(+y) > -1) ||
          (_mods[y] && handler.mods.indexOf(+y) === -1)
        ) {
          modifiersMatch = false;
        }
      }
    }

    // 调用处理程序,如果是修饰键不做处理
    if (
      (handler.mods.length === 0 &&
        !_mods[16] &&
        !_mods[18] &&
        !_mods[17] &&
        !_mods[91]) ||
      modifiersMatch ||
      handler.shortcut === '*'
    ) {
      if (handler.method(event, handler) === false) {
        if (event.preventDefault) event.preventDefault();
        else event.returnValue = false;
        if (event.stopPropagation) event.stopPropagation();
        if (event.cancelBubble) event.cancelBubble = true;
      }
    }
  }
}

// 处理keydown事件
function dispatch (event, element) {
  const asterisk = _handlers['*'];
  let key = event.keyCode || event.which || event.charCode;

  // 表单控件过滤 默认表单控件不触发快捷键
  if (!hotkeys.filter.call(this, event)) return

  // Gecko(Firefox)的command键值224,在Webkit(Chrome)中保持一致
  // Webkit左右 command 键值不一样
  if (key === 93 || key === 224) key = 91;

  /**
   * Collect bound keys
   * If an Input Method Editor is processing key input and the event is keydown, return 229.
   * https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229
   * http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
   */
  if (_downKeys.indexOf(key) === -1 && key !== 229) _downKeys.push(key);
  /**
   * Jest test cases are required.
   * ===============================
   */
  ['ctrlKey', 'altKey', 'shiftKey', 'metaKey'].forEach((keyName) => {
    const keyNum = modifierMap[keyName];
    if (event[keyName] && _downKeys.indexOf(keyNum) === -1) {
      _downKeys.push(keyNum);
    } else if (!event[keyName] && _downKeys.indexOf(keyNum) > -1) {
      _downKeys.splice(_downKeys.indexOf(keyNum), 1);
    } else if (keyName === 'metaKey' && event[keyName] && _downKeys.length === 3) {
      /**
       * Fix if Command is pressed:
       * ===============================
       */
      if (!(event.ctrlKey || event.shiftKey || event.altKey)) {
        _downKeys = _downKeys.slice(_downKeys.indexOf(keyNum));
      }
    }
  });
  /**
   * -------------------------------
   */

  if (key in _mods) {
    _mods[key] = true;

    // 将特殊字符的key注册到 hotkeys 上
    for (const k in _modifier) {
      if (_modifier[k] === key) hotkeys[k] = true;
    }

    if (!asterisk) return
  }

  // 将 modifierMap 里面的修饰键绑定到 event 中
  for (const e in _mods) {
    if (Object.prototype.hasOwnProperty.call(_mods, e)) {
      _mods[e] = event[modifierMap[e]];
    }
  }
  /**
   * https://github.com/jaywcjlove/hotkeys/pull/129
   * This solves the issue in Firefox on Windows where hotkeys corresponding to special characters would not trigger.
   * An example of this is ctrl+alt+m on a Swedish keyboard which is used to type μ.
   * Browser support: https://caniuse.com/#feat=keyboardevent-getmodifierstate
   */
  if (event.getModifierState && (!(event.altKey && !event.ctrlKey) && event.getModifierState('AltGraph'))) {
    if (_downKeys.indexOf(17) === -1) {
      _downKeys.push(17);
    }

    if (_downKeys.indexOf(18) === -1) {
      _downKeys.push(18);
    }

    _mods[17] = true;
    _mods[18] = true;
  }

  // 获取范围 默认为 `all`
  const scope = getScope();
  // 对任何快捷键都需要做的处理
  if (asterisk) {
    for (let i = 0; i < asterisk.length; i++) {
      if (
        asterisk[i].scope === scope &&
        ((event.type === 'keydown' && asterisk[i].keydown) ||
        (event.type === 'keyup' && asterisk[i].keyup))
      ) {
        eventHandler(event, asterisk[i], scope, element);
      }
    }
  }
  // key 不在 _handlers 中返回
  if (!(key in _handlers)) return

  for (let i = 0; i < _handlers[key].length; i++) {
    if (
      (event.type === 'keydown' && _handlers[key][i].keydown) ||
      (event.type === 'keyup' && _handlers[key][i].keyup)
    ) {
      if (_handlers[key][i].key) {
        const record = _handlers[key][i];
        const { splitKey } = record;
        const keyShortcut = record.key.split(splitKey);
        const _downKeysCurrent = []; // 记录当前按键键值
        for (let a = 0; a < keyShortcut.length; a++) {
          _downKeysCurrent.push(code(keyShortcut[a]));
        }
        if (_downKeysCurrent.sort().join('') === _downKeys.sort().join('')) {
          // 找到处理内容
          eventHandler(event, record, scope, element);
        }
      }
    }
  }
}

// 判断 element 是否已经绑定事件
function isElementBind (element) {
  return elementHasBindEvent.indexOf(element) > -1
}

function hotkeys (key, option, method) {
  _downKeys = [];
  const keys = getKeys(key); // 需要处理的快捷键列表
  let mods = [];
  let scope = 'all'; // scope默认为all,所有范围都有效
  let element = document; // 快捷键事件绑定节点
  let i = 0;
  let keyup = false;
  let keydown = true;
  let splitKey = '+';

  // 对为设定范围的判断
  if (method === undefined && typeof option === 'function') {
    method = option;
  }

  if (Object.prototype.toString.call(option) === '[object Object]') {
    if (option.scope) scope = option.scope; // eslint-disable-line
    if (option.element) element = option.element; // eslint-disable-line
    if (option.keyup) keyup = option.keyup; // eslint-disable-line
    if (option.keydown !== undefined) keydown = option.keydown; // eslint-disable-line
    if (typeof option.splitKey === 'string') splitKey = option.splitKey; // eslint-disable-line
  }

  if (typeof option === 'string') scope = option;

  // 对于每个快捷键进行处理
  for (; i < keys.length; i++) {
    key = keys[i].split(splitKey); // 按键列表
    mods = [];

    // 如果是组合快捷键取得组合快捷键
    if (key.length > 1) mods = getMods(_modifier, key);

    // 将非修饰键转化为键码
    key = key[key.length - 1];
    key = key === '*' ? '*' : code(key); // *表示匹配所有快捷键

    // 判断key是否在_handlers中,不在就赋一个空数组
    if (!(key in _handlers)) _handlers[key] = [];
    _handlers[key].push({
      keyup,
      keydown,
      scope,
      mods,
      shortcut: keys[i],
      method,
      key: keys[i],
      splitKey,
      element
    });
  }
  // 在全局document上设置快捷键
  if (typeof element !== 'undefined' && !isElementBind(element) && window) {
    elementHasBindEvent.push(element);
    addEvent(element, 'keydown', (e) => {
      dispatch(e, element);
    });
    if (!winListendFocus) {
      winListendFocus = true;
      addEvent(window, 'focus', () => {
        _downKeys = [];
      });
    }
    addEvent(element, 'keyup', (e) => {
      dispatch(e, element);
      clearModifier(e);
    });
  }
}

function trigger (shortcut, scope = 'all') {
  Object.keys(_handlers).forEach((key) => {
    const data = _handlers[key].find((item) => item.scope === scope && item.shortcut === shortcut);
    if (data && data.method) {
      data.method();
    }
  });
}

const _api = {
  setScope,
  getScope,
  deleteScope,
  getPressedKeyCodes,
  isPressed,
  filter,
  trigger,
  unbind,
  keyMap: _keyMap,
  modifier: _modifier,
  modifierMap
};
for (const a in _api) {
  if (Object.prototype.hasOwnProperty.call(_api, a)) {
    hotkeys[a] = _api[a];
  }
}

if (typeof window !== 'undefined') {
  const _hotkeys = window.hotkeys;
  hotkeys.noConflict = (deep) => {
    if (deep && window.hotkeys === hotkeys) {
      window.hotkeys = _hotkeys;
    }
    return hotkeys
  };
  window.hotkeys = hotkeys;
}

/*!
 * @name         hotKeyRegister.js
 * @description  vue-debug-helper的快捷键配置
 * @version      0.0.1
 * @author       xxxily
 * @date         2022/04/26 14:37
 * @github       https://github.com/xxxily
 */

function hotKeyRegister () {
  const hotKeyMap = {
    'shift+alt+a,shift+alt+ctrl+a': function (event, handler) {
      debug.log('全部组件混合统计', helper.methods.componentsSummaryStatisticsSort());
    },
    'shift+alt+l': function (event, handler) {
      debug.log('当前存活组件统计', helper.methods.componentsStatistics());
    },
    'shift+alt+d': function (event, handler) {
      debug.log('已销毁组件统计', helper.methods.destroyStatisticsSort());
    },
    'shift+alt+c': function (event, handler) {
      helper.methods.clearAll();
      debug.log('清空统计信息');
    },
    'shift+alt+e': function (event, handler) {
      if (helper.ddConfig.enable) {
        debug.log('取消数据注入(undd)');
        helper.methods.undd();
      } else {
        const filter = window.prompt('组件过滤器(如果为空,则对所有组件注入)', '');
        const size = window.prompt('指定注入数据的大小值(默认1Mb)', 1024);
        debug.log('数据注入(dd)');
        helper.methods.dd(filter, Number(size));
      }
    }
  };

  Object.keys(hotKeyMap).forEach(key => {
    hotkeys(key, hotKeyMap[key]);
  });
}

/*!
 * @name         vueDetector.js
 * @description  检测页面是否存在Vue对象
 * @version      0.0.1
 * @author       xxxily
 * @date         2022/04/27 11:43
 * @github       https://github.com/xxxily
 */

/**
 * 检测页面是否存在Vue对象,方法参考:https://github.com/vuejs/devtools/blob/main/packages/shell-chrome/src/detector.js
 * @param {window} win windwod对象
 * @param {function} callback 检测到Vue对象后的回调函数
 */
function vueDetect (win, callback) {
  let delay = 1000;
  let detectRemainingTries = 10;

  function runDetect () {
    // Method 1: use defineProperty to detect Vue, has BUG, so use Method 2
    // 使用下面方式会导致 'Vue' in window 为 true,从而引发其他问题
    // Object.defineProperty(win, 'Vue', {
    //   enumerable: true,
    //   configurable: true,
    //   get () {
    //     return win.__originalVue__
    //   },
    //   set (value) {
    //     win.__originalVue__ = value

    //     if (value && value.mixin) {
    //       callback(value)
    //     }
    //   }
    // })

    // Method 2: Check  Vue 3
    const vueDetected = !!(window.__VUE__);
    if (vueDetected) {
      callback(window.__VUE__);
      return
    }

    // Method 3: Scan all elements inside document
    const all = document.querySelectorAll('*');
    let el;
    for (let i = 0; i < all.length; i++) {
      if (all[i].__vue__) {
        el = all[i];
        break
      }
    }
    if (el) {
      let Vue = Object.getPrototypeOf(el.__vue__).constructor;
      while (Vue.super) {
        Vue = Vue.super;
      }
      callback(Vue);
      return
    }

    if (detectRemainingTries > 0) {
      detectRemainingTries--;
      setTimeout(() => {
        runDetect();
      }, delay);
      delay *= 5;
    }
  }

  setTimeout(() => {
    runDetect();
  }, 100);
}

/**
 * 判断是否处于Iframe中
 * @returns {boolean}
 */
function isInIframe () {
  return window !== window.top
}

/**
 * 由于tampermonkey对window对象进行了封装,我们实际访问到的window并非页面真实的window
 * 这就导致了如果我们需要将某些对象挂载到页面的window进行调试的时候就无法挂载了
 * 所以必须使用特殊手段才能访问到页面真实的window对象,于是就有了下面这个函数
 * @returns {Promise<void>}
 */
async function getPageWindow () {
  return new Promise(function (resolve, reject) {
    if (window._pageWindow) {
      return resolve(window._pageWindow)
    }

    const listenEventList = ['load', 'mousemove', 'scroll', 'get-page-window-event'];

    function getWin (event) {
      window._pageWindow = this;
      // debug.log('getPageWindow succeed', event)
      listenEventList.forEach(eventType => {
        window.removeEventListener(eventType, getWin, true);
      });
      resolve(window._pageWindow);
    }

    listenEventList.forEach(eventType => {
      window.addEventListener(eventType, getWin, true);
    });

    /* 自行派发事件以便用最短的时候获得pageWindow对象 */
    window.dispatchEvent(new window.Event('get-page-window-event'));
  })
}

let registerStatus = 'init';
window._debugMode_ = true

;(async function () {
  if (isInIframe()) {
    debug.log('running in iframe, skip init', window.location.href);
    return false
  }

  debug.log('init');

  const win = await getPageWindow();
  vueDetect(win, function (Vue) {
    mixinRegister(Vue);
    menuRegister();
    hotKeyRegister();

    debug.log('vue debug helper register success');
    registerStatus = 'success';
  });

  setTimeout(() => {
    if (registerStatus !== 'success') {
      debug.warn('vue debug helper register failed, please check if vue is loaded .', win.location.href);
    }
  }, 1000 * 10);
})();