vue-debug-helper

Vue components debug helper

当前为 2022-04-27 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name vue-debug-helper
  3. // @name:en vue-debug-helper
  4. // @name:zh Vue调试分析助手
  5. // @name:zh-TW Vue調試分析助手
  6. // @name:ja Vueデバッグ分析アシスタント
  7. // @namespace https://github.com/xxxily/vue-debug-helper
  8. // @homepage https://github.com/xxxily/vue-debug-helper
  9. // @version 0.0.2
  10. // @description Vue components debug helper
  11. // @description:en Vue components debug helper
  12. // @description:zh Vue组件探测、统计、分析辅助脚本
  13. // @description:zh-TW Vue組件探測、統計、分析輔助腳本
  14. // @description:ja Vueコンポーネントの検出、統計、分析補助スクリプト
  15. // @author ankvps
  16. // @icon https://cdn.jsdelivr.net/gh/xxxily/vue-debug-helper@main/logo.png
  17. // @match http://*/*
  18. // @match https://*/*
  19. // @grant unsafeWindow
  20. // @grant GM_addStyle
  21. // @grant GM_setValue
  22. // @grant GM_getValue
  23. // @grant GM_deleteValue
  24. // @grant GM_listValues
  25. // @grant GM_addValueChangeListener
  26. // @grant GM_removeValueChangeListener
  27. // @grant GM_registerMenuCommand
  28. // @grant GM_unregisterMenuCommand
  29. // @grant GM_getTab
  30. // @grant GM_saveTab
  31. // @grant GM_getTabs
  32. // @grant GM_openInTab
  33. // @grant GM_download
  34. // @grant GM_xmlhttpRequest
  35. // @run-at document-start
  36. // @connect 127.0.0.1
  37. // @license GPL
  38. // ==/UserScript==
  39. (function (w) { if (w) { w._vueDebugHelper_ = 'https://github.com/xxxily/vue-debug-helper'; } })();
  40.  
  41. class Debug {
  42. constructor (msg) {
  43. const t = this;
  44. msg = msg || 'debug message:';
  45. t.log = t.createDebugMethod('log', null, msg);
  46. t.error = t.createDebugMethod('error', null, msg);
  47. t.info = t.createDebugMethod('info', null, msg);
  48. t.warn = t.createDebugMethod('warn', null, msg);
  49. }
  50.  
  51. create (msg) {
  52. return new Debug(msg)
  53. }
  54.  
  55. createDebugMethod (name, color, tipsMsg) {
  56. name = name || 'info';
  57.  
  58. const bgColorMap = {
  59. info: '#2274A5',
  60. log: '#95B46A',
  61. error: '#D33F49'
  62. };
  63.  
  64. return function () {
  65. if (!window._debugMode_) {
  66. return false
  67. }
  68.  
  69. const curTime = new Date();
  70. const H = curTime.getHours();
  71. const M = curTime.getMinutes();
  72. const S = curTime.getSeconds();
  73. const msg = tipsMsg || 'debug message:';
  74.  
  75. const arg = Array.from(arguments);
  76. arg.unshift(`color: white; background-color: ${color || bgColorMap[name] || '#95B46A'}`);
  77. arg.unshift(`%c [${H}:${M}:${S}] ${msg} `);
  78. window.console[name].apply(window.console, arg);
  79. }
  80. }
  81.  
  82. isDebugMode () {
  83. return Boolean(window._debugMode_)
  84. }
  85. }
  86.  
  87. var Debug$1 = new Debug();
  88.  
  89. var debug = Debug$1.create('vue-debug-helper message:');
  90.  
  91. /**
  92. * 对特定数据结构的对象进行排序
  93. * @param {object} obj 一个对象,其结构应该类似于:{key1: [], key2: []}
  94. * @param {boolean} reverse -可选 是否反转、降序排列,默认为false
  95. * @param {object} opts -可选 指定数组的配置项,默认为{key: 'key', value: 'value'}
  96. * @param {object} opts.key -可选 指定对象键名的别名,默认为'key'
  97. * @param {object} opts.value -可选 指定对象值的别名,默认为'value'
  98. * @returns {array} 返回一个数组,其结构应该类似于:[{key: key1, value: []}, {key: key2, value: []}]
  99. */
  100. const objSort = (obj, reverse, opts = { key: 'key', value: 'value' }) => {
  101. const arr = [];
  102. for (const key in obj) {
  103. if (Object.prototype.hasOwnProperty.call(obj, key) && Array.isArray(obj[key])) {
  104. const tmpObj = {};
  105. tmpObj[opts.key] = key;
  106. tmpObj[opts.value] = obj[key];
  107. arr.push(tmpObj);
  108. }
  109. }
  110.  
  111. arr.sort((a, b) => {
  112. return a[opts.value].length - b[opts.value].length
  113. });
  114.  
  115. reverse && arr.reverse();
  116. return arr
  117. };
  118.  
  119. /**
  120. * 根据指定长度创建空白数据
  121. * @param {number} size -可选 指定数据长度,默认为1024
  122. * @param {string} str - 可选 指定数据的字符串,默认为'd'
  123. */
  124. function createEmptyData (size = 1024, str = 'd') {
  125. const arr = [];
  126. arr.length = size + 1;
  127. return arr.join(str)
  128. }
  129.  
  130. window.vueDebugHelper = {
  131. /* 存储全部未被销毁的组件对象 */
  132. components: {},
  133. /* 存储全部创建过的组件的概要信息,即使销毁了概要信息依然存在 */
  134. componentsSummary: {},
  135. /* 基于componentsSummary的组件情况统计 */
  136. componentsSummaryStatistics: {},
  137. /* 已销毁的组件概要信息列表 */
  138. destroyList: [],
  139. /* 基于destroyList的组件情况统计 */
  140. destroyStatistics: {},
  141. /* 给组件注入空白数据的配置信息 */
  142. ddConfig: {
  143. enabled: false,
  144. filters: [],
  145. size: 1024
  146. }
  147. };
  148.  
  149. const helper = window.vueDebugHelper;
  150.  
  151. const methods = {
  152. objSort,
  153. createEmptyData,
  154. /* 清除全部helper的全部记录数据,以便重新统计 */
  155. clearAll () {
  156. helper.components = {};
  157. helper.componentsSummary = {};
  158. helper.componentsSummaryStatistics = {};
  159. helper.destroyList = [];
  160. helper.destroyStatistics = {};
  161. },
  162.  
  163. /**
  164. * 对当前的helper.components进行统计与排序
  165. * 如果一直没运行过清理函数,则表示统计页面创建至今依然存活的组件对象
  166. * 运行过清理函数,则表示统计清理后新创建且至今依然存活的组件对象
  167. */
  168. componentsStatistics (reverse = true) {
  169. const tmpObj = {};
  170.  
  171. Object.keys(helper.components).forEach(key => {
  172. const component = helper.components[key];
  173.  
  174. tmpObj[component._componentName]
  175. ? tmpObj[component._componentName].push(component)
  176. : (tmpObj[component._componentName] = [component]);
  177. });
  178.  
  179. return objSort(tmpObj, reverse, {
  180. key: 'componentName',
  181. value: 'componentInstance'
  182. })
  183. },
  184.  
  185. /**
  186. * 对componentsSummaryStatistics进行排序输出,以便可以直观查看组件的创建情况
  187. */
  188. componentsSummaryStatisticsSort (reverse = true) {
  189. return objSort(helper.componentsSummaryStatistics, reverse, {
  190. key: 'componentName',
  191. value: 'componentsSummary'
  192. })
  193. },
  194.  
  195. /**
  196. * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况
  197. */
  198. destroyStatisticsSort (reverse = true) {
  199. return objSort(helper.destroyStatistics, reverse, {
  200. key: 'componentName',
  201. value: 'destroyList'
  202. })
  203. },
  204.  
  205. /**
  206. * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况
  207. */
  208. getDestroyByDuration (duration = 1000) {
  209. const destroyList = helper.destroyList;
  210. const destroyListLength = destroyList.length;
  211. const destroyListDuration = destroyList.map(item => item.duration).sort();
  212. const maxDuration = Math.max(...destroyListDuration);
  213. const minDuration = Math.min(...destroyListDuration);
  214. const avgDuration =
  215. destroyListDuration.reduce((a, b) => a + b, 0) / destroyListLength;
  216. const durationRange = maxDuration - minDuration;
  217. const durationRangePercent = (duration - minDuration) / durationRange;
  218.  
  219. return {
  220. destroyList,
  221. destroyListLength,
  222. destroyListDuration,
  223. maxDuration,
  224. minDuration,
  225. avgDuration,
  226. durationRange,
  227. durationRangePercent
  228. }
  229. },
  230.  
  231. /**
  232. * 获取组件的调用链信息
  233. */
  234. getComponentChain (component, moreDetail = false) {
  235. const result = [];
  236. let current = component;
  237. let deep = 0;
  238.  
  239. while (current && deep < 50) {
  240. deep++;
  241.  
  242. if (moreDetail) {
  243. result.push({
  244. name: current._componentName,
  245. componentsSummary: helper.componentsSummary[current._uid] || null
  246. });
  247. } else {
  248. result.push(current._componentName);
  249. }
  250.  
  251. current = current.$parent;
  252. }
  253.  
  254. if (moreDetail) {
  255. return result
  256. } else {
  257. return result.join(' -> ')
  258. }
  259. },
  260.  
  261. /**
  262. * 给指定组件注入大量空数据,以便观察组件的内存泄露情况
  263. * @param {Array|string} filter -必选 指定组件的名称,如果为空则表示注入所有组件
  264. * @param {number} size -可选 指定注入空数据的大小,单位Kb,默认为1024Kb,即1Mb
  265. * @returns
  266. */
  267. dd (filter, size = 1024) {
  268. filter = filter || [];
  269.  
  270. /* 如果是字符串,则支持通过, | 两个符号来指定多个组件名称的过滤器 */
  271. if (typeof filter === 'string') {
  272. /* 移除前后的, |分隔符,防止出现空字符的过滤规则 */
  273. filter.replace(/^(,|\|)/, '').replace(/(,|\|)$/, '');
  274.  
  275. if (/\|/.test(filter)) {
  276. filter = filter.split('|');
  277. } else {
  278. filter = filter.split(',');
  279. }
  280. }
  281.  
  282. helper.ddConfig = {
  283. enabled: true,
  284. filters: filter,
  285. size
  286. };
  287. },
  288. /* 禁止给组件注入空数据 */
  289. undd () {
  290. helper.ddConfig = {
  291. enabled: false,
  292. filters: [],
  293. size: 1024
  294. };
  295. }
  296. };
  297.  
  298. helper.methods = methods;
  299.  
  300. function mixinRegister (Vue) {
  301. if (!Vue || !Vue.mixin) {
  302. debug.error('未检查到VUE对象,请检查是否引入了VUE,且将VUE对象挂载到全局变量window.Vue上');
  303. return false
  304. }
  305.  
  306. Vue.mixin({
  307. beforeCreate: function () {
  308. const tag = this.$options?._componentTag || this.$vnode?.tag || this._uid;
  309. const chain = helper.methods.getComponentChain(this);
  310. this._componentTag = tag;
  311. this._componentChain = chain;
  312. this._componentName = isNaN(Number(tag)) ? tag.replace(/^vue\-component\-\d+\-/, '') : 'anonymous-component';
  313. this._createdTime = Date.now();
  314.  
  315. /* 判断是否为函数式组件,函数式组件无状态 (没有响应式数据),也没有实例,也没生命周期概念 */
  316. if (this._componentName === 'anonymous-component' && !this.$parent && !this.$vnode) {
  317. this._componentName = 'functional-component';
  318. }
  319.  
  320. helper.components[this._uid] = this;
  321.  
  322. /**
  323. * 收集所有创建过的组件信息,此处只存储组件的基础信息,没销毁的组件会包含组件实例
  324. * 严禁对组件内其它对象进行引用,否则会导致组件实列无法被正常回收
  325. */
  326. const componentSummary = {
  327. uid: this._uid,
  328. name: this._componentName,
  329. tag: this._componentTag,
  330. createdTime: this._createdTime,
  331. // 0 表示还没被销毁
  332. destroyTime: 0,
  333. // 0 表示还没被销毁,duration可持续当当前查看时间
  334. duration: 0,
  335. component: this,
  336. chain
  337. };
  338. helper.componentsSummary[this._uid] = componentSummary;
  339.  
  340. /* 添加到componentsSummaryStatistics里,生成统计信息 */
  341. Array.isArray(helper.componentsSummaryStatistics[this._componentName])
  342. ? helper.componentsSummaryStatistics[this._componentName].push(componentSummary)
  343. : (helper.componentsSummaryStatistics[this._componentName] = [componentSummary]);
  344. },
  345. created: function () {
  346. /* 增加空白数据,方便观察内存泄露情况 */
  347. if (helper.ddConfig.enabled) {
  348. let needDd = false;
  349.  
  350. if (helper.ddConfig.filters.length === 0) {
  351. needDd = true;
  352. } else {
  353. for (let index = 0; index < helper.ddConfig.filters.length; index++) {
  354. const filter = helper.ddConfig.filters[index];
  355. if (filter === this._componentName || String(this._componentName).endsWith(filter)) {
  356. needDd = true;
  357. break
  358. }
  359. }
  360. }
  361.  
  362. if (needDd) {
  363. const size = helper.ddConfig.size * 1024;
  364. const componentInfo = `tag: ${this._componentTag}, uid: ${this._uid}, createdTime: ${this._createdTime}`;
  365. /* 此处必须使用JSON.stringify对产生的字符串进行消费,否则没法将内存占用上去 */
  366. this.$data.__dd__ = componentInfo + ' ' + JSON.stringify(helper.methods.createEmptyData(size, 'd'));
  367. console.log(`[dd success] ${componentInfo} componentChain: ${this._componentChain}`);
  368. }
  369. }
  370. },
  371. destroyed: function () {
  372. if (this._componentTag) {
  373. const uid = this._uid;
  374. const name = this._componentName;
  375. const destroyTime = Date.now();
  376.  
  377. /* helper里的componentSummary有可能通过调用clear函数而被清除掉,所以需进行判断再更新赋值 */
  378. const componentSummary = helper.componentsSummary[this._uid];
  379. if (componentSummary) {
  380. /* 补充/更新组件信息 */
  381. componentSummary.destroyTime = destroyTime;
  382. componentSummary.duration = destroyTime - this._createdTime;
  383.  
  384. helper.destroyList.push(componentSummary);
  385.  
  386. /* 统计被销毁的组件信息 */
  387. Array.isArray(helper.destroyStatistics[name])
  388. ? helper.destroyStatistics[name].push(componentSummary)
  389. : (helper.destroyStatistics[name] = [componentSummary]);
  390.  
  391. /* 删除已销毁的组件实例 */
  392. delete componentSummary.component;
  393. }
  394.  
  395. // 解除引用关系
  396. delete this._componentTag;
  397. delete this._componentChain;
  398. delete this._componentName;
  399. delete this._createdTime;
  400. delete helper.components[uid];
  401. } else {
  402. console.error('存在未被正常标记的组件,请检查组件采集逻辑是否需完善', this);
  403. }
  404. }
  405. });
  406. }
  407.  
  408. /*!
  409. * @name menuCommand.js
  410. * @version 0.0.1
  411. * @author Blaze
  412. * @date 2019/9/21 14:22
  413. */
  414.  
  415. const monkeyMenu = {
  416. on (title, fn, accessKey) {
  417. return window.GM_registerMenuCommand && window.GM_registerMenuCommand(title, fn, accessKey)
  418. },
  419. off (id) {
  420. return window.GM_unregisterMenuCommand && window.GM_unregisterMenuCommand(id)
  421. },
  422. /* 切换类型的菜单功能 */
  423. switch (title, fn, defVal) {
  424. const t = this;
  425. t.on(title, fn);
  426. }
  427. };
  428.  
  429. /**
  430. * 简单的i18n库
  431. */
  432.  
  433. class I18n {
  434. constructor (config) {
  435. this._languages = {};
  436. this._locale = this.getClientLang();
  437. this._defaultLanguage = '';
  438. this.init(config);
  439. }
  440.  
  441. init (config) {
  442. if (!config) return false
  443.  
  444. const t = this;
  445. t._locale = config.locale || t._locale;
  446. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  447. t._languages = config.languages || t._languages;
  448. t._defaultLanguage = config.defaultLanguage || t._defaultLanguage;
  449. }
  450.  
  451. use () {}
  452.  
  453. t (path) {
  454. const t = this;
  455. let result = t.getValByPath(t._languages[t._locale] || {}, path);
  456.  
  457. /* 版本回退 */
  458. if (!result && t._locale !== t._defaultLanguage) {
  459. result = t.getValByPath(t._languages[t._defaultLanguage] || {}, path);
  460. }
  461.  
  462. return result || ''
  463. }
  464.  
  465. /* 当前语言值 */
  466. language () {
  467. return this._locale
  468. }
  469.  
  470. languages () {
  471. return this._languages
  472. }
  473.  
  474. changeLanguage (locale) {
  475. if (this._languages[locale]) {
  476. this._languages = locale;
  477. return locale
  478. } else {
  479. return false
  480. }
  481. }
  482.  
  483. /**
  484. * 根据文本路径获取对象里面的值
  485. * @param obj {Object} -必选 要操作的对象
  486. * @param path {String} -必选 路径信息
  487. * @returns {*}
  488. */
  489. getValByPath (obj, path) {
  490. path = path || '';
  491. const pathArr = path.split('.');
  492. let result = obj;
  493.  
  494. /* 递归提取结果值 */
  495. for (let i = 0; i < pathArr.length; i++) {
  496. if (!result) break
  497. result = result[pathArr[i]];
  498. }
  499.  
  500. return result
  501. }
  502.  
  503. /* 获取客户端当前的语言环境 */
  504. getClientLang () {
  505. return navigator.languages ? navigator.languages[0] : navigator.language
  506. }
  507. }
  508.  
  509. var zhCN = {
  510. about: '关于',
  511. issues: '反馈',
  512. setting: '设置',
  513. hotkeys: '快捷键',
  514. donate: '赞赏',
  515. };
  516.  
  517. var enUS = {
  518. about: 'about',
  519. issues: 'issues',
  520. setting: 'setting',
  521. hotkeys: 'hotkeys',
  522. donate: 'donate'
  523. };
  524.  
  525. var zhTW = {
  526. about: '關於',
  527. issues: '反饋',
  528. setting: '設置',
  529. hotkeys: '快捷鍵',
  530. donate: '讚賞',
  531. };
  532.  
  533. const messages = {
  534. 'zh-CN': zhCN,
  535. zh: zhCN,
  536. 'zh-HK': zhTW,
  537. 'zh-TW': zhTW,
  538. 'en-US': enUS,
  539. en: enUS,
  540. };
  541.  
  542. /*!
  543. * @name i18n.js
  544. * @description vue-debug-helper的国际化配置
  545. * @version 0.0.1
  546. * @author xxxily
  547. * @date 2022/04/26 14:56
  548. * @github https://github.com/xxxily
  549. */
  550.  
  551. const i18n = new I18n({
  552. defaultLanguage: 'en',
  553. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  554. // locale: 'zh-TW',
  555. languages: messages
  556. });
  557.  
  558. /*!
  559. * @name menu.js
  560. * @description vue-debug-helper的菜单配置
  561. * @version 0.0.1
  562. * @author xxxily
  563. * @date 2022/04/25 22:28
  564. * @github https://github.com/xxxily
  565. */
  566.  
  567. function menuRegister () {
  568. monkeyMenu.on('查看vueDebugHelper对象', () => {
  569. debug.log('vueDebugHelper对象', helper);
  570. });
  571.  
  572. monkeyMenu.on('当前存活组件统计', () => {
  573. debug.log('当前存活组件统计', helper.methods.componentsStatistics());
  574. });
  575.  
  576. monkeyMenu.on('已销毁组件统计', () => {
  577. debug.log('已销毁组件统计', helper.methods.destroyStatisticsSort());
  578. });
  579.  
  580. monkeyMenu.on('全部组件混合统计', () => {
  581. debug.log('全部组件混合统计', helper.methods.componentsSummaryStatisticsSort());
  582. });
  583.  
  584. monkeyMenu.on('组件存活时间信息', () => {
  585. debug.log('组件存活时间信息', helper.methods.getDestroyByDuration());
  586. });
  587.  
  588. monkeyMenu.on('清空统计信息', () => {
  589. helper.methods.clearAll();
  590. debug.log('清空统计信息');
  591. });
  592.  
  593. monkeyMenu.on('数据注入(dd)', () => {
  594. const filter = window.prompt('组件过滤器(如果为空,则对所有组件注入)', '');
  595. const size = window.prompt('指定注入数据的大小值(默认1Mb)', 1024);
  596. debug.log('数据注入(dd)');
  597. helper.methods.dd(filter, Number(size));
  598. });
  599.  
  600. monkeyMenu.on('取消数据注入(undd)', () => {
  601. debug.log('取消数据注入(undd)');
  602. helper.methods.undd();
  603. });
  604.  
  605. // monkeyMenu.on('i18n.t('setting')', () => {
  606. // window.alert('功能开发中,敬请期待...')
  607. // })
  608.  
  609. monkeyMenu.on(i18n.t('issues'), () => {
  610. window.GM_openInTab('https://github.com/xxxily/vue-debug-helper/issues', {
  611. active: true,
  612. insert: true,
  613. setParent: true
  614. });
  615. });
  616.  
  617. monkeyMenu.on(i18n.t('donate'), () => {
  618. window.GM_openInTab('https://cdn.jsdelivr.net/gh/xxxily/h5player@master/donate.png', {
  619. active: true,
  620. insert: true,
  621. setParent: true
  622. });
  623. });
  624. }
  625.  
  626. const isff = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase().indexOf('firefox') > 0 : false;
  627.  
  628. // 绑定事件
  629. function addEvent (object, event, method) {
  630. if (object.addEventListener) {
  631. object.addEventListener(event, method, false);
  632. } else if (object.attachEvent) {
  633. object.attachEvent(`on${event}`, () => { method(window.event); });
  634. }
  635. }
  636.  
  637. // 修饰键转换成对应的键码
  638. function getMods (modifier, key) {
  639. const mods = key.slice(0, key.length - 1);
  640. for (let i = 0; i < mods.length; i++) mods[i] = modifier[mods[i].toLowerCase()];
  641. return mods
  642. }
  643.  
  644. // 处理传的key字符串转换成数组
  645. function getKeys (key) {
  646. if (typeof key !== 'string') key = '';
  647. key = key.replace(/\s/g, ''); // 匹配任何空白字符,包括空格、制表符、换页符等等
  648. const keys = key.split(','); // 同时设置多个快捷键,以','分割
  649. let index = keys.lastIndexOf('');
  650.  
  651. // 快捷键可能包含',',需特殊处理
  652. for (; index >= 0;) {
  653. keys[index - 1] += ',';
  654. keys.splice(index, 1);
  655. index = keys.lastIndexOf('');
  656. }
  657.  
  658. return keys
  659. }
  660.  
  661. // 比较修饰键的数组
  662. function compareArray (a1, a2) {
  663. const arr1 = a1.length >= a2.length ? a1 : a2;
  664. const arr2 = a1.length >= a2.length ? a2 : a1;
  665. let isIndex = true;
  666.  
  667. for (let i = 0; i < arr1.length; i++) {
  668. if (arr2.indexOf(arr1[i]) === -1) isIndex = false;
  669. }
  670. return isIndex
  671. }
  672.  
  673. // Special Keys
  674. const _keyMap = {
  675. backspace: 8,
  676. tab: 9,
  677. clear: 12,
  678. enter: 13,
  679. return: 13,
  680. esc: 27,
  681. escape: 27,
  682. space: 32,
  683. left: 37,
  684. up: 38,
  685. right: 39,
  686. down: 40,
  687. del: 46,
  688. delete: 46,
  689. ins: 45,
  690. insert: 45,
  691. home: 36,
  692. end: 35,
  693. pageup: 33,
  694. pagedown: 34,
  695. capslock: 20,
  696. num_0: 96,
  697. num_1: 97,
  698. num_2: 98,
  699. num_3: 99,
  700. num_4: 100,
  701. num_5: 101,
  702. num_6: 102,
  703. num_7: 103,
  704. num_8: 104,
  705. num_9: 105,
  706. num_multiply: 106,
  707. num_add: 107,
  708. num_enter: 108,
  709. num_subtract: 109,
  710. num_decimal: 110,
  711. num_divide: 111,
  712. '⇪': 20,
  713. ',': 188,
  714. '.': 190,
  715. '/': 191,
  716. '`': 192,
  717. '-': isff ? 173 : 189,
  718. '=': isff ? 61 : 187,
  719. ';': isff ? 59 : 186,
  720. '\'': 222,
  721. '[': 219,
  722. ']': 221,
  723. '\\': 220
  724. };
  725.  
  726. // Modifier Keys
  727. const _modifier = {
  728. // shiftKey
  729. '⇧': 16,
  730. shift: 16,
  731. // altKey
  732. '⌥': 18,
  733. alt: 18,
  734. option: 18,
  735. // ctrlKey
  736. '⌃': 17,
  737. ctrl: 17,
  738. control: 17,
  739. // metaKey
  740. '⌘': 91,
  741. cmd: 91,
  742. command: 91
  743. };
  744. const modifierMap = {
  745. 16: 'shiftKey',
  746. 18: 'altKey',
  747. 17: 'ctrlKey',
  748. 91: 'metaKey',
  749.  
  750. shiftKey: 16,
  751. ctrlKey: 17,
  752. altKey: 18,
  753. metaKey: 91
  754. };
  755. const _mods = {
  756. 16: false,
  757. 18: false,
  758. 17: false,
  759. 91: false
  760. };
  761. const _handlers = {};
  762.  
  763. // F1~F12 special key
  764. for (let k = 1; k < 20; k++) {
  765. _keyMap[`f${k}`] = 111 + k;
  766. }
  767.  
  768. // https://github.com/jaywcjlove/hotkeys
  769.  
  770. let _downKeys = []; // 记录摁下的绑定键
  771. let winListendFocus = false; // window是否已经监听了focus事件
  772. let _scope = 'all'; // 默认热键范围
  773. const elementHasBindEvent = []; // 已绑定事件的节点记录
  774.  
  775. // 返回键码
  776. const code = (x) => _keyMap[x.toLowerCase()] ||
  777. _modifier[x.toLowerCase()] ||
  778. x.toUpperCase().charCodeAt(0);
  779.  
  780. // 设置获取当前范围(默认为'所有')
  781. function setScope (scope) {
  782. _scope = scope || 'all';
  783. }
  784. // 获取当前范围
  785. function getScope () {
  786. return _scope || 'all'
  787. }
  788. // 获取摁下绑定键的键值
  789. function getPressedKeyCodes () {
  790. return _downKeys.slice(0)
  791. }
  792.  
  793. // 表单控件控件判断 返回 Boolean
  794. // hotkey is effective only when filter return true
  795. function filter (event) {
  796. const target = event.target || event.srcElement;
  797. const { tagName } = target;
  798. let flag = true;
  799. // ignore: isContentEditable === 'true', <input> and <textarea> when readOnly state is false, <select>
  800. if (
  801. target.isContentEditable ||
  802. ((tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') && !target.readOnly)
  803. ) {
  804. flag = false;
  805. }
  806. return flag
  807. }
  808.  
  809. // 判断摁下的键是否为某个键,返回true或者false
  810. function isPressed (keyCode) {
  811. if (typeof keyCode === 'string') {
  812. keyCode = code(keyCode); // 转换成键码
  813. }
  814. return _downKeys.indexOf(keyCode) !== -1
  815. }
  816.  
  817. // 循环删除handlers中的所有 scope(范围)
  818. function deleteScope (scope, newScope) {
  819. let handlers;
  820. let i;
  821.  
  822. // 没有指定scope,获取scope
  823. if (!scope) scope = getScope();
  824.  
  825. for (const key in _handlers) {
  826. if (Object.prototype.hasOwnProperty.call(_handlers, key)) {
  827. handlers = _handlers[key];
  828. for (i = 0; i < handlers.length;) {
  829. if (handlers[i].scope === scope) handlers.splice(i, 1);
  830. else i++;
  831. }
  832. }
  833. }
  834.  
  835. // 如果scope被删除,将scope重置为all
  836. if (getScope() === scope) setScope(newScope || 'all');
  837. }
  838.  
  839. // 清除修饰键
  840. function clearModifier (event) {
  841. let key = event.keyCode || event.which || event.charCode;
  842. const i = _downKeys.indexOf(key);
  843.  
  844. // 从列表中清除按压过的键
  845. if (i >= 0) {
  846. _downKeys.splice(i, 1);
  847. }
  848. // 特殊处理 cmmand 键,在 cmmand 组合快捷键 keyup 只执行一次的问题
  849. if (event.key && event.key.toLowerCase() === 'meta') {
  850. _downKeys.splice(0, _downKeys.length);
  851. }
  852.  
  853. // 修饰键 shiftKey altKey ctrlKey (command||metaKey) 清除
  854. if (key === 93 || key === 224) key = 91;
  855. if (key in _mods) {
  856. _mods[key] = false;
  857.  
  858. // 将修饰键重置为false
  859. for (const k in _modifier) if (_modifier[k] === key) hotkeys[k] = false;
  860. }
  861. }
  862.  
  863. function unbind (keysInfo, ...args) {
  864. // unbind(), unbind all keys
  865. if (!keysInfo) {
  866. Object.keys(_handlers).forEach((key) => delete _handlers[key]);
  867. } else if (Array.isArray(keysInfo)) {
  868. // support like : unbind([{key: 'ctrl+a', scope: 's1'}, {key: 'ctrl-a', scope: 's2', splitKey: '-'}])
  869. keysInfo.forEach((info) => {
  870. if (info.key) eachUnbind(info);
  871. });
  872. } else if (typeof keysInfo === 'object') {
  873. // support like unbind({key: 'ctrl+a, ctrl+b', scope:'abc'})
  874. if (keysInfo.key) eachUnbind(keysInfo);
  875. } else if (typeof keysInfo === 'string') {
  876. // support old method
  877. // eslint-disable-line
  878. let [scope, method] = args;
  879. if (typeof scope === 'function') {
  880. method = scope;
  881. scope = '';
  882. }
  883. eachUnbind({
  884. key: keysInfo,
  885. scope,
  886. method,
  887. splitKey: '+'
  888. });
  889. }
  890. }
  891.  
  892. // 解除绑定某个范围的快捷键
  893. const eachUnbind = ({
  894. key, scope, method, splitKey = '+'
  895. }) => {
  896. const multipleKeys = getKeys(key);
  897. multipleKeys.forEach((originKey) => {
  898. const unbindKeys = originKey.split(splitKey);
  899. const len = unbindKeys.length;
  900. const lastKey = unbindKeys[len - 1];
  901. const keyCode = lastKey === '*' ? '*' : code(lastKey);
  902. if (!_handlers[keyCode]) return
  903. // 判断是否传入范围,没有就获取范围
  904. if (!scope) scope = getScope();
  905. const mods = len > 1 ? getMods(_modifier, unbindKeys) : [];
  906. _handlers[keyCode] = _handlers[keyCode].filter((record) => {
  907. // 通过函数判断,是否解除绑定,函数相等直接返回
  908. const isMatchingMethod = method ? record.method === method : true;
  909. return !(
  910. isMatchingMethod &&
  911. record.scope === scope &&
  912. compareArray(record.mods, mods)
  913. )
  914. });
  915. });
  916. };
  917.  
  918. // 对监听对应快捷键的回调函数进行处理
  919. function eventHandler (event, handler, scope, element) {
  920. if (handler.element !== element) {
  921. return
  922. }
  923. let modifiersMatch;
  924.  
  925. // 看它是否在当前范围
  926. if (handler.scope === scope || handler.scope === 'all') {
  927. // 检查是否匹配修饰符(如果有返回true)
  928. modifiersMatch = handler.mods.length > 0;
  929.  
  930. for (const y in _mods) {
  931. if (Object.prototype.hasOwnProperty.call(_mods, y)) {
  932. if (
  933. (!_mods[y] && handler.mods.indexOf(+y) > -1) ||
  934. (_mods[y] && handler.mods.indexOf(+y) === -1)
  935. ) {
  936. modifiersMatch = false;
  937. }
  938. }
  939. }
  940.  
  941. // 调用处理程序,如果是修饰键不做处理
  942. if (
  943. (handler.mods.length === 0 &&
  944. !_mods[16] &&
  945. !_mods[18] &&
  946. !_mods[17] &&
  947. !_mods[91]) ||
  948. modifiersMatch ||
  949. handler.shortcut === '*'
  950. ) {
  951. if (handler.method(event, handler) === false) {
  952. if (event.preventDefault) event.preventDefault();
  953. else event.returnValue = false;
  954. if (event.stopPropagation) event.stopPropagation();
  955. if (event.cancelBubble) event.cancelBubble = true;
  956. }
  957. }
  958. }
  959. }
  960.  
  961. // 处理keydown事件
  962. function dispatch (event, element) {
  963. const asterisk = _handlers['*'];
  964. let key = event.keyCode || event.which || event.charCode;
  965.  
  966. // 表单控件过滤 默认表单控件不触发快捷键
  967. if (!hotkeys.filter.call(this, event)) return
  968.  
  969. // Gecko(Firefox)的command键值224,在Webkit(Chrome)中保持一致
  970. // Webkit左右 command 键值不一样
  971. if (key === 93 || key === 224) key = 91;
  972.  
  973. /**
  974. * Collect bound keys
  975. * If an Input Method Editor is processing key input and the event is keydown, return 229.
  976. * https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229
  977. * http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
  978. */
  979. if (_downKeys.indexOf(key) === -1 && key !== 229) _downKeys.push(key);
  980. /**
  981. * Jest test cases are required.
  982. * ===============================
  983. */
  984. ['ctrlKey', 'altKey', 'shiftKey', 'metaKey'].forEach((keyName) => {
  985. const keyNum = modifierMap[keyName];
  986. if (event[keyName] && _downKeys.indexOf(keyNum) === -1) {
  987. _downKeys.push(keyNum);
  988. } else if (!event[keyName] && _downKeys.indexOf(keyNum) > -1) {
  989. _downKeys.splice(_downKeys.indexOf(keyNum), 1);
  990. } else if (keyName === 'metaKey' && event[keyName] && _downKeys.length === 3) {
  991. /**
  992. * Fix if Command is pressed:
  993. * ===============================
  994. */
  995. if (!(event.ctrlKey || event.shiftKey || event.altKey)) {
  996. _downKeys = _downKeys.slice(_downKeys.indexOf(keyNum));
  997. }
  998. }
  999. });
  1000. /**
  1001. * -------------------------------
  1002. */
  1003.  
  1004. if (key in _mods) {
  1005. _mods[key] = true;
  1006.  
  1007. // 将特殊字符的key注册到 hotkeys 上
  1008. for (const k in _modifier) {
  1009. if (_modifier[k] === key) hotkeys[k] = true;
  1010. }
  1011.  
  1012. if (!asterisk) return
  1013. }
  1014.  
  1015. // 将 modifierMap 里面的修饰键绑定到 event 中
  1016. for (const e in _mods) {
  1017. if (Object.prototype.hasOwnProperty.call(_mods, e)) {
  1018. _mods[e] = event[modifierMap[e]];
  1019. }
  1020. }
  1021. /**
  1022. * https://github.com/jaywcjlove/hotkeys/pull/129
  1023. * This solves the issue in Firefox on Windows where hotkeys corresponding to special characters would not trigger.
  1024. * An example of this is ctrl+alt+m on a Swedish keyboard which is used to type μ.
  1025. * Browser support: https://caniuse.com/#feat=keyboardevent-getmodifierstate
  1026. */
  1027. if (event.getModifierState && (!(event.altKey && !event.ctrlKey) && event.getModifierState('AltGraph'))) {
  1028. if (_downKeys.indexOf(17) === -1) {
  1029. _downKeys.push(17);
  1030. }
  1031.  
  1032. if (_downKeys.indexOf(18) === -1) {
  1033. _downKeys.push(18);
  1034. }
  1035.  
  1036. _mods[17] = true;
  1037. _mods[18] = true;
  1038. }
  1039.  
  1040. // 获取范围 默认为 `all`
  1041. const scope = getScope();
  1042. // 对任何快捷键都需要做的处理
  1043. if (asterisk) {
  1044. for (let i = 0; i < asterisk.length; i++) {
  1045. if (
  1046. asterisk[i].scope === scope &&
  1047. ((event.type === 'keydown' && asterisk[i].keydown) ||
  1048. (event.type === 'keyup' && asterisk[i].keyup))
  1049. ) {
  1050. eventHandler(event, asterisk[i], scope, element);
  1051. }
  1052. }
  1053. }
  1054. // key 不在 _handlers 中返回
  1055. if (!(key in _handlers)) return
  1056.  
  1057. for (let i = 0; i < _handlers[key].length; i++) {
  1058. if (
  1059. (event.type === 'keydown' && _handlers[key][i].keydown) ||
  1060. (event.type === 'keyup' && _handlers[key][i].keyup)
  1061. ) {
  1062. if (_handlers[key][i].key) {
  1063. const record = _handlers[key][i];
  1064. const { splitKey } = record;
  1065. const keyShortcut = record.key.split(splitKey);
  1066. const _downKeysCurrent = []; // 记录当前按键键值
  1067. for (let a = 0; a < keyShortcut.length; a++) {
  1068. _downKeysCurrent.push(code(keyShortcut[a]));
  1069. }
  1070. if (_downKeysCurrent.sort().join('') === _downKeys.sort().join('')) {
  1071. // 找到处理内容
  1072. eventHandler(event, record, scope, element);
  1073. }
  1074. }
  1075. }
  1076. }
  1077. }
  1078.  
  1079. // 判断 element 是否已经绑定事件
  1080. function isElementBind (element) {
  1081. return elementHasBindEvent.indexOf(element) > -1
  1082. }
  1083.  
  1084. function hotkeys (key, option, method) {
  1085. _downKeys = [];
  1086. const keys = getKeys(key); // 需要处理的快捷键列表
  1087. let mods = [];
  1088. let scope = 'all'; // scope默认为all,所有范围都有效
  1089. let element = document; // 快捷键事件绑定节点
  1090. let i = 0;
  1091. let keyup = false;
  1092. let keydown = true;
  1093. let splitKey = '+';
  1094.  
  1095. // 对为设定范围的判断
  1096. if (method === undefined && typeof option === 'function') {
  1097. method = option;
  1098. }
  1099.  
  1100. if (Object.prototype.toString.call(option) === '[object Object]') {
  1101. if (option.scope) scope = option.scope; // eslint-disable-line
  1102. if (option.element) element = option.element; // eslint-disable-line
  1103. if (option.keyup) keyup = option.keyup; // eslint-disable-line
  1104. if (option.keydown !== undefined) keydown = option.keydown; // eslint-disable-line
  1105. if (typeof option.splitKey === 'string') splitKey = option.splitKey; // eslint-disable-line
  1106. }
  1107.  
  1108. if (typeof option === 'string') scope = option;
  1109.  
  1110. // 对于每个快捷键进行处理
  1111. for (; i < keys.length; i++) {
  1112. key = keys[i].split(splitKey); // 按键列表
  1113. mods = [];
  1114.  
  1115. // 如果是组合快捷键取得组合快捷键
  1116. if (key.length > 1) mods = getMods(_modifier, key);
  1117.  
  1118. // 将非修饰键转化为键码
  1119. key = key[key.length - 1];
  1120. key = key === '*' ? '*' : code(key); // *表示匹配所有快捷键
  1121.  
  1122. // 判断key是否在_handlers中,不在就赋一个空数组
  1123. if (!(key in _handlers)) _handlers[key] = [];
  1124. _handlers[key].push({
  1125. keyup,
  1126. keydown,
  1127. scope,
  1128. mods,
  1129. shortcut: keys[i],
  1130. method,
  1131. key: keys[i],
  1132. splitKey,
  1133. element
  1134. });
  1135. }
  1136. // 在全局document上设置快捷键
  1137. if (typeof element !== 'undefined' && !isElementBind(element) && window) {
  1138. elementHasBindEvent.push(element);
  1139. addEvent(element, 'keydown', (e) => {
  1140. dispatch(e, element);
  1141. });
  1142. if (!winListendFocus) {
  1143. winListendFocus = true;
  1144. addEvent(window, 'focus', () => {
  1145. _downKeys = [];
  1146. });
  1147. }
  1148. addEvent(element, 'keyup', (e) => {
  1149. dispatch(e, element);
  1150. clearModifier(e);
  1151. });
  1152. }
  1153. }
  1154.  
  1155. function trigger (shortcut, scope = 'all') {
  1156. Object.keys(_handlers).forEach((key) => {
  1157. const data = _handlers[key].find((item) => item.scope === scope && item.shortcut === shortcut);
  1158. if (data && data.method) {
  1159. data.method();
  1160. }
  1161. });
  1162. }
  1163.  
  1164. const _api = {
  1165. setScope,
  1166. getScope,
  1167. deleteScope,
  1168. getPressedKeyCodes,
  1169. isPressed,
  1170. filter,
  1171. trigger,
  1172. unbind,
  1173. keyMap: _keyMap,
  1174. modifier: _modifier,
  1175. modifierMap
  1176. };
  1177. for (const a in _api) {
  1178. if (Object.prototype.hasOwnProperty.call(_api, a)) {
  1179. hotkeys[a] = _api[a];
  1180. }
  1181. }
  1182.  
  1183. if (typeof window !== 'undefined') {
  1184. const _hotkeys = window.hotkeys;
  1185. hotkeys.noConflict = (deep) => {
  1186. if (deep && window.hotkeys === hotkeys) {
  1187. window.hotkeys = _hotkeys;
  1188. }
  1189. return hotkeys
  1190. };
  1191. window.hotkeys = hotkeys;
  1192. }
  1193.  
  1194. /*!
  1195. * @name hotKeyRegister.js
  1196. * @description vue-debug-helper的快捷键配置
  1197. * @version 0.0.1
  1198. * @author xxxily
  1199. * @date 2022/04/26 14:37
  1200. * @github https://github.com/xxxily
  1201. */
  1202.  
  1203. function hotKeyRegister () {
  1204. const hotKeyMap = {
  1205. 'shift+alt+a,shift+alt+ctrl+a': function (event, handler) {
  1206. debug.log('全部组件混合统计', helper.methods.componentsSummaryStatisticsSort());
  1207. },
  1208. 'shift+alt+l': function (event, handler) {
  1209. debug.log('当前存活组件统计', helper.methods.componentsStatistics());
  1210. },
  1211. 'shift+alt+d': function (event, handler) {
  1212. debug.log('已销毁组件统计', helper.methods.destroyStatisticsSort());
  1213. },
  1214. 'shift+alt+c': function (event, handler) {
  1215. helper.methods.clearAll();
  1216. debug.log('清空统计信息');
  1217. },
  1218. 'shift+alt+e': function (event, handler) {
  1219. if (helper.ddConfig.enable) {
  1220. debug.log('取消数据注入(undd)');
  1221. helper.methods.undd();
  1222. } else {
  1223. const filter = window.prompt('组件过滤器(如果为空,则对所有组件注入)', '');
  1224. const size = window.prompt('指定注入数据的大小值(默认1Mb)', 1024);
  1225. debug.log('数据注入(dd)');
  1226. helper.methods.dd(filter, Number(size));
  1227. }
  1228. }
  1229. };
  1230.  
  1231. Object.keys(hotKeyMap).forEach(key => {
  1232. hotkeys(key, hotKeyMap[key]);
  1233. });
  1234. }
  1235.  
  1236. /*!
  1237. * @name vueDetector.js
  1238. * @description 检测页面是否存在Vue对象
  1239. * @version 0.0.1
  1240. * @author xxxily
  1241. * @date 2022/04/27 11:43
  1242. * @github https://github.com/xxxily
  1243. */
  1244.  
  1245. /**
  1246. * 检测页面是否存在Vue对象,方法参考:https://github.com/vuejs/devtools/blob/main/packages/shell-chrome/src/detector.js
  1247. * @param {window} win windwod对象
  1248. * @param {function} callback 检测到Vue对象后的回调函数
  1249. */
  1250. function vueDetect (win, callback) {
  1251. let delay = 1000;
  1252. let detectRemainingTries = 10;
  1253.  
  1254. function runDetect () {
  1255. // Method 1: use defineProperty to detect Vue, has BUG, so use Method 2
  1256. // 使用下面方式会导致 'Vue' in window 为 true,从而引发其他问题
  1257. // Object.defineProperty(win, 'Vue', {
  1258. // enumerable: true,
  1259. // configurable: true,
  1260. // get () {
  1261. // return win.__originalVue__
  1262. // },
  1263. // set (value) {
  1264. // win.__originalVue__ = value
  1265.  
  1266. // if (value && value.mixin) {
  1267. // callback(value)
  1268. // }
  1269. // }
  1270. // })
  1271.  
  1272. // Method 2: Check Vue 3
  1273. const vueDetected = !!(window.__VUE__);
  1274. if (vueDetected) {
  1275. callback(window.__VUE__);
  1276. return
  1277. }
  1278.  
  1279. // Method 3: Scan all elements inside document
  1280. const all = document.querySelectorAll('*');
  1281. let el;
  1282. for (let i = 0; i < all.length; i++) {
  1283. if (all[i].__vue__) {
  1284. el = all[i];
  1285. break
  1286. }
  1287. }
  1288. if (el) {
  1289. let Vue = Object.getPrototypeOf(el.__vue__).constructor;
  1290. while (Vue.super) {
  1291. Vue = Vue.super;
  1292. }
  1293. callback(Vue);
  1294. return
  1295. }
  1296.  
  1297. if (detectRemainingTries > 0) {
  1298. detectRemainingTries--;
  1299. setTimeout(() => {
  1300. runDetect();
  1301. }, delay);
  1302. delay *= 5;
  1303. }
  1304. }
  1305.  
  1306. setTimeout(() => {
  1307. runDetect();
  1308. }, 100);
  1309. }
  1310.  
  1311. /**
  1312. * 判断是否处于Iframe中
  1313. * @returns {boolean}
  1314. */
  1315. function isInIframe () {
  1316. return window !== window.top
  1317. }
  1318.  
  1319. /**
  1320. * 由于tampermonkey对window对象进行了封装,我们实际访问到的window并非页面真实的window
  1321. * 这就导致了如果我们需要将某些对象挂载到页面的window进行调试的时候就无法挂载了
  1322. * 所以必须使用特殊手段才能访问到页面真实的window对象,于是就有了下面这个函数
  1323. * @returns {Promise<void>}
  1324. */
  1325. async function getPageWindow () {
  1326. return new Promise(function (resolve, reject) {
  1327. if (window._pageWindow) {
  1328. return resolve(window._pageWindow)
  1329. }
  1330.  
  1331. const listenEventList = ['load', 'mousemove', 'scroll', 'get-page-window-event'];
  1332.  
  1333. function getWin (event) {
  1334. window._pageWindow = this;
  1335. // debug.log('getPageWindow succeed', event)
  1336. listenEventList.forEach(eventType => {
  1337. window.removeEventListener(eventType, getWin, true);
  1338. });
  1339. resolve(window._pageWindow);
  1340. }
  1341.  
  1342. listenEventList.forEach(eventType => {
  1343. window.addEventListener(eventType, getWin, true);
  1344. });
  1345.  
  1346. /* 自行派发事件以便用最短的时候获得pageWindow对象 */
  1347. window.dispatchEvent(new window.Event('get-page-window-event'));
  1348. })
  1349. }
  1350.  
  1351. let registerStatus = 'init';
  1352. window._debugMode_ = true
  1353.  
  1354. ;(async function () {
  1355. if (isInIframe()) {
  1356. debug.log('running in iframe, skip init', window.location.href);
  1357. return false
  1358. }
  1359.  
  1360. debug.log('init');
  1361.  
  1362. const win = await getPageWindow();
  1363. vueDetect(win, function (Vue) {
  1364. mixinRegister(Vue);
  1365. menuRegister();
  1366. hotKeyRegister();
  1367.  
  1368. debug.log('vue debug helper register success');
  1369. registerStatus = 'success';
  1370. });
  1371.  
  1372. setTimeout(() => {
  1373. if (registerStatus !== 'success') {
  1374. debug.warn('vue debug helper register failed, please check if vue is loaded .', win.location.href);
  1375. }
  1376. }, 1000 * 10);
  1377. })();