vue-debug-helper

Vue components debug helper

As of 2022-04-28. See the latest version.

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