Vue調試分析助手

Vue組件探測、統計、分析輔助腳本

  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.22
  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_getResourceText
  21. // @grant GM_addStyle
  22. // @grant GM_setValue
  23. // @grant GM_getValue
  24. // @grant GM_deleteValue
  25. // @grant GM_listValues
  26. // @grant GM_addValueChangeListener
  27. // @grant GM_removeValueChangeListener
  28. // @grant GM_registerMenuCommand
  29. // @grant GM_unregisterMenuCommand
  30. // @grant GM_getTab
  31. // @grant GM_saveTab
  32. // @grant GM_getTabs
  33. // @grant GM_openInTab
  34. // @grant GM_download
  35. // @grant GM_xmlhttpRequest
  36. // @require https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js
  37. // @require https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/core.js
  38. // @require https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/md5.js
  39. // @require https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js
  40. // @require https://cdn.jsdelivr.net/npm/jquery-contextmenu@2.9.2/dist/jquery.contextMenu.min.js
  41. // @require https://cdn.jsdelivr.net/npm/jquery-contextmenu@2.9.2/dist/jquery.ui.position.min.js
  42. // @resource contextMenuCss https://cdn.jsdelivr.net/npm/jquery-contextmenu@2.9.2/dist/jquery.contextMenu.min.css
  43. // @run-at document-start
  44. // @connect 127.0.0.1
  45. // @license GPL
  46. // ==/UserScript==
  47. (function (w) { if (w) { w._vueDebugHelper_ = 'https://github.com/xxxily/vue-debug-helper'; } })();
  48.  
  49. class AssertionError extends Error {}
  50. AssertionError.prototype.name = 'AssertionError';
  51.  
  52. /**
  53. * Minimal assert function
  54. * @param {any} t Value to check if falsy
  55. * @param {string=} m Optional assertion error message
  56. * @throws {AssertionError}
  57. */
  58. function assert (t, m) {
  59. if (!t) {
  60. var err = new AssertionError(m);
  61. if (Error.captureStackTrace) Error.captureStackTrace(err, assert);
  62. throw err
  63. }
  64. }
  65.  
  66. /* eslint-env browser */
  67.  
  68. let ls;
  69. if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
  70. // A simple localStorage interface so that lsp works in SSR contexts. Not for persistant storage in node.
  71. const _nodeStorage = {};
  72. ls = {
  73. getItem (name) {
  74. return _nodeStorage[name] || null
  75. },
  76. setItem (name, value) {
  77. if (arguments.length < 2) throw new Error('Failed to execute \'setItem\' on \'Storage\': 2 arguments required, but only 1 present.')
  78. _nodeStorage[name] = (value).toString();
  79. },
  80. removeItem (name) {
  81. delete _nodeStorage[name];
  82. }
  83. };
  84. } else {
  85. ls = window.localStorage;
  86. }
  87.  
  88. var localStorageProxy = (name, opts = {}) => {
  89. assert(name, 'namepace required');
  90. const {
  91. defaults = {},
  92. lspReset = false,
  93. storageEventListener = true
  94. } = opts;
  95.  
  96. const state = new EventTarget();
  97. try {
  98. const restoredState = JSON.parse(ls.getItem(name)) || {};
  99. if (restoredState.lspReset !== lspReset) {
  100. ls.removeItem(name);
  101. for (const [k, v] of Object.entries({
  102. ...defaults
  103. })) {
  104. state[k] = v;
  105. }
  106. } else {
  107. for (const [k, v] of Object.entries({
  108. ...defaults,
  109. ...restoredState
  110. })) {
  111. state[k] = v;
  112. }
  113. }
  114. } catch (e) {
  115. console.error(e);
  116. ls.removeItem(name);
  117. }
  118.  
  119. state.lspReset = lspReset;
  120.  
  121. if (storageEventListener && typeof window !== 'undefined' && typeof window.addEventListener !== 'undefined') {
  122. state.addEventListener('storage', (ev) => {
  123. // Replace state with whats stored on localStorage... it is newer.
  124. for (const k of Object.keys(state)) {
  125. delete state[k];
  126. }
  127. const restoredState = JSON.parse(ls.getItem(name)) || {};
  128. for (const [k, v] of Object.entries({
  129. ...defaults,
  130. ...restoredState
  131. })) {
  132. state[k] = v;
  133. }
  134. opts.lspReset = restoredState.lspReset;
  135. state.dispatchEvent(new Event('update'));
  136. });
  137. }
  138.  
  139. function boundHandler (rootRef) {
  140. return {
  141. get (obj, prop) {
  142. if (typeof obj[prop] === 'object' && obj[prop] !== null) {
  143. return new Proxy(obj[prop], boundHandler(rootRef))
  144. } else if (typeof obj[prop] === 'function' && obj === rootRef && prop !== 'constructor') {
  145. // this returns bound EventTarget functions
  146. return obj[prop].bind(obj)
  147. } else {
  148. return obj[prop]
  149. }
  150. },
  151. set (obj, prop, value) {
  152. obj[prop] = value;
  153. try {
  154. ls.setItem(name, JSON.stringify(rootRef));
  155. rootRef.dispatchEvent(new Event('update'));
  156. return true
  157. } catch (e) {
  158. console.error(e);
  159. return false
  160. }
  161. }
  162. }
  163. }
  164.  
  165. return new Proxy(state, boundHandler(state))
  166. };
  167.  
  168. /**
  169. * 对特定数据结构的对象进行排序
  170. * @param {object} obj 一个对象,其结构应该类似于:{key1: [], key2: []}
  171. * @param {boolean} reverse -可选 是否反转、降序排列,默认为false
  172. * @param {object} opts -可选 指定数组的配置项,默认为{key: 'key', value: 'value'}
  173. * @param {object} opts.key -可选 指定对象键名的别名,默认为'key'
  174. * @param {object} opts.value -可选 指定对象值的别名,默认为'value'
  175. * @returns {array} 返回一个数组,其结构应该类似于:[{key: key1, value: []}, {key: key2, value: []}]
  176. */
  177. const objSort = (obj, reverse, opts = { key: 'key', value: 'value' }) => {
  178. const arr = [];
  179. for (const key in obj) {
  180. if (Object.prototype.hasOwnProperty.call(obj, key) && Array.isArray(obj[key])) {
  181. const tmpObj = {};
  182. tmpObj[opts.key] = key;
  183. tmpObj[opts.value] = obj[key];
  184. arr.push(tmpObj);
  185. }
  186. }
  187.  
  188. arr.sort((a, b) => {
  189. return a[opts.value].length - b[opts.value].length
  190. });
  191.  
  192. reverse && arr.reverse();
  193. return arr
  194. };
  195.  
  196. /**
  197. * 根据指定长度创建空白数据
  198. * @param {number} size -可选 指str的重复次数,默认为1024次,如果str为单个单字节字符,则意味着默认产生1Mb的空白数据
  199. * @param {string|number|any} str - 可选 指定数据的字符串,默认为'd'
  200. */
  201. function createEmptyData (count = 1024, str = 'd') {
  202. const arr = [];
  203. arr.length = count + 1;
  204. return arr.join(str)
  205. }
  206.  
  207. /**
  208. * 将字符串分隔的过滤器转换为数组形式的过滤器
  209. * @param {string|array} filter - 必选 字符串或数组,字符串支持使用 , |符号对多个项进行分隔
  210. * @returns {array}
  211. */
  212. function toArrFilters (filter) {
  213. filter = filter || [];
  214.  
  215. /* 如果是字符串,则支持通过, | 两个符号来指定多个组件名称的过滤器 */
  216. if (typeof filter === 'string') {
  217. /* 移除前后的, |分隔符,防止出现空字符的过滤规则 */
  218. filter.replace(/^(,|\|)/, '').replace(/(,|\|)$/, '');
  219.  
  220. if (/\|/.test(filter)) {
  221. filter = filter.split('|');
  222. } else {
  223. filter = filter.split(',');
  224. }
  225. }
  226.  
  227. filter = filter.map(item => item.trim());
  228.  
  229. return filter
  230. }
  231.  
  232. /**
  233. * 将某个过滤器的字符串添加到指定的过滤器集合里
  234. * @param {object} obj helper.config
  235. * @param {string} filtersName
  236. * @param {string} str
  237. * @returns
  238. */
  239. function addToFilters (obj, filtersName, str) {
  240. const strType = typeof str;
  241. if (!obj || !filtersName || !str || !(strType === 'string' || strType === 'number')) {
  242. return
  243. }
  244.  
  245. const filters = obj[filtersName];
  246. if (!filters) {
  247. obj[filtersName] = [str];
  248. } else if (Array.isArray(filters)) {
  249. if (filters.includes(str)) {
  250. /* 将str提到最后 */
  251. const index = filters.indexOf(str);
  252. filters.splice(index, 1);
  253. filters.push(str);
  254. } else {
  255. filters.push(str);
  256. }
  257.  
  258. /* 去重 */
  259. obj[filtersName] = Array.from(new Set(filters));
  260. }
  261. }
  262.  
  263. /**
  264. * 字符串过滤器和字符串的匹配方法
  265. * @param {string} filter -必选 过滤器的字符串
  266. * @param {string} str -必选 要跟过滤字符串进行匹配的字符串
  267. * @returns
  268. */
  269. function stringMatch (filter, str) {
  270. let isMatch = false;
  271.  
  272. if (!filter || !str) {
  273. return isMatch
  274. }
  275.  
  276. filter = String(filter);
  277. str = String(str);
  278.  
  279. /* 带星表示进行模糊匹配,且不区分大小写 */
  280. if (/\*/.test(filter)) {
  281. filter = filter.replace(/\*/g, '').toLocaleLowerCase();
  282. if (str.toLocaleLowerCase().indexOf(filter) > -1) {
  283. isMatch = true;
  284. }
  285. } else if (str.includes(filter)) {
  286. isMatch = true;
  287. }
  288.  
  289. return isMatch
  290. }
  291.  
  292. /**
  293. * 判断某个字符串是否跟filters相匹配
  294. * @param {array|string} filters - 必选 字符串或数组,字符串支持使用 , |符号对多个项进行分隔
  295. * @param {string|number} str - 必选 一个字符串或数字,用于跟过滤器进行匹配判断
  296. */
  297. function filtersMatch (filters, str) {
  298. if (!filters || !str) {
  299. return false
  300. }
  301.  
  302. filters = Array.isArray(filters) ? filters : toArrFilters(filters);
  303. str = String(str);
  304.  
  305. let result = false;
  306. for (let i = 0; i < filters.length; i++) {
  307. const filter = String(filters[i]);
  308.  
  309. if (stringMatch(filter, str)) {
  310. result = true;
  311. break
  312. }
  313. }
  314.  
  315. return result
  316. }
  317.  
  318. const inBrowser = typeof window !== 'undefined';
  319.  
  320. function getVueDevtools () {
  321. return inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__
  322. }
  323.  
  324. function copyToClipboard (text) {
  325. if (inBrowser) {
  326. const input = document.createElement('input');
  327. input.value = text;
  328. document.body.appendChild(input);
  329. input.select();
  330. document.execCommand('copy');
  331. document.body.removeChild(input);
  332. }
  333. }
  334.  
  335. function openInTab (url, opts) {
  336. if (window.GM_openInTab) {
  337. window.GM_openInTab(url, opts || {
  338. active: true,
  339. insert: true,
  340. setParent: true
  341. });
  342. }
  343. }
  344.  
  345. window.vueDebugHelper = {
  346. /* 存储全部未被销毁的组件对象 */
  347. components: {},
  348. /* 存储全部创建过的组件的概要信息,即使销毁了概要信息依然存在 */
  349. componentsSummary: {},
  350. /* 基于componentsSummary的组件情况统计 */
  351. componentsSummaryStatistics: {},
  352. /* 已销毁的组件概要信息列表 */
  353. destroyList: [],
  354. /* 基于destroyList的组件情况统计 */
  355. destroyStatistics: {},
  356.  
  357. config: {
  358. inspect: {
  359. enabled: false
  360. },
  361.  
  362. contextMenu: {
  363. /* 简化菜单,将部分菜单项放到更多菜单的子项中 */
  364. simplify: false
  365. },
  366.  
  367. performanceObserver: {
  368. enabled: false,
  369. // https://runebook.dev/zh-CN/docs/dom/performanceentry/entrytype
  370. entryTypes: ['element', 'navigation', 'resource', 'mark', 'measure', 'paint', 'longtask']
  371. },
  372.  
  373. /* 控制接口缓存 */
  374. ajaxCache: {
  375. enabled: false,
  376. filters: ['*'],
  377.  
  378. /* 设置缓存多久失效,默认为1天 */
  379. expires: 1000 * 60 * 60 * 24
  380. },
  381.  
  382. blockAjax: {
  383. enabled: false,
  384. filters: []
  385. },
  386.  
  387. replaceAjax: {
  388. enabled: false,
  389. replaceRules: []
  390. },
  391.  
  392. /* 测量选择器时间差 */
  393. measureSelectorInterval: {
  394. selector1: '',
  395. selector2: ''
  396. },
  397.  
  398. /* 是否在控制台打印组件生命周期的相关信息 */
  399. lifecycle: {
  400. show: false,
  401. filters: ['created'],
  402. componentFilters: []
  403. },
  404.  
  405. /* 查找组件的过滤器配置 */
  406. findComponentsFilters: [],
  407.  
  408. /* 阻止组件创建的过滤器 */
  409. blockFilters: [],
  410.  
  411. devtools: true,
  412.  
  413. /* 改写Vue.component */
  414. hackVueComponent: false,
  415.  
  416. /* 给组件注入空白数据的配置信息 */
  417. dd: {
  418. enabled: false,
  419. filters: [],
  420. count: 1024
  421. }
  422. }
  423. };
  424.  
  425. const helper = window.vueDebugHelper;
  426.  
  427. /* 配置信息跟localStorage联动 */
  428. const state = localStorageProxy('vueDebugHelperConfig', {
  429. defaults: helper.config,
  430. lspReset: false,
  431. storageEventListener: false
  432. });
  433. helper.config = state;
  434.  
  435. const methods = {
  436. objSort,
  437. createEmptyData,
  438. /* 清除全部helper的全部记录数据,以便重新统计 */
  439. clearAll () {
  440. helper.components = {};
  441. helper.componentsSummary = {};
  442. helper.componentsSummaryStatistics = {};
  443. helper.destroyList = [];
  444. helper.destroyStatistics = {};
  445. },
  446.  
  447. /**
  448. * 对当前的helper.components进行统计与排序
  449. * 如果一直没运行过清理函数,则表示统计页面创建至今依然存活的组件对象
  450. * 运行过清理函数,则表示统计清理后新创建且至今依然存活的组件对象
  451. */
  452. componentsStatistics (reverse = true) {
  453. const tmpObj = {};
  454.  
  455. Object.keys(helper.components).forEach(key => {
  456. const component = helper.components[key];
  457.  
  458. tmpObj[component._componentName]
  459. ? tmpObj[component._componentName].push(component)
  460. : (tmpObj[component._componentName] = [component]);
  461. });
  462.  
  463. return objSort(tmpObj, reverse, {
  464. key: 'componentName',
  465. value: 'componentInstance'
  466. })
  467. },
  468.  
  469. /**
  470. * 对componentsSummaryStatistics进行排序输出,以便可以直观查看组件的创建情况
  471. */
  472. componentsSummaryStatisticsSort (reverse = true) {
  473. return objSort(helper.componentsSummaryStatistics, reverse, {
  474. key: 'componentName',
  475. value: 'componentsSummary'
  476. })
  477. },
  478.  
  479. /**
  480. * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况
  481. */
  482. destroyStatisticsSort (reverse = true) {
  483. return objSort(helper.destroyStatistics, reverse, {
  484. key: 'componentName',
  485. value: 'destroyList'
  486. })
  487. },
  488.  
  489. /**
  490. * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况
  491. */
  492. getDestroyByDuration (duration = 1000) {
  493. const destroyList = helper.destroyList;
  494. const destroyListLength = destroyList.length;
  495. const destroyListDuration = destroyList.map(item => item.duration).sort();
  496. const maxDuration = Math.max(...destroyListDuration);
  497. const minDuration = Math.min(...destroyListDuration);
  498. const avgDuration = destroyListDuration.reduce((a, b) => a + b, 0) / destroyListLength;
  499. const durationRange = maxDuration - minDuration;
  500. const durationRangePercent = (duration - minDuration) / durationRange;
  501.  
  502. return {
  503. destroyList,
  504. destroyListLength,
  505. destroyListDuration,
  506. maxDuration,
  507. minDuration,
  508. avgDuration,
  509. durationRange,
  510. durationRangePercent
  511. }
  512. },
  513.  
  514. initComponentInfo (vm) {
  515. if (vm && !vm._componentTag) {
  516. const tag = vm.$vnode?.tag || vm.$options?._componentTag || vm._uid;
  517. vm._componentTag = tag;
  518. vm._componentName = isNaN(Number(tag)) ? tag.replace(/^vue-component-\d+-/, '') : 'anonymous-component';
  519. vm._componentChain = this.getComponentChain(vm);
  520.  
  521. /* 判断是否为函数式组件,函数式组件无状态 (没有响应式数据),也没有实例,也没生命周期概念 */
  522. if (vm._componentName === 'anonymous-component' && !vm.$parent && !vm.$vnode) {
  523. vm._componentName = 'functional-component';
  524. }
  525. }
  526. },
  527.  
  528. /**
  529. * 获取组件的调用链信息
  530. */
  531. getComponentChain (component, moreDetail = false) {
  532. const result = [];
  533. let current = component;
  534. let deep = 0;
  535.  
  536. while (current && deep < 50) {
  537. deep++;
  538.  
  539. /**
  540. * 由于脚本注入的运行时间会比应用创建时间晚,所以会导致部分先创建的组件缺少相关信息
  541. * 这里尝试对部分信息进行修复,以便更好的查看组件的创建情况
  542. */
  543. if (!current._componentTag) {
  544. const tag = current.$vnode?.tag || current.$options?._componentTag || current._uid;
  545. current._componentTag = tag;
  546. current._componentName = isNaN(Number(tag)) ? tag.replace(/^vue-component-\d+-/, '') : 'anonymous-component';
  547. }
  548.  
  549. if (moreDetail) {
  550. result.push({
  551. tag: current._componentTag,
  552. name: current._componentName,
  553. componentsSummary: helper.componentsSummary[current._uid] || null
  554. });
  555. } else {
  556. result.push(current._componentName);
  557. }
  558.  
  559. current = current.$parent;
  560. }
  561.  
  562. if (moreDetail) {
  563. return result
  564. } else {
  565. return result.join(' -> ')
  566. }
  567. },
  568.  
  569. printLifeCycleInfo (lifecycleFilters, componentFilters) {
  570. lifecycleFilters = toArrFilters(lifecycleFilters);
  571. componentFilters = toArrFilters(componentFilters);
  572.  
  573. helper.config.lifecycle = {
  574. show: true,
  575. filters: lifecycleFilters,
  576. componentFilters: componentFilters
  577. };
  578. },
  579. notPrintLifeCycleInfo () {
  580. helper.config.lifecycle.show = false;
  581. },
  582.  
  583. /**
  584. * 查找组件
  585. * @param {string|array} filters 组件名称或组件uid的过滤器,可以是字符串或者数组,如果是字符串多个过滤选可用,或|分隔
  586. * 如果过滤项是数字,则跟组件的id进行精确匹配,如果是字符串,则跟组件的tag信息进行模糊匹配
  587. * @returns {object} {components: [], componentNames: []}
  588. */
  589. findComponents (filters) {
  590. filters = toArrFilters(filters);
  591.  
  592. /* 对filters进行预处理,如果为纯数字则表示通过id查找组件 */
  593. filters = filters.map(filter => {
  594. if (/^\d+$/.test(filter)) {
  595. return Number(filter)
  596. } else {
  597. return filter
  598. }
  599. });
  600.  
  601. helper.config.findComponentsFilters = filters;
  602.  
  603. const result = {
  604. components: [],
  605. globalComponents: [],
  606. destroyedComponents: []
  607. };
  608.  
  609. /* 在helper.components里进行组件查找 */
  610. const components = helper.components;
  611. const keys = Object.keys(components);
  612. for (let i = 0; i < keys.length; i++) {
  613. const component = components[keys[i]];
  614.  
  615. for (let j = 0; j < filters.length; j++) {
  616. const filter = filters[j];
  617.  
  618. if (typeof filter === 'number' && component._uid === filter) {
  619. result.components.push(component);
  620. break
  621. } else if (typeof filter === 'string') {
  622. const { _componentTag, _componentName } = component;
  623.  
  624. if (stringMatch(filter, _componentTag) || stringMatch(filter, _componentName)) {
  625. result.components.push(component);
  626. break
  627. }
  628. }
  629. }
  630. }
  631.  
  632. /* 进行全局组件查找 */
  633. const globalComponentsKeys = Object.keys(helper.Vue.options.components);
  634. for (let i = 0; i < globalComponentsKeys.length; i++) {
  635. const key = String(globalComponentsKeys[i]);
  636. const component = helper.Vue.options.components[globalComponentsKeys[i]];
  637.  
  638. if (filtersMatch(filters, key)) {
  639. const tmpObj = {};
  640. tmpObj[key] = component;
  641. result.globalComponents.push(tmpObj);
  642. }
  643. }
  644.  
  645. helper.destroyList.forEach(item => {
  646. for (let j = 0; j < filters.length; j++) {
  647. const filter = filters[j];
  648.  
  649. if (typeof filter === 'number' && item.uid === filter) {
  650. result.destroyedComponents.push(item);
  651. break
  652. } else if (typeof filter === 'string') {
  653. if (stringMatch(filter, item.tag) || stringMatch(filter, item.name)) {
  654. result.destroyedComponents.push(item);
  655. break
  656. }
  657. }
  658. }
  659. });
  660.  
  661. return result
  662. },
  663.  
  664. findNotContainElementComponents () {
  665. const result = [];
  666. const keys = Object.keys(helper.components);
  667. keys.forEach(key => {
  668. const component = helper.components[key];
  669. const elStr = Object.prototype.toString.call(component.$el);
  670. if (!/(HTML|Comment)/.test(elStr)) {
  671. result.push(component);
  672. }
  673. });
  674.  
  675. return result
  676. },
  677.  
  678. /**
  679. * 阻止组件的创建
  680. * @param {string|array} filters 组件名称过滤器,可以是字符串或者数组,如果是字符串多个过滤选可用,或|分隔
  681. */
  682. blockComponents (filters) {
  683. filters = toArrFilters(filters);
  684. helper.config.blockFilters = filters;
  685. },
  686.  
  687. /**
  688. * 给指定组件注入大量空数据,以便观察组件的内存泄露情况
  689. * @param {Array|string} filter -必选 指定组件的名称,如果为空则表示注入所有组件
  690. * @param {number} count -可选 指定注入空数据的大小,单位Kb,默认为1024Kb,即1Mb
  691. * @returns
  692. */
  693. dd (filter, count = 1024) {
  694. filter = toArrFilters(filter);
  695. helper.config.dd = {
  696. enabled: true,
  697. filters: filter,
  698. count
  699. };
  700. },
  701. /* 禁止给组件注入空数据 */
  702. undd () {
  703. helper.config.dd = {
  704. enabled: false,
  705. filters: [],
  706. count: 1024
  707. };
  708.  
  709. /* 删除之前注入的数据 */
  710. Object.keys(helper.components).forEach(key => {
  711. const component = helper.components[key];
  712. component.$data && delete component.$data.__dd__;
  713. });
  714. },
  715.  
  716. toggleDevtools () {
  717. helper.config.devtools = !helper.config.devtools;
  718. }
  719. };
  720.  
  721. helper.methods = methods;
  722.  
  723. class Debug {
  724. constructor (msg, printTime = false) {
  725. const t = this;
  726. msg = msg || 'debug message:';
  727. t.log = t.createDebugMethod('log', null, msg);
  728. t.error = t.createDebugMethod('error', null, msg);
  729. t.info = t.createDebugMethod('info', null, msg);
  730. t.warn = t.createDebugMethod('warn', null, msg);
  731. }
  732.  
  733. create (msg) {
  734. return new Debug(msg)
  735. }
  736.  
  737. createDebugMethod (name, color, tipsMsg) {
  738. name = name || 'info';
  739.  
  740. const bgColorMap = {
  741. info: '#2274A5',
  742. log: '#95B46A',
  743. warn: '#F5A623',
  744. error: '#D33F49'
  745. };
  746.  
  747. const printTime = this.printTime;
  748.  
  749. return function () {
  750. if (!window._debugMode_) {
  751. return false
  752. }
  753.  
  754. const msg = tipsMsg || 'debug message:';
  755.  
  756. const arg = Array.from(arguments);
  757. arg.unshift(`color: white; background-color: ${color || bgColorMap[name] || '#95B46A'}`);
  758.  
  759. if (printTime) {
  760. const curTime = new Date();
  761. const H = curTime.getHours();
  762. const M = curTime.getMinutes();
  763. const S = curTime.getSeconds();
  764. arg.unshift(`%c [${H}:${M}:${S}] ${msg} `);
  765. } else {
  766. arg.unshift(`%c ${msg} `);
  767. }
  768.  
  769. window.console[name].apply(window.console, arg);
  770. }
  771. }
  772.  
  773. isDebugMode () {
  774. return Boolean(window._debugMode_)
  775. }
  776. }
  777.  
  778. var Debug$1 = new Debug();
  779.  
  780. var debug = Debug$1.create('vueDebugHelper:');
  781.  
  782. /**
  783. * 打印生命周期信息
  784. * @param {Vue} vm vue组件实例
  785. * @param {string} lifeCycle vue生命周期名称
  786. * @returns
  787. */
  788. function printLifeCycle (vm, lifeCycle) {
  789. const lifeCycleConf = helper.config.lifecycle || { show: false, filters: ['created'], componentFilters: [] };
  790.  
  791. if (!vm || !lifeCycle || !lifeCycleConf.show) {
  792. return false
  793. }
  794.  
  795. const file = vm.options?.__file || vm.$options?.__file || '';
  796.  
  797. const { _componentTag, _componentName, _componentChain, _createdHumanTime, _uid } = vm;
  798. let info = `[${lifeCycle}] tag: ${_componentTag}, uid: ${_uid}, createdTime: ${_createdHumanTime}, chain: ${_componentChain}`;
  799.  
  800. if (file) {
  801. info += `, file: ${file}`;
  802. }
  803.  
  804. const matchComponentFilters = lifeCycleConf.componentFilters.length === 0 || filtersMatch(lifeCycleConf.componentFilters, _componentName);
  805. if (lifeCycleConf.filters.includes(lifeCycle) && matchComponentFilters) {
  806. debug.log(info);
  807. }
  808. }
  809.  
  810. function mixinRegister (Vue) {
  811. if (!Vue || !Vue.mixin) {
  812. debug.error('未检查到VUE对象,请检查是否引入了VUE,且将VUE对象挂载到全局变量window.Vue上');
  813. return false
  814. }
  815.  
  816. Vue.mixin({
  817. beforeCreate: function () {
  818. // const tag = this.$options?._componentTag || this.$vnode?.tag || this._uid
  819. helper.methods.initComponentInfo(this);
  820.  
  821. this._createdTime = Date.now();
  822. /* 增加人类方便查看的时间信息 */
  823. const timeObj = new Date(this._createdTime);
  824. this._createdHumanTime = `${timeObj.getHours()}:${timeObj.getMinutes()}:${timeObj.getSeconds()}`;
  825.  
  826. helper.components[this._uid] = this;
  827.  
  828. /**
  829. * 收集所有创建过的组件信息,此处只存储组件的基础信息,没销毁的组件会包含组件实例
  830. * 严禁对组件内其它对象进行引用,否则会导致组件实列无法被正常回收
  831. */
  832. const componentSummary = {
  833. uid: this._uid,
  834. name: this._componentName,
  835. tag: this._componentTag,
  836. createdTime: this._createdTime,
  837. createdHumanTime: this._createdHumanTime,
  838. // 0 表示还没被销毁
  839. destroyTime: 0,
  840. // 0 表示还没被销毁,duration可持续当当前查看时间
  841. duration: 0,
  842. component: this,
  843. chain: this._componentChain
  844. };
  845. helper.componentsSummary[this._uid] = componentSummary;
  846.  
  847. /* 添加到componentsSummaryStatistics里,生成统计信息 */
  848. Array.isArray(helper.componentsSummaryStatistics[this._componentName])
  849. ? helper.componentsSummaryStatistics[this._componentName].push(componentSummary)
  850. : (helper.componentsSummaryStatistics[this._componentName] = [componentSummary]);
  851.  
  852. printLifeCycle(this, 'beforeCreate');
  853. },
  854. created: function () {
  855. /* 增加空白数据,方便观察内存泄露情况 */
  856. if (helper.config.dd.enabled) {
  857. let needDd = false;
  858.  
  859. if (helper.config.dd.filters.length === 0) {
  860. needDd = true;
  861. } else {
  862. for (let index = 0; index < helper.config.dd.filters.length; index++) {
  863. const filter = helper.config.dd.filters[index];
  864. if (filter === this._componentName || String(this._componentName).endsWith(filter)) {
  865. needDd = true;
  866. break
  867. }
  868. }
  869. }
  870.  
  871. if (needDd) {
  872. const count = helper.config.dd.count * 1024;
  873. const componentInfo = `tag: ${this._componentTag}, uid: ${this._uid}, createdTime: ${this._createdHumanTime}`;
  874.  
  875. /* 此处必须使用JSON.stringify对产生的字符串进行消费,否则没法将内存占用上去 */
  876. this.$data.__dd__ = JSON.stringify(componentInfo + ' ' + helper.methods.createEmptyData(count, this._uid));
  877.  
  878. console.log(`[dd success] ${componentInfo} chain: ${this._componentChain}`);
  879. }
  880. }
  881.  
  882. printLifeCycle(this, 'created');
  883. },
  884. beforeMount: function () {
  885. printLifeCycle(this, 'beforeMount');
  886. },
  887. mounted: function () {
  888. printLifeCycle(this, 'mounted');
  889. },
  890. beforeUpdate: function () {
  891. printLifeCycle(this, 'beforeUpdate');
  892. },
  893. activated: function () {
  894. printLifeCycle(this, 'activated');
  895. },
  896. deactivated: function () {
  897. printLifeCycle(this, 'deactivated');
  898. },
  899. updated: function () {
  900. printLifeCycle(this, 'updated');
  901. },
  902. beforeDestroy: function () {
  903. printLifeCycle(this, 'beforeDestroy');
  904. },
  905. destroyed: function () {
  906. printLifeCycle(this, 'destroyed');
  907.  
  908. if (this._componentTag) {
  909. const uid = this._uid;
  910. const name = this._componentName;
  911. const destroyTime = Date.now();
  912.  
  913. /* helper里的componentSummary有可能通过调用clear函数而被清除掉,所以需进行判断再更新赋值 */
  914. const componentSummary = helper.componentsSummary[this._uid];
  915. if (componentSummary) {
  916. /* 补充/更新组件信息 */
  917. componentSummary.destroyTime = destroyTime;
  918. componentSummary.duration = destroyTime - this._createdTime;
  919.  
  920. helper.destroyList.push(componentSummary);
  921.  
  922. /* 统计被销毁的组件信息 */
  923. Array.isArray(helper.destroyStatistics[name])
  924. ? helper.destroyStatistics[name].push(componentSummary)
  925. : (helper.destroyStatistics[name] = [componentSummary]);
  926.  
  927. /* 删除已销毁的组件实例 */
  928. delete componentSummary.component;
  929. }
  930.  
  931. // 解除引用关系
  932. delete this._componentTag;
  933. delete this._componentChain;
  934. delete this._componentName;
  935. delete this._createdTime;
  936. delete this._createdHumanTime;
  937. delete this.$data.__dd__;
  938. delete helper.components[uid];
  939. } else {
  940. console.error('存在未被正常标记的组件,请检查组件采集逻辑是否需完善', this);
  941. }
  942. }
  943. });
  944. }
  945.  
  946. /*!
  947. * @name menuCommand.js
  948. * @version 0.0.1
  949. * @author Blaze
  950. * @date 2019/9/21 14:22
  951. */
  952.  
  953. const monkeyMenu = {
  954. menuIds: {},
  955. on (title, fn, accessKey) {
  956. if (window.GM_registerMenuCommand) {
  957. const menuId = window.GM_registerMenuCommand(title, fn, accessKey);
  958.  
  959. this.menuIds[menuId] = {
  960. title,
  961. fn,
  962. accessKey
  963. };
  964.  
  965. return menuId
  966. }
  967. },
  968.  
  969. off (id) {
  970. if (window.GM_unregisterMenuCommand) {
  971. delete this.menuIds[id];
  972. return window.GM_unregisterMenuCommand(id)
  973. }
  974. },
  975.  
  976. clear () {
  977. Object.keys(this.menuIds).forEach(id => {
  978. this.off(id);
  979. });
  980. },
  981.  
  982. /**
  983. * 通过菜单配置进行批量注册(不可用),注册(不可用)前会清空之前注册(不可用)过的所有菜单
  984. * @param {array|function} menuOpts 菜单配置,如果是函数则会调用该函数获取菜单配置,并且当菜单被点击后会重新创建菜单,实现菜单的动态更新
  985. */
  986. build (menuOpts) {
  987. this.clear();
  988.  
  989. if (Array.isArray(menuOpts)) {
  990. menuOpts.forEach(menu => {
  991. if (menu.disable === true) { return }
  992. this.on(menu.title, menu.fn, menu.accessKey);
  993. });
  994. } else if (menuOpts instanceof Function) {
  995. const menuList = menuOpts();
  996. if (Array.isArray(menuList)) {
  997. this._menuBuilder_ = menuOpts;
  998.  
  999. menuList.forEach(menu => {
  1000. if (menu.disable === true) { return }
  1001.  
  1002. const menuFn = () => {
  1003. try {
  1004. menu.fn.apply(menu, arguments);
  1005. } catch (e) {
  1006. console.error('[monkeyMenu]', menu.title, e);
  1007. }
  1008.  
  1009. // 每次菜单点击后,重新注册(不可用)菜单,这样可以确保菜单的状态是最新的
  1010. setTimeout(() => {
  1011. // console.log('[monkeyMenu rebuild]', menu.title)
  1012. this.build(this._menuBuilder_);
  1013. }, 100);
  1014. };
  1015.  
  1016. this.on(menu.title, menuFn, menu.accessKey);
  1017. });
  1018. } else {
  1019. console.error('monkeyMenu build error, no menuList return', menuOpts);
  1020. }
  1021. }
  1022. }
  1023. };
  1024.  
  1025. /**
  1026. * 简单的i18n库
  1027. */
  1028.  
  1029. class I18n {
  1030. constructor (config) {
  1031. this._languages = {};
  1032. this._locale = this.getClientLang();
  1033. this._defaultLanguage = '';
  1034. this.init(config);
  1035. }
  1036.  
  1037. init (config) {
  1038. if (!config) return false
  1039.  
  1040. const t = this;
  1041. t._locale = config.locale || t._locale;
  1042. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  1043. t._languages = config.languages || t._languages;
  1044. t._defaultLanguage = config.defaultLanguage || t._defaultLanguage;
  1045. }
  1046.  
  1047. use () {}
  1048.  
  1049. t (path) {
  1050. const t = this;
  1051. let result = t.getValByPath(t._languages[t._locale] || {}, path);
  1052.  
  1053. /* 版本回退 */
  1054. if (!result && t._locale !== t._defaultLanguage) {
  1055. result = t.getValByPath(t._languages[t._defaultLanguage] || {}, path);
  1056. }
  1057.  
  1058. return result || ''
  1059. }
  1060.  
  1061. /* 当前语言值 */
  1062. language () {
  1063. return this._locale
  1064. }
  1065.  
  1066. languages () {
  1067. return this._languages
  1068. }
  1069.  
  1070. changeLanguage (locale) {
  1071. if (this._languages[locale]) {
  1072. this._languages = locale;
  1073. return locale
  1074. } else {
  1075. return false
  1076. }
  1077. }
  1078.  
  1079. /**
  1080. * 根据文本路径获取对象里面的值
  1081. * @param obj {Object} -必选 要操作的对象
  1082. * @param path {String} -必选 路径信息
  1083. * @returns {*}
  1084. */
  1085. getValByPath (obj, path) {
  1086. path = path || '';
  1087. const pathArr = path.split('.');
  1088. let result = obj;
  1089.  
  1090. /* 递归提取结果值 */
  1091. for (let i = 0; i < pathArr.length; i++) {
  1092. if (!result) break
  1093. result = result[pathArr[i]];
  1094. }
  1095.  
  1096. return result
  1097. }
  1098.  
  1099. /* 获取客户端当前的语言环境 */
  1100. getClientLang () {
  1101. return navigator.languages ? navigator.languages[0] : navigator.language
  1102. }
  1103. }
  1104.  
  1105. var zhCN = {
  1106. help: '插件帮助中心',
  1107. about: '关于',
  1108. issues: '反馈',
  1109. setting: '设置',
  1110. hotkeys: '快捷键',
  1111. donate: '赞赏',
  1112. docs: '文档',
  1113. quit: '退出',
  1114. update: '更新',
  1115. refreshPage: '刷新页面',
  1116. debugHelper: {
  1117. viewVueDebugHelperObject: 'vueDebugHelper对象',
  1118. componentsStatistics: '当前存活组件统计',
  1119. destroyStatisticsSort: '已销毁组件统计',
  1120. componentsSummaryStatisticsSort: '全部组件混合统计',
  1121. getDestroyByDuration: '组件存活时间信息',
  1122. clearAll: '清空统计信息',
  1123. printLifeCycleInfo: '打印组件生命周期信息',
  1124. notPrintLifeCycleInfo: '取消组件生命周期信息打印',
  1125. printLifeCycleInfoPrompt: {
  1126. lifecycleFilters: '输入要打印的生命周期名称,多个可用,或|分隔,支持的值:beforeCreate|created|beforeMount|mounted|beforeUpdate|updated|activated|deactivated|beforeDestroy|destroyed',
  1127. componentFilters: '输入要打印的组件名称,多个可用,或|分隔,不输入则打印所有组件,字符串后面加*可执行模糊匹配'
  1128. },
  1129. findComponents: '查找组件',
  1130. findComponentsPrompt: {
  1131. filters: '输入要查找的组件名称,或uid,多个可用,或|分隔,字符串后面加*可执行模糊匹配'
  1132. },
  1133. findNotContainElementComponents: '查找不包含DOM对象的组件',
  1134. blockComponents: '阻断组件的创建',
  1135. blockComponentsPrompt: {
  1136. filters: '输入要阻断的组件名称,多个可用,或|分隔,输入为空则取消阻断,字符串后面加*可执行模糊匹配'
  1137. },
  1138. dd: '数据注入(dd)',
  1139. undd: '取消数据注入(undd)',
  1140. ddPrompt: {
  1141. filter: '组件过滤器(如果为空,则对所有组件注入)',
  1142. count: '指定注入数据的重复次数(默认1024)'
  1143. },
  1144. toggleHackVueComponent: '改写/还原Vue.component',
  1145. hackVueComponent: {
  1146. hack: '改写Vue.component',
  1147. unhack: '还原Vue.component'
  1148. },
  1149. toggleInspect: '切换Inspect',
  1150. inspectStatus: {
  1151. on: '开启Inspect',
  1152. off: '关闭Inspect'
  1153. },
  1154. togglePerformanceObserver: '开启/关闭性能观察',
  1155. performanceObserverStatus: {
  1156. on: '开启性能观察',
  1157. off: '关闭性能观察'
  1158. },
  1159. performanceObserverPrompt: {
  1160. entryTypes: '输入要观察的类型,多个类型可用,或|分隔,支持的类型有:element,navigation,resource,mark,measure,paint,longtask',
  1161. notSupport: '当前浏览器不支持性能观察'
  1162. },
  1163. enableAjaxCacheTips: '接口缓存功能已开启',
  1164. disableAjaxCacheTips: '接口缓存功能已关闭',
  1165. toggleAjaxCache: '开启/关闭接口缓存',
  1166. ajaxCacheStatus: {
  1167. on: '开启接口缓存',
  1168. off: '关闭接口缓存'
  1169. },
  1170. clearAjaxCache: '清空接口缓存数据',
  1171. clearAjaxCacheTips: '接口缓存数据已清空',
  1172. jaxCachePrompt: {
  1173. filters: '输入要缓存的接口地址,多个可用,或|分隔,字符串后面加*可执行模糊匹配',
  1174. expires: '输入缓存过期时间,单位为分钟,默认为1440分钟(即24小时)'
  1175. },
  1176. toggleBlockAjax: '开启/关闭接口请求拦截',
  1177. blockAjax: {
  1178. enabled: '开启接口请求拦截',
  1179. disable: '关闭接口请求拦截',
  1180. prompt: {
  1181. filters: '输入要拦截的接口地址,多个可用,或|分隔,字符串后面加*可执行模糊匹配'
  1182. }
  1183. },
  1184.  
  1185. measureSelectorInterval: '测量选择器时间差',
  1186. measureSelectorIntervalPrompt: {
  1187. selector1: '输入起始选择器',
  1188. selector2: '输入结束选择器'
  1189. },
  1190. selectorReadyTips: '元素已就绪',
  1191. devtools: {
  1192. enabled: '自动开启vue-devtools',
  1193. disable: '禁止开启vue-devtools'
  1194. },
  1195. simplifyMode: {
  1196. enabled: '简化右键菜单',
  1197. disable: '展开全部菜单'
  1198. }
  1199. },
  1200. contextMenu: {
  1201. consoleComponent: '查看组件',
  1202. consoleComponentData: '查看组件数据',
  1203. consoleComponentProps: '查看组件props',
  1204. consoleComponentChain: '查看组件调用链',
  1205. consoleParentComponent: '查看父组件',
  1206. componentAction: '相关操作',
  1207. copyFilePath: '复制文件路径',
  1208. copyComponentName: '复制组件名称',
  1209. copyComponentData: '复制组件$data',
  1210. copyComponentProps: '复制组件$props',
  1211. copyComponentTag: '复制组件标签',
  1212. copyComponentUid: '复制组件uid',
  1213. copyComponentChian: '复制组件调用链',
  1214. findComponents: '查找组件',
  1215. printLifeCycleInfo: '打印生命周期信息',
  1216. blockComponents: '阻断组件',
  1217. more: '更多'
  1218. }
  1219. };
  1220.  
  1221. var enUS = {
  1222. about: 'about',
  1223. issues: 'feedback',
  1224. setting: 'settings',
  1225. hotkeys: 'Shortcut keys',
  1226. donate: 'donate',
  1227. quit: 'quit',
  1228. refreshPage: 'Refresh the page',
  1229. debugHelper: {
  1230. viewVueDebugHelperObject: 'vueDebugHelper object',
  1231. componentsStatistics: 'Current surviving component statistics',
  1232. destroyStatisticsSort: 'Destroyed component statistics',
  1233. componentsSummaryStatisticsSort: 'All components mixed statistics',
  1234. getDestroyByDuration: 'Component survival time information',
  1235. clearAll: 'Clear statistics',
  1236. printLifeCycleInfo: 'Print component life cycle information',
  1237. notPrintLifeCycleInfo: 'Cancel the printing of component life cycle information',
  1238. printLifeCycleInfoPrompt: {
  1239. lifecycleFilters: 'Enter the lifecycle name to be printed, multiple available, or | separated, supported values: beforeCreate|created|beforeMount|mounted|beforeUpdate|updated|activated|deactivated|beforeDestroy|destroyed',
  1240. componentFilters: 'Enter the name of the component to be printed, multiple available, or | separated, if not input, print all components, add * after the string to perform fuzzy matching'
  1241. },
  1242. findComponents: 'Find Components',
  1243. findComponentsPrompt: {
  1244. filters: 'Enter the name of the component to find, or uid, multiple available, or | separated, followed by * to perform fuzzy matching'
  1245. },
  1246. findNotContainElementComponents: 'Find components that do not contain DOM objects',
  1247. blockComponents: 'Block the creation of components',
  1248. blockComponentsPrompt: {
  1249. filters: 'Enter the name of the component to be blocked, multiple available, or | separated, the input is empty to cancel the blocking, add * after the string to perform fuzzy matching'
  1250. },
  1251. dd: 'Data injection (dd)',
  1252. undd: 'Cancel data injection (undd)',
  1253. ddPrompt: {
  1254. filter: 'Component filter (if empty, inject all components)',
  1255. count: 'Specify the number of repetitions of injected data (default 1024)'
  1256. },
  1257. toggleHackVueComponent: 'Rewrite/restore Vue.component',
  1258. hackVueComponent: {
  1259. hack: 'Rewrite Vue.component',
  1260. unhack: 'Restore Vue.component'
  1261. },
  1262. toggleInspect: 'Toggle Inspect',
  1263. inspectStatus: {
  1264. on: 'Enable Inspect',
  1265. off: 'Turn off Inspect'
  1266. },
  1267. togglePerformanceObserver: 'Turn on/off performance observation',
  1268. performanceObserverStatus: {
  1269. on: 'Enable performance observation',
  1270. off: 'Turn off performance observation'
  1271. },
  1272. performanceObserverPrompt: {
  1273. entryTypes: 'Enter the type to be observed, multiple types are available, or | separated, the supported types are: element, navigation, resource, mark, measure, paint, longtask',
  1274. notSupport: 'The current browser does not support performance observation'
  1275. },
  1276. enableAjaxCacheTips: 'The interface cache function is enabled',
  1277. disableAjaxCacheTips: 'The interface cache function has been closed',
  1278. toggleAjaxCache: 'Enable/disable interface cache',
  1279. ajaxCacheStatus: {
  1280. on: 'Enable interface cache',
  1281. off: 'Turn off the interface cache'
  1282. },
  1283. clearAjaxCache: 'Clear interface cache data',
  1284. clearAjaxCacheTips: 'The interface cache data has been cleared',
  1285. jaxCachePrompt: {
  1286. filters: 'Enter the interface address to be cached, multiple available, or | separated, followed by * to perform fuzzy matching',
  1287. expires: 'Enter the cache expiration time in minutes, the default is 1440 minutes (ie 24 hours)'
  1288. },
  1289. measureSelectorInterval: 'Measure selector time difference',
  1290. measureSelectorIntervalPrompt: {
  1291. selector1: 'input start selector',
  1292. selector2: 'input end selector'
  1293. },
  1294. selectorReadyTips: 'The element is ready',
  1295. devtools: {
  1296. enabled: 'Automatically enable vue-devtools',
  1297. disable: 'Disable to enable vue-devtools'
  1298. }
  1299. },
  1300. contextMenu: {
  1301. consoleComponent: 'View component',
  1302. consoleComponentData: 'View component data',
  1303. consoleComponentProps: 'View component props',
  1304. consoleComponentChain: 'View the component call chain',
  1305. consoleParentComponent: 'View parent component',
  1306. componentAction: 'Related actions',
  1307. copyFilePath: 'Copy file path',
  1308. copyComponentName: 'Copy component name',
  1309. copyComponentData: 'Copy component $data',
  1310. copyComponentProps: 'Copy component $props',
  1311. copyComponentTag: 'Copy component tag',
  1312. copyComponentUid: 'Copy component uid',
  1313. copyComponentChian: 'Copy component call chain',
  1314. findComponents: 'Find Components',
  1315. printLifeCycleInfo: 'Print life cycle information',
  1316. blockComponents: 'Block Components'
  1317. }
  1318. };
  1319.  
  1320. var zhTW = {
  1321. about: '關於',
  1322. issues: '反饋',
  1323. setting: '設置',
  1324. hotkeys: '快捷鍵',
  1325. donate: '讚賞',
  1326. quit: '退出',
  1327. refreshPage: '刷新頁面',
  1328. debugHelper: {
  1329. viewVueDebugHelperObject: 'vueDebugHelper對象',
  1330. componentsStatistics: '當前存活組件統計',
  1331. destroyStatisticsSort: '已銷毀組件統計',
  1332. componentsSummaryStatisticsSort: '全部組件混合統計',
  1333. getDestroyByDuration: '組件存活時間信息',
  1334. clearAll: '清空統計信息',
  1335. printLifeCycleInfo: '打印組件生命週期信息',
  1336. notPrintLifeCycleInfo: '取消組件生命週期信息打印',
  1337. printLifeCycleInfoPrompt: {
  1338. lifecycleFilters: '輸入要打印的生命週期名稱,多個可用,或|分隔,支持的值:beforeCreate|created|beforeMount|mounted|beforeUpdate|updated|activated|deactivated|beforeDestroy|destroyed',
  1339. componentFilters: '輸入要打印的組件名稱,多個可用,或|分隔,不輸入則打印所有組件,字符串後面加*可執行模糊匹配'
  1340. },
  1341. findComponents: '查找組件',
  1342. findComponentsPrompt: {
  1343. filters: '輸入要查找的組件名稱,或uid,多個可用,或|分隔,字符串後面加*可執行模糊匹配'
  1344. },
  1345. findNotContainElementComponents: '查找不包含DOM對象的組件',
  1346. blockComponents: '阻斷組件的創建',
  1347. blockComponentsPrompt: {
  1348. filters: '輸入要阻斷的組件名稱,多個可用,或|分隔,輸入為空則取消阻斷,字符串後面加*可執行模糊匹配'
  1349. },
  1350. dd: '數據注入(dd)',
  1351. undd: '取消數據注入(undd)',
  1352. ddPrompt: {
  1353. filter: '組件過濾器(如果為空,則對所有組件注入)',
  1354. count: '指定注入數據的重複次數(默認1024)'
  1355. },
  1356. toggleHackVueComponent: '改寫/還原Vue.component',
  1357. hackVueComponent: {
  1358. hack: '改寫Vue.component',
  1359. unhack: '還原Vue.component'
  1360. },
  1361. toggleInspect: '切換Inspect',
  1362. inspectStatus: {
  1363. on: '開啟Inspect',
  1364. off: '關閉Inspect'
  1365. },
  1366. togglePerformanceObserver: '開啟/關閉性能觀察',
  1367. performanceObserverStatus: {
  1368. on: '開啟性能觀察',
  1369. off: '關閉性能觀察'
  1370. },
  1371. performanceObserverPrompt: {
  1372. entryTypes: '輸入要觀察的類型,多個類型可用,或|分隔,支持的類型有:element,navigation,resource,mark,measure,paint,longtask',
  1373. notSupport: '當前瀏覽器不支持性能觀察'
  1374. },
  1375. enableAjaxCacheTips: '接口緩存功能已開啟',
  1376. disableAjaxCacheTips: '接口緩存功能已關閉',
  1377. toggleAjaxCache: '開啟/關閉接口緩存',
  1378. ajaxCacheStatus: {
  1379. on: '開啟接口緩存',
  1380. off: '關閉接口緩存'
  1381. },
  1382. clearAjaxCache: '清空接口緩存數據',
  1383. clearAjaxCacheTips: '接口緩存數據已清空',
  1384. jaxCachePrompt: {
  1385. filters: '輸入要緩存的接口地址,多個可用,或|分隔,字符串後面加*可執行模糊匹配',
  1386. expires: '輸入緩存過期時間,單位為分鐘,默認為1440分鐘(即24小時)'
  1387. },
  1388. measureSelectorInterval: '測量選擇器時間差',
  1389. measureSelectorIntervalPrompt: {
  1390. selector1: '輸入起始選擇器',
  1391. selector2: '輸入結束選擇器'
  1392. },
  1393. selectorReadyTips: '元素已就緒',
  1394. devtools: {
  1395. enabled: '自動開啟vue-devtools',
  1396. disable: '禁止開啟vue-devtools'
  1397. }
  1398. },
  1399. contextMenu: {
  1400. consoleComponent: '查看組件',
  1401. consoleComponentData: '查看組件數據',
  1402. consoleComponentProps: '查看組件props',
  1403. consoleComponentChain: '查看組件調用鏈',
  1404. consoleParentComponent: '查看父組件',
  1405. componentAction: '相關操作',
  1406. copyFilePath: '複製文件路徑',
  1407. copyComponentName: '複製組件名稱',
  1408. copyComponentData: '複製組件$data',
  1409. copyComponentProps: '複製組件$props',
  1410. copyComponentTag: '複製組件標籤',
  1411. copyComponentUid: '複製組件uid',
  1412. copyComponentChian: '複製組件調用鏈',
  1413. findComponents: '查找組件',
  1414. printLifeCycleInfo: '打印生命週期信息',
  1415. blockComponents: '阻斷組件'
  1416. }
  1417. };
  1418.  
  1419. const messages = {
  1420. 'zh-CN': zhCN,
  1421. zh: zhCN,
  1422. 'zh-HK': zhTW,
  1423. 'zh-TW': zhTW,
  1424. 'en-US': enUS,
  1425. en: enUS,
  1426. };
  1427.  
  1428. /*!
  1429. * @name i18n.js
  1430. * @description vue-debug-helper的国际化配置
  1431. * @version 0.0.1
  1432. * @author xxxily
  1433. * @date 2022/04/26 14:56
  1434. * @github https://github.com/xxxily
  1435. */
  1436.  
  1437. const i18n = new I18n({
  1438. defaultLanguage: 'en',
  1439. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  1440. // locale: 'zh-TW',
  1441. languages: messages
  1442. });
  1443.  
  1444. /*!
  1445. * @name index.js
  1446. * @description hookJs JS AOP切面编程辅助库
  1447. * @version 0.0.1
  1448. * @author Blaze
  1449. * @date 2020/10/22 17:40
  1450. * @github https://github.com/xxxily
  1451. */
  1452.  
  1453. const win = typeof window === 'undefined' ? global : window;
  1454. const toStr = Function.prototype.call.bind(Object.prototype.toString);
  1455. /* 特殊场景,如果把Boolean也hook了,很容易导致调用溢出,所以是需要使用原生Boolean */
  1456. const toBoolean = Boolean.originMethod ? Boolean.originMethod : Boolean;
  1457. const util = {
  1458. toStr,
  1459. isObj: obj => toStr(obj) === '[object Object]',
  1460. /* 判断是否为引用类型,用于更宽泛的场景 */
  1461. isRef: obj => typeof obj === 'object',
  1462. isReg: obj => toStr(obj) === '[object RegExp]',
  1463. isFn: obj => obj instanceof Function,
  1464. isAsyncFn: fn => toStr(fn) === '[object AsyncFunction]',
  1465. isPromise: obj => toStr(obj) === '[object Promise]',
  1466. firstUpperCase: str => str.replace(/^\S/, s => s.toUpperCase()),
  1467. toArr: arg => Array.from(Array.isArray(arg) ? arg : [arg]),
  1468.  
  1469. debug: {
  1470. log () {
  1471. let log = win.console.log;
  1472. /* 如果log也被hook了,则使用未被hook前的log函数 */
  1473. if (log.originMethod) { log = log.originMethod; }
  1474. if (win._debugMode_) {
  1475. log.apply(win.console, arguments);
  1476. }
  1477. }
  1478. },
  1479. /* 获取包含自身、继承、可枚举、不可枚举的键名 */
  1480. getAllKeys (obj) {
  1481. const tmpArr = [];
  1482. for (const key in obj) { tmpArr.push(key); }
  1483. const allKeys = Array.from(new Set(tmpArr.concat(Reflect.ownKeys(obj))));
  1484. return allKeys
  1485. }
  1486. };
  1487.  
  1488. class HookJs {
  1489. constructor (useProxy) {
  1490. this.useProxy = useProxy || false;
  1491. this.hookPropertiesKeyName = '_hookProperties' + Date.now();
  1492. }
  1493.  
  1494. hookJsPro () {
  1495. return new HookJs(true)
  1496. }
  1497.  
  1498. _addHook (hookMethod, fn, type, classHook) {
  1499. const hookKeyName = type + 'Hooks';
  1500. const hookMethodProperties = hookMethod[this.hookPropertiesKeyName];
  1501. if (!hookMethodProperties[hookKeyName]) {
  1502. hookMethodProperties[hookKeyName] = [];
  1503. }
  1504.  
  1505. /* 注册(不可用)(储存)要被调用的hook函数,同时防止重复注册(不可用) */
  1506. let hasSameHook = false;
  1507. for (let i = 0; i < hookMethodProperties[hookKeyName].length; i++) {
  1508. if (fn === hookMethodProperties[hookKeyName][i]) {
  1509. hasSameHook = true;
  1510. break
  1511. }
  1512. }
  1513.  
  1514. if (!hasSameHook) {
  1515. fn.classHook = classHook || false;
  1516. hookMethodProperties[hookKeyName].push(fn);
  1517. }
  1518. }
  1519.  
  1520. _runHooks (parentObj, methodName, originMethod, hookMethod, target, ctx, args, classHook, hookPropertiesKeyName) {
  1521. const hookMethodProperties = hookMethod[hookPropertiesKeyName];
  1522. const beforeHooks = hookMethodProperties.beforeHooks || [];
  1523. const afterHooks = hookMethodProperties.afterHooks || [];
  1524. const errorHooks = hookMethodProperties.errorHooks || [];
  1525. const hangUpHooks = hookMethodProperties.hangUpHooks || [];
  1526. const replaceHooks = hookMethodProperties.replaceHooks || [];
  1527. const execInfo = {
  1528. result: null,
  1529. error: null,
  1530. args: args,
  1531. type: ''
  1532. };
  1533.  
  1534. function runHooks (hooks, type) {
  1535. let hookResult = null;
  1536. execInfo.type = type || '';
  1537. if (Array.isArray(hooks)) {
  1538. hooks.forEach(fn => {
  1539. if (util.isFn(fn) && classHook === fn.classHook) {
  1540. hookResult = fn(args, parentObj, methodName, originMethod, execInfo, ctx);
  1541. }
  1542. });
  1543. }
  1544. return hookResult
  1545. }
  1546.  
  1547. const runTarget = (function () {
  1548. if (classHook) {
  1549. return function () {
  1550. // eslint-disable-next-line new-cap
  1551. return new target(...args)
  1552. }
  1553. } else {
  1554. return function () {
  1555. return target.apply(ctx, args)
  1556. }
  1557. }
  1558. })();
  1559.  
  1560. const beforeHooksResult = runHooks(beforeHooks, 'before');
  1561. /* 支持终止后续调用的指令 */
  1562. if (beforeHooksResult && beforeHooksResult === 'STOP-INVOKE') {
  1563. return beforeHooksResult
  1564. }
  1565.  
  1566. if (hangUpHooks.length || replaceHooks.length) {
  1567. /**
  1568. * 当存在hangUpHooks或replaceHooks的时候是不会触发原来函数的
  1569. * 本质上来说hangUpHooks和replaceHooks是一样的,只是外部的定义描述不一致和分类不一致而已
  1570. */
  1571. runHooks(hangUpHooks, 'hangUp');
  1572. runHooks(replaceHooks, 'replace');
  1573. } else {
  1574. if (errorHooks.length) {
  1575. try {
  1576. execInfo.result = runTarget();
  1577. } catch (err) {
  1578. execInfo.error = err;
  1579. const errorHooksResult = runHooks(errorHooks, 'error');
  1580. /* 支持执行错误后不抛出异常的指令 */
  1581. if (errorHooksResult && errorHooksResult === 'SKIP-ERROR') ; else {
  1582. throw err
  1583. }
  1584. }
  1585. } else {
  1586. execInfo.result = runTarget();
  1587. }
  1588. }
  1589.  
  1590. /**
  1591. * 执行afterHooks,如果返回的是Promise,理论上应该进行进一步的细分处理
  1592. * 但添加细分处理逻辑后发现性能下降得比较厉害,且容易出现各种异常,所以决定不在hook里处理Promise情况
  1593. * 下面是原Promise处理逻辑,添加后会导致以下网站卡死或无法访问:
  1594. * wenku.baidu.com
  1595. * https://pubs.rsc.org/en/content/articlelanding/2021/sc/d1sc01881g#!divAbstract
  1596. * https://www.elsevier.com/connect/coronavirus-information-center
  1597. */
  1598. // if (execInfo.result && execInfo.result.then && util.isPromise(execInfo.result)) {
  1599. // execInfo.result.then(function (data) {
  1600. // execInfo.result = data
  1601. // runHooks(afterHooks, 'after')
  1602. // return Promise.resolve.apply(ctx, arguments)
  1603. // }).catch(function (err) {
  1604. // execInfo.error = err
  1605. // runHooks(errorHooks, 'error')
  1606. // return Promise.reject.apply(ctx, arguments)
  1607. // })
  1608. // }
  1609.  
  1610. runHooks(afterHooks, 'after');
  1611.  
  1612. return execInfo.result
  1613. }
  1614.  
  1615. _proxyMethodcGenerator (parentObj, methodName, originMethod, classHook, context, proxyHandler) {
  1616. const t = this;
  1617. const useProxy = t.useProxy;
  1618. let hookMethod = null;
  1619.  
  1620. /* 存在缓存则使用缓存的hookMethod */
  1621. if (t.isHook(originMethod)) {
  1622. hookMethod = originMethod;
  1623. } else if (originMethod[t.hookPropertiesKeyName] && t.isHook(originMethod[t.hookPropertiesKeyName].hookMethod)) {
  1624. hookMethod = originMethod[t.hookPropertiesKeyName].hookMethod;
  1625. }
  1626.  
  1627. if (hookMethod) {
  1628. if (!hookMethod[t.hookPropertiesKeyName].isHook) {
  1629. /* 重新标注被hook状态 */
  1630. hookMethod[t.hookPropertiesKeyName].isHook = true;
  1631. util.debug.log(`[hook method] ${util.toStr(parentObj)} ${methodName}`);
  1632. }
  1633. return hookMethod
  1634. }
  1635.  
  1636. /* 使用Proxy模式进行hook可以获得更多特性,但性能也会稍差一些 */
  1637. if (useProxy && Proxy) {
  1638. /* 注意:使用Proxy代理,hookMethod和originMethod将共用同一对象 */
  1639. const handler = { ...proxyHandler };
  1640.  
  1641. /* 下面的写法确定了proxyHandler是无法覆盖construct和apply操作的 */
  1642. if (classHook) {
  1643. handler.construct = function (target, args, newTarget) {
  1644. context = context || this;
  1645. return t._runHooks(parentObj, methodName, originMethod, hookMethod, target, context, args, true, t.hookPropertiesKeyName)
  1646. };
  1647. } else {
  1648. handler.apply = function (target, ctx, args) {
  1649. ctx = context || ctx;
  1650. return t._runHooks(parentObj, methodName, originMethod, hookMethod, target, ctx, args, false, t.hookPropertiesKeyName)
  1651. };
  1652. }
  1653.  
  1654. hookMethod = new Proxy(originMethod, handler);
  1655. } else {
  1656. hookMethod = function () {
  1657. /**
  1658. * 注意此处不能通过 context = context || this
  1659. * 然后通过把context当ctx传递过去
  1660. * 这将导致ctx引用错误
  1661. */
  1662. const ctx = context || this;
  1663. return t._runHooks(parentObj, methodName, originMethod, hookMethod, originMethod, ctx, arguments, classHook, t.hookPropertiesKeyName)
  1664. };
  1665.  
  1666. /* 确保子对象和原型链跟originMethod保持一致 */
  1667. const keys = Reflect.ownKeys(originMethod);
  1668. keys.forEach(keyName => {
  1669. try {
  1670. Object.defineProperty(hookMethod, keyName, {
  1671. get: function () {
  1672. return originMethod[keyName]
  1673. },
  1674. set: function (val) {
  1675. originMethod[keyName] = val;
  1676. }
  1677. });
  1678. } catch (err) {
  1679. // 设置defineProperty的时候出现异常,可能导致hookMethod部分功能确实,也可能不受影响
  1680. util.debug.log(`[proxyMethodcGenerator] hookMethod defineProperty abnormal. hookMethod:${methodName}, definePropertyName:${keyName}`, err);
  1681. }
  1682. });
  1683. hookMethod.prototype = originMethod.prototype;
  1684. }
  1685.  
  1686. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName] = {};
  1687.  
  1688. hookMethodProperties.originMethod = originMethod;
  1689. hookMethodProperties.hookMethod = hookMethod;
  1690. hookMethodProperties.isHook = true;
  1691. hookMethodProperties.classHook = classHook;
  1692.  
  1693. util.debug.log(`[hook method] ${util.toStr(parentObj)} ${methodName}`);
  1694.  
  1695. return hookMethod
  1696. }
  1697.  
  1698. _getObjKeysByRule (obj, rule) {
  1699. let excludeRule = null;
  1700. let result = rule;
  1701.  
  1702. if (util.isObj(rule) && rule.include) {
  1703. excludeRule = rule.exclude;
  1704. rule = rule.include;
  1705. result = rule;
  1706. }
  1707.  
  1708. /**
  1709. * for in、Object.keys与Reflect.ownKeys的区别见:
  1710. * https://es6.ruanyifeng.com/#docs/object#%E5%B1%9E%E6%80%A7%E7%9A%84%E9%81%8D%E5%8E%86
  1711. */
  1712. if (rule === '*') {
  1713. result = Object.keys(obj);
  1714. } else if (rule === '**') {
  1715. result = Reflect.ownKeys(obj);
  1716. } else if (rule === '***') {
  1717. result = util.getAllKeys(obj);
  1718. } else if (util.isReg(rule)) {
  1719. result = util.getAllKeys(obj).filter(keyName => rule.test(keyName));
  1720. }
  1721.  
  1722. /* 如果存在排除规则,则需要进行排除 */
  1723. if (excludeRule) {
  1724. result = Array.isArray(result) ? result : [result];
  1725. if (util.isReg(excludeRule)) {
  1726. result = result.filter(keyName => !excludeRule.test(keyName));
  1727. } else if (Array.isArray(excludeRule)) {
  1728. result = result.filter(keyName => !excludeRule.includes(keyName));
  1729. } else {
  1730. result = result.filter(keyName => excludeRule !== keyName);
  1731. }
  1732. }
  1733.  
  1734. return util.toArr(result)
  1735. }
  1736.  
  1737. /**
  1738. * 判断某个函数是否已经被hook
  1739. * @param fn {Function} -必选 要判断的函数
  1740. * @returns {boolean}
  1741. */
  1742. isHook (fn) {
  1743. if (!fn || !fn[this.hookPropertiesKeyName]) {
  1744. return false
  1745. }
  1746. const hookMethodProperties = fn[this.hookPropertiesKeyName];
  1747. return util.isFn(hookMethodProperties.originMethod) && fn !== hookMethodProperties.originMethod
  1748. }
  1749.  
  1750. /**
  1751. * 判断对象下的某个值是否具备hook的条件
  1752. * 注意:具备hook条件和能否直接修改值是两回事,
  1753. * 在进行hook的时候还要检查descriptor.writable是否为false
  1754. * 如果为false则要修改成true才能hook成功
  1755. * @param parentObj
  1756. * @param keyName
  1757. * @returns {boolean}
  1758. */
  1759. isAllowHook (parentObj, keyName) {
  1760. /* 有些对象会设置getter,让读取值的时候就抛错,所以需要try catch 判断能否正常读取属性 */
  1761. try { if (!parentObj[keyName]) return false } catch (e) { return false }
  1762. const descriptor = Object.getOwnPropertyDescriptor(parentObj, keyName);
  1763. return !(descriptor && descriptor.configurable === false)
  1764. }
  1765.  
  1766. /**
  1767. * hook 核心函数
  1768. * @param parentObj {Object} -必选 被hook函数依赖的父对象
  1769. * @param hookMethods {Object|Array|RegExp|string} -必选 被hook函数的函数名或函数名的匹配规则
  1770. * @param fn {Function} -必选 hook之后的回调方法
  1771. * @param type {String} -可选 默认before,指定运行hook函数回调的时机,可选字符串:before、after、replace、error、hangUp
  1772. * @param classHook {Boolean} -可选 默认false,指定是否为针对new(class)操作的hook
  1773. * @param context {Object} -可选 指定运行被hook函数时的上下文对象
  1774. * @param proxyHandler {Object} -可选 仅当用Proxy进行hook时有效,默认使用的是Proxy的apply handler进行hook,如果你有特殊需求也可以配置自己的handler以实现更复杂的功能
  1775. * 附注:不使用Proxy进行hook,可以获得更高性能,但也意味着通用性更差些,对于要hook HTMLElement.prototype、EventTarget.prototype这些对象里面的非实例的函数往往会失败而导致被hook函数执行出错
  1776. * @returns {boolean}
  1777. */
  1778. hook (parentObj, hookMethods, fn, type, classHook, context, proxyHandler) {
  1779. classHook = toBoolean(classHook);
  1780. type = type || 'before';
  1781.  
  1782. if ((!util.isRef(parentObj) && !util.isFn(parentObj)) || !util.isFn(fn) || !hookMethods) {
  1783. return false
  1784. }
  1785.  
  1786. const t = this;
  1787.  
  1788. hookMethods = t._getObjKeysByRule(parentObj, hookMethods);
  1789. hookMethods.forEach(methodName => {
  1790. if (!t.isAllowHook(parentObj, methodName)) {
  1791. util.debug.log(`${util.toStr(parentObj)} [${methodName}] does not support modification`);
  1792. return false
  1793. }
  1794.  
  1795. const descriptor = Object.getOwnPropertyDescriptor(parentObj, methodName);
  1796. if (descriptor && descriptor.writable === false) {
  1797. Object.defineProperty(parentObj, methodName, { writable: true });
  1798. }
  1799.  
  1800. const originMethod = parentObj[methodName];
  1801. let hookMethod = null;
  1802.  
  1803. /* 非函数无法进行hook操作 */
  1804. if (!util.isFn(originMethod)) {
  1805. return false
  1806. }
  1807.  
  1808. hookMethod = t._proxyMethodcGenerator(parentObj, methodName, originMethod, classHook, context, proxyHandler);
  1809.  
  1810. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName];
  1811. if (hookMethodProperties.classHook !== classHook) {
  1812. util.debug.log(`${util.toStr(parentObj)} [${methodName}] Cannot support functions hook and classes hook at the same time `);
  1813. return false
  1814. }
  1815.  
  1816. /* 使用hookMethod接管需要被hook的方法 */
  1817. if (parentObj[methodName] !== hookMethod) {
  1818. parentObj[methodName] = hookMethod;
  1819. }
  1820.  
  1821. t._addHook(hookMethod, fn, type, classHook);
  1822. });
  1823. }
  1824.  
  1825. /* 专门针对new操作的hook,本质上是hook函数的别名,可以少传classHook这个参数,并且明确语义 */
  1826. hookClass (parentObj, hookMethods, fn, type, context, proxyHandler) {
  1827. return this.hook(parentObj, hookMethods, fn, type, true, context, proxyHandler)
  1828. }
  1829.  
  1830. /**
  1831. * 取消对某个函数的hook
  1832. * @param parentObj {Object} -必选 要取消被hook函数依赖的父对象
  1833. * @param hookMethods {Object|Array|RegExp|string} -必选 要取消被hook函数的函数名或函数名的匹配规则
  1834. * @param type {String} -可选 默认before,指定要取消的hook类型,可选字符串:before、after、replace、error、hangUp,如果不指定该选项则取消所有类型下的所有回调
  1835. * @param fn {Function} -必选 取消指定的hook回调函数,如果不指定该选项则取消对应type类型下的所有回调
  1836. * @returns {boolean}
  1837. */
  1838. unHook (parentObj, hookMethods, type, fn) {
  1839. if (!util.isRef(parentObj) || !hookMethods) {
  1840. return false
  1841. }
  1842.  
  1843. const t = this;
  1844. hookMethods = t._getObjKeysByRule(parentObj, hookMethods);
  1845. hookMethods.forEach(methodName => {
  1846. if (!t.isAllowHook(parentObj, methodName)) {
  1847. return false
  1848. }
  1849.  
  1850. const hookMethod = parentObj[methodName];
  1851.  
  1852. if (!t.isHook(hookMethod)) {
  1853. return false
  1854. }
  1855.  
  1856. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName];
  1857. const originMethod = hookMethodProperties.originMethod;
  1858.  
  1859. if (type) {
  1860. const hookKeyName = type + 'Hooks';
  1861. const hooks = hookMethodProperties[hookKeyName] || [];
  1862.  
  1863. if (fn) {
  1864. /* 删除指定类型下的指定hook函数 */
  1865. for (let i = 0; i < hooks.length; i++) {
  1866. if (fn === hooks[i]) {
  1867. hookMethodProperties[hookKeyName].splice(i, 1);
  1868. util.debug.log(`[unHook ${hookKeyName} func] ${util.toStr(parentObj)} ${methodName}`, fn);
  1869. break
  1870. }
  1871. }
  1872. } else {
  1873. /* 删除指定类型下的所有hook函数 */
  1874. if (Array.isArray(hookMethodProperties[hookKeyName])) {
  1875. hookMethodProperties[hookKeyName] = [];
  1876. util.debug.log(`[unHook all ${hookKeyName}] ${util.toStr(parentObj)} ${methodName}`);
  1877. }
  1878. }
  1879. } else {
  1880. /* 彻底还原被hook的函数 */
  1881. if (util.isFn(originMethod)) {
  1882. parentObj[methodName] = originMethod;
  1883. delete parentObj[methodName][t.hookPropertiesKeyName];
  1884.  
  1885. // Object.keys(hookMethod).forEach(keyName => {
  1886. // if (/Hooks$/.test(keyName) && Array.isArray(hookMethod[keyName])) {
  1887. // hookMethod[keyName] = []
  1888. // }
  1889. // })
  1890. //
  1891. // hookMethod.isHook = false
  1892. // parentObj[methodName] = originMethod
  1893. // delete parentObj[methodName].originMethod
  1894. // delete parentObj[methodName].hookMethod
  1895. // delete parentObj[methodName].isHook
  1896. // delete parentObj[methodName].isClassHook
  1897.  
  1898. util.debug.log(`[unHook method] ${util.toStr(parentObj)} ${methodName}`);
  1899. }
  1900. }
  1901. });
  1902. }
  1903.  
  1904. /* 源函数运行前的hook */
  1905. before (obj, hookMethods, fn, classHook, context, proxyHandler) {
  1906. return this.hook(obj, hookMethods, fn, 'before', classHook, context, proxyHandler)
  1907. }
  1908.  
  1909. /* 源函数运行后的hook */
  1910. after (obj, hookMethods, fn, classHook, context, proxyHandler) {
  1911. return this.hook(obj, hookMethods, fn, 'after', classHook, context, proxyHandler)
  1912. }
  1913.  
  1914. /* 替换掉要hook的函数,不再运行源函数,换成运行其他逻辑 */
  1915. replace (obj, hookMethods, fn, classHook, context, proxyHandler) {
  1916. return this.hook(obj, hookMethods, fn, 'replace', classHook, context, proxyHandler)
  1917. }
  1918.  
  1919. /* 源函数运行出错时的hook */
  1920. error (obj, hookMethods, fn, classHook, context, proxyHandler) {
  1921. return this.hook(obj, hookMethods, fn, 'error', classHook, context, proxyHandler)
  1922. }
  1923.  
  1924. /* 底层实现逻辑与replace一样,都是替换掉要hook的函数,不再运行源函数,只不过是为了明确语义,将源函数挂起不再执行,原则上也不再执行其他逻辑,如果要执行其他逻辑请使用replace hook */
  1925. hangUp (obj, hookMethods, fn, classHook, context, proxyHandler) {
  1926. return this.hook(obj, hookMethods, fn, 'hangUp', classHook, context, proxyHandler)
  1927. }
  1928. }
  1929.  
  1930. var hookJs = new HookJs();
  1931.  
  1932. /*!
  1933. * @name vueHooks.js
  1934. * @description 对Vue对象进行的hooks封装
  1935. * @version 0.0.1
  1936. * @author xxxily
  1937. * @date 2022/05/10 14:11
  1938. * @github https://github.com/xxxily
  1939. */
  1940.  
  1941. const hookJsPro = hookJs.hookJsPro();
  1942.  
  1943. let vueComponentHook = null;
  1944.  
  1945. const vueHooks = {
  1946. /* 对extend进行hooks封装,以便进行组件阻断 */
  1947. blockComponents (Vue, config) {
  1948. hookJsPro.before(Vue, 'extend', (args, parentObj, methodName, originMethod, execInfo, ctx) => {
  1949. const extendOpts = args[0];
  1950. // extendOpts.__file && debug.info(`[extendOptions:${extendOpts.name}]`, extendOpts.__file)
  1951.  
  1952. const hasBlockFilter = config.blockFilters && config.blockFilters.length;
  1953. if (hasBlockFilter && extendOpts.name && filtersMatch(config.blockFilters, extendOpts.name)) {
  1954. debug.info(`[block component]: name: ${extendOpts.name}`);
  1955. return 'STOP-INVOKE'
  1956. }
  1957. });
  1958.  
  1959. /* 禁止因为阻断组件的创建而导致的错误提示输出,减少不必要的信息噪音 */
  1960. hookJsPro.before(Vue.util, 'warn', (args) => {
  1961. const msg = args[0];
  1962. if (msg.includes('STOP-INVOKE')) {
  1963. return 'STOP-INVOKE'
  1964. }
  1965. });
  1966. },
  1967.  
  1968. hackVueComponent (Vue, callback) {
  1969. if (vueComponentHook) {
  1970. debug.warn('[Vue.component] you have already hacked');
  1971. return
  1972. }
  1973.  
  1974. vueComponentHook = (args, parentObj, methodName, originMethod, execInfo, ctx) => {
  1975. const name = args[0];
  1976. const opts = args[1];
  1977.  
  1978. if (callback instanceof Function) {
  1979. callback.apply(Vue, args);
  1980. } else {
  1981. /* 打印全局组件的注册(不可用)信息 */
  1982. if (Vue.options.components[name]) {
  1983. debug.warn(`[Vue.component][REPEAT][old-cid:${Vue.options.components[name].cid}]`, name, opts);
  1984. } else {
  1985. debug.log('[Vue.component]', name, opts);
  1986. }
  1987. }
  1988. };
  1989.  
  1990. hookJsPro.before(Vue, 'component', vueComponentHook);
  1991. debug.log(i18n.t('debugHelper.hackVueComponent.hack') + ' (success)');
  1992. },
  1993.  
  1994. unHackVueComponent (Vue) {
  1995. if (vueComponentHook) {
  1996. hookJsPro.unHook(Vue, 'component', 'before', vueComponentHook);
  1997. vueComponentHook = null;
  1998. debug.log(i18n.t('debugHelper.hackVueComponent.unhack') + ' (success)');
  1999. } else {
  2000. debug.warn('[Vue.component] you have not hack vue component, not need to unhack');
  2001. }
  2002. }
  2003. };
  2004.  
  2005. /*
  2006. * author: wendux
  2007. * email: 824783146@qq.com
  2008. * source code: https://github.com/wendux/Ajax-hook
  2009. */
  2010.  
  2011. // Save original XMLHttpRequest as _rxhr
  2012. var realXhr = '_rxhr';
  2013.  
  2014. var events = ['load', 'loadend', 'timeout', 'error', 'readystatechange', 'abort'];
  2015.  
  2016. function configEvent (event, xhrProxy) {
  2017. var e = {};
  2018. for (var attr in event) e[attr] = event[attr];
  2019. // xhrProxy instead
  2020. e.target = e.currentTarget = xhrProxy;
  2021. return e
  2022. }
  2023.  
  2024. function hook (proxy, win) {
  2025. win = win || window;
  2026. // Avoid double hookAjax
  2027. win[realXhr] = win[realXhr] || win.XMLHttpRequest;
  2028.  
  2029. win.XMLHttpRequest = function () {
  2030. // We shouldn't hookAjax XMLHttpRequest.prototype because we can't
  2031. // guarantee that all attributes are on the prototype。
  2032. // Instead, hooking XMLHttpRequest instance can avoid this problem.
  2033.  
  2034. var xhr = new win[realXhr]();
  2035.  
  2036. // Generate all callbacks(eg. onload) are enumerable (not undefined).
  2037. for (var i = 0; i < events.length; ++i) {
  2038. if (xhr[events[i]] === undefined) xhr[events[i]] = null;
  2039. }
  2040.  
  2041. for (var attr in xhr) {
  2042. var type = '';
  2043. try {
  2044. type = typeof xhr[attr]; // May cause exception on some browser
  2045. } catch (e) {
  2046. }
  2047. if (type === 'function') {
  2048. // hookAjax methods of xhr, such as `open`、`send` ...
  2049. this[attr] = hookFunction(attr);
  2050. } else {
  2051. Object.defineProperty(this, attr, {
  2052. get: getterFactory(attr),
  2053. set: setterFactory(attr),
  2054. enumerable: true
  2055. });
  2056. }
  2057. }
  2058. var that = this;
  2059. xhr.getProxy = function () {
  2060. return that
  2061. };
  2062. this.xhr = xhr;
  2063. };
  2064.  
  2065. Object.assign(win.XMLHttpRequest, { UNSENT: 0, OPENED: 1, HEADERS_RECEIVED: 2, LOADING: 3, DONE: 4 });
  2066.  
  2067. // Generate getter for attributes of xhr
  2068. function getterFactory (attr) {
  2069. return function () {
  2070. var v = this.hasOwnProperty(attr + '_') ? this[attr + '_'] : this.xhr[attr];
  2071. var attrGetterHook = (proxy[attr] || {}).getter;
  2072. return attrGetterHook && attrGetterHook(v, this) || v
  2073. }
  2074. }
  2075.  
  2076. // Generate setter for attributes of xhr; by this we have an opportunity
  2077. // to hookAjax event callbacks (eg: `onload`) of xhr;
  2078. function setterFactory (attr) {
  2079. return function (v) {
  2080. var xhr = this.xhr;
  2081. var that = this;
  2082. var hook = proxy[attr];
  2083. // hookAjax event callbacks such as `onload`、`onreadystatechange`...
  2084. if (attr.substring(0, 2) === 'on') {
  2085. that[attr + '_'] = v;
  2086. xhr[attr] = function (e) {
  2087. e = configEvent(e, that);
  2088. var ret = proxy[attr] && proxy[attr].call(that, xhr, e);
  2089. ret || v.call(that, e);
  2090. };
  2091. } else {
  2092. // If the attribute isn't writable, generate proxy attribute
  2093. var attrSetterHook = (hook || {}).setter;
  2094. v = attrSetterHook && attrSetterHook(v, that) || v;
  2095. this[attr + '_'] = v;
  2096. try {
  2097. // Not all attributes of xhr are writable(setter may undefined).
  2098. xhr[attr] = v;
  2099. } catch (e) {
  2100. }
  2101. }
  2102. }
  2103. }
  2104.  
  2105. // Hook methods of xhr.
  2106. function hookFunction (fun) {
  2107. return function () {
  2108. var args = [].slice.call(arguments);
  2109. if (proxy[fun]) {
  2110. var ret = proxy[fun].call(this, args, this.xhr);
  2111. // If the proxy return value exists, return it directly,
  2112. // otherwise call the function of xhr.
  2113. if (ret) return ret
  2114. }
  2115. return this.xhr[fun].apply(this.xhr, args)
  2116. }
  2117. }
  2118.  
  2119. // Return the real XMLHttpRequest
  2120. return win[realXhr]
  2121. }
  2122.  
  2123. function unHook (win) {
  2124. win = win || window;
  2125. if (win[realXhr]) win.XMLHttpRequest = win[realXhr];
  2126. win[realXhr] = undefined;
  2127. }
  2128.  
  2129. /*
  2130. * author: wendux
  2131. * email: 824783146@qq.com
  2132. * source code: https://github.com/wendux/Ajax-hook
  2133. */
  2134.  
  2135. var eventLoad = events[0];
  2136. var eventLoadEnd = events[1];
  2137. var eventTimeout = events[2];
  2138. var eventError = events[3];
  2139. var eventReadyStateChange = events[4];
  2140. var eventAbort = events[5];
  2141.  
  2142. var singleton;
  2143. var prototype = 'prototype';
  2144.  
  2145. function proxy (proxy, win) {
  2146. if (singleton) {
  2147. throw new Error('Proxy already exists')
  2148. }
  2149.  
  2150. singleton = new Proxy$1(proxy, win);
  2151. return singleton
  2152. }
  2153.  
  2154. function unProxy (win) {
  2155. singleton = null;
  2156. unHook(win);
  2157. }
  2158.  
  2159. function trim (str) {
  2160. return str.replace(/^\s+|\s+$/g, '')
  2161. }
  2162.  
  2163. function getEventTarget (xhr) {
  2164. return xhr.watcher || (xhr.watcher = document.createElement('a'))
  2165. }
  2166.  
  2167. function triggerListener (xhr, name) {
  2168. var xhrProxy = xhr.getProxy();
  2169. var callback = 'on' + name + '_';
  2170. var event = configEvent({ type: name }, xhrProxy);
  2171. xhrProxy[callback] && xhrProxy[callback](event);
  2172. var evt;
  2173. if (typeof (Event) === 'function') {
  2174. evt = new Event(name, { bubbles: false });
  2175. } else {
  2176. // https://stackoverflow.com/questions/27176983/dispatchevent-not-working-in-ie11
  2177. evt = document.createEvent('Event');
  2178. evt.initEvent(name, false, true);
  2179. }
  2180. getEventTarget(xhr).dispatchEvent(evt);
  2181. }
  2182.  
  2183. function Handler (xhr) {
  2184. this.xhr = xhr;
  2185. this.xhrProxy = xhr.getProxy();
  2186. }
  2187.  
  2188. Handler[prototype] = Object.create({
  2189. resolve: function resolve (response) {
  2190. var xhrProxy = this.xhrProxy;
  2191. var xhr = this.xhr;
  2192. xhrProxy.readyState = 4;
  2193. xhr.resHeader = response.headers;
  2194. xhrProxy.response = xhrProxy.responseText = response.response;
  2195. xhrProxy.statusText = response.statusText;
  2196. xhrProxy.status = response.status;
  2197. triggerListener(xhr, eventReadyStateChange);
  2198. triggerListener(xhr, eventLoad);
  2199. triggerListener(xhr, eventLoadEnd);
  2200. },
  2201. reject: function reject (error) {
  2202. this.xhrProxy.status = 0;
  2203. triggerListener(this.xhr, error.type);
  2204. triggerListener(this.xhr, eventLoadEnd);
  2205. }
  2206. });
  2207.  
  2208. function makeHandler (next) {
  2209. function sub (xhr) {
  2210. Handler.call(this, xhr);
  2211. }
  2212.  
  2213. sub[prototype] = Object.create(Handler[prototype]);
  2214. sub[prototype].next = next;
  2215. return sub
  2216. }
  2217.  
  2218. var RequestHandler = makeHandler(function (rq) {
  2219. var xhr = this.xhr;
  2220. rq = rq || xhr.config;
  2221. xhr.withCredentials = rq.withCredentials;
  2222. xhr.open(rq.method, rq.url, rq.async !== false, rq.user, rq.password);
  2223. for (var key in rq.headers) {
  2224. xhr.setRequestHeader(key, rq.headers[key]);
  2225. }
  2226. xhr.send(rq.body);
  2227. });
  2228.  
  2229. var ResponseHandler = makeHandler(function (response) {
  2230. this.resolve(response);
  2231. });
  2232.  
  2233. var ErrorHandler = makeHandler(function (error) {
  2234. this.reject(error);
  2235. });
  2236.  
  2237. function Proxy$1 (proxy, win) {
  2238. var onRequest = proxy.onRequest;
  2239. var onResponse = proxy.onResponse;
  2240. var onError = proxy.onError;
  2241.  
  2242. function handleResponse (xhr, xhrProxy) {
  2243. var handler = new ResponseHandler(xhr);
  2244. var ret = {
  2245. response: xhrProxy.response,
  2246. status: xhrProxy.status,
  2247. statusText: xhrProxy.statusText,
  2248. config: xhr.config,
  2249. headers: xhr.resHeader || xhr.getAllResponseHeaders().split('\r\n').reduce(function (ob, str) {
  2250. if (str === '') return ob
  2251. var m = str.split(':');
  2252. ob[m.shift()] = trim(m.join(':'));
  2253. return ob
  2254. }, {})
  2255. };
  2256. if (!onResponse) return handler.resolve(ret)
  2257. onResponse(ret, handler);
  2258. }
  2259.  
  2260. function onerror (xhr, xhrProxy, error, errorType) {
  2261. var handler = new ErrorHandler(xhr);
  2262. error = { config: xhr.config, error: error, type: errorType };
  2263. if (onError) {
  2264. onError(error, handler);
  2265. } else {
  2266. handler.next(error);
  2267. }
  2268. }
  2269.  
  2270. function preventXhrProxyCallback () {
  2271. return true
  2272. }
  2273.  
  2274. function errorCallback (errorType) {
  2275. return function (xhr, e) {
  2276. onerror(xhr, this, e, errorType);
  2277. return true
  2278. }
  2279. }
  2280.  
  2281. function stateChangeCallback (xhr, xhrProxy) {
  2282. if (xhr.readyState === 4 && xhr.status !== 0) {
  2283. handleResponse(xhr, xhrProxy);
  2284. } else if (xhr.readyState !== 4) {
  2285. triggerListener(xhr, eventReadyStateChange);
  2286. }
  2287. return true
  2288. }
  2289.  
  2290. return hook({
  2291. onload: preventXhrProxyCallback,
  2292. onloadend: preventXhrProxyCallback,
  2293. onerror: errorCallback(eventError),
  2294. ontimeout: errorCallback(eventTimeout),
  2295. onabort: errorCallback(eventAbort),
  2296. onreadystatechange: function (xhr) {
  2297. return stateChangeCallback(xhr, this)
  2298. },
  2299. open: function open (args, xhr) {
  2300. var _this = this;
  2301. var config = xhr.config = { headers: {} };
  2302. config.method = args[0];
  2303. config.url = args[1];
  2304. config.async = args[2];
  2305. config.user = args[3];
  2306. config.password = args[4];
  2307. config.xhr = xhr;
  2308. var evName = 'on' + eventReadyStateChange;
  2309. if (!xhr[evName]) {
  2310. xhr[evName] = function () {
  2311. return stateChangeCallback(xhr, _this)
  2312. };
  2313. }
  2314.  
  2315. // 如果有请求拦截器,则在调用onRequest后再打开链接。因为onRequest最佳调用时机是在send前,
  2316. // 所以我们在send拦截函数中再手动调用open,因此返回true阻止xhr.open调用。
  2317. //
  2318. // 如果没有请求拦截器,则不用阻断xhr.open调用
  2319. if (onRequest) return true
  2320. },
  2321. send: function (args, xhr) {
  2322. var config = xhr.config;
  2323. config.withCredentials = xhr.withCredentials;
  2324. config.body = args[0];
  2325. if (onRequest) {
  2326. // In 'onRequest', we may call XHR's event handler, such as `xhr.onload`.
  2327. // However, XHR's event handler may not be set until xhr.send is called in
  2328. // the user's code, so we use `setTimeout` to avoid this situation
  2329. var req = function () {
  2330. onRequest(config, new RequestHandler(xhr));
  2331. };
  2332. config.async === false ? req() : setTimeout(req);
  2333. return true
  2334. }
  2335. },
  2336. setRequestHeader: function (args, xhr) {
  2337. // Collect request headers
  2338. xhr.config.headers[args[0].toLowerCase()] = args[1];
  2339. return true
  2340. },
  2341. addEventListener: function (args, xhr) {
  2342. var _this = this;
  2343. if (events.indexOf(args[0]) !== -1) {
  2344. var handler = args[1];
  2345. getEventTarget(xhr).addEventListener(args[0], function (e) {
  2346. var event = configEvent(e, _this);
  2347. event.type = args[0];
  2348. event.isTrusted = true;
  2349. handler.call(_this, event);
  2350. });
  2351. return true
  2352. }
  2353. },
  2354. getAllResponseHeaders: function (_, xhr) {
  2355. var headers = xhr.resHeader;
  2356. if (headers) {
  2357. var header = '';
  2358. for (var key in headers) {
  2359. header += key + ': ' + headers[key] + '\r\n';
  2360. }
  2361. return header
  2362. }
  2363. },
  2364. getResponseHeader: function (args, xhr) {
  2365. var headers = xhr.resHeader;
  2366. if (headers) {
  2367. return headers[(args[0] || '').toLowerCase()]
  2368. }
  2369. }
  2370. }, win)
  2371. }
  2372.  
  2373. /*!
  2374. * @name fetch-proxy.js
  2375. * @description fetch请求hook,用法保持跟 https://github.com/wendux/Ajax-hook 的xhr-proxy一致,以便支持fetch请求的监听和修改
  2376. * @version 0.0.1
  2377. * @author xxxily
  2378. * @date 2022/05/20 16:18
  2379. * @github https://github.com/xxxily
  2380. */
  2381.  
  2382. /**
  2383. * 虽然此库用法保持跟Ajax-hook一致,但由于fetch最终对请求结果的消费方式与XMLHttpRequest不一样,
  2384. * 所以在进行hook操作时必须加以区分
  2385. *
  2386. * 具体请参考:
  2387. * https://www.ruanyifeng.com/blog/2020/12/fetch-tutorial.html
  2388. *
  2389. * 为了区别判断,将在onRequest, onResponse, onError的第三个参数标识是否为fetch请求,如果为true,则说明是是fetch请求
  2390. * 然后按需对fetch对象进行区分处理即可
  2391. */
  2392.  
  2393. const realFetch = '_rFetch_';
  2394.  
  2395. function makeHandler$1 (resolve, reject, next) {
  2396. return Object.create({
  2397. resolve,
  2398. reject,
  2399. next
  2400. })
  2401. }
  2402.  
  2403. function fetchProxy (proxy = {}, win) {
  2404. win = win || window;
  2405. win[realFetch] = win[realFetch] || win.fetch;
  2406.  
  2407. const { onRequest, onResponse, onError } = proxy;
  2408.  
  2409. function customFetch () {
  2410. /**
  2411. * 提前锁定fetch,防止在onRequest进行异步操作时,
  2412. * 外部触发了unFetchHook,再去找win[realFetch]已经查无此fetch了
  2413. */
  2414. const fetch = win[realFetch] || win.fetch;
  2415.  
  2416. const t = this;
  2417. let fetchResolve = function () {};
  2418. let fetchReject = function () {};
  2419. const args = arguments;
  2420. const config = args[1] || {};
  2421.  
  2422. /* 保持config参数结构跟Ajax-hook一致 */
  2423. config.url = args[0];
  2424. config.headers = config.headers || {};
  2425. if (!config.method) {
  2426. config.method = 'GET';
  2427. } else {
  2428. config.method = config.method.toUpperCase();
  2429. }
  2430.  
  2431. /* 发起真实请求 */
  2432. async function gotoFetch (config) {
  2433. const url = config.url;
  2434. // delete config.url
  2435. const args = [url, config];
  2436.  
  2437. if (fetch === customFetch) {
  2438. throw new Error('[fetch loop] fetch is equal to customFetch')
  2439. }
  2440.  
  2441. const response = await fetch.apply(t, args).catch((err) => {
  2442. if (onError instanceof Function) {
  2443. const errorHandler = makeHandler$1(fetchResolve, fetchReject, function (err) { fetchReject(err); });
  2444. onError(err, errorHandler, true);
  2445. } else {
  2446. throw err
  2447. }
  2448. });
  2449.  
  2450. if (onResponse instanceof Function) {
  2451. const responseHandler = makeHandler$1(fetchResolve, fetchReject, function (response) { fetchResolve(response); });
  2452.  
  2453. response.config = config;
  2454. onResponse(response, responseHandler, true);
  2455. } else {
  2456. /* 完成请求 */
  2457. fetchResolve(response);
  2458. }
  2459. }
  2460.  
  2461. /* 判断由谁来发起真实的请求 */
  2462. if (onRequest instanceof Function) {
  2463. const requestHandler = makeHandler$1(fetchResolve, fetchReject, function (config) { gotoFetch(config); });
  2464. onRequest(config, requestHandler, true);
  2465. } else {
  2466. gotoFetch(config);
  2467. }
  2468.  
  2469. /* 返回个空的promise,让gotoFetch进行真实的请求处理,并进行promise控制 */
  2470. return new Promise((resolve, reject) => {
  2471. fetchResolve = function (result) { resolve(result); };
  2472. fetchReject = function (err) { reject(err); };
  2473. })
  2474. }
  2475.  
  2476. win.fetch = customFetch;
  2477. }
  2478.  
  2479. function unFetchProxy (win) {
  2480. win = win || window;
  2481. if (win[realFetch]) {
  2482. win.fetch = win[realFetch];
  2483. delete win[realFetch];
  2484. }
  2485. }
  2486.  
  2487. /* 使用示例 */
  2488. // fetchProxy({
  2489. // onRequest: async (config, handler, isFetch) => {
  2490. // console.log('[fetchHooks onRequest]', config.url, config)
  2491. // handler.next(config)
  2492. // },
  2493. // onError: (err, handler, isFetch) => {
  2494. // handler.next(err)
  2495. // },
  2496. // onResponse: async (response, handler, isFetch) => {
  2497. // console.log('[fetchHooks onResponse]', response)
  2498.  
  2499. // /* 当和Ajax-hook混合使用时,需要判断isFetch,进行区分处理 */
  2500. // if (isFetch) {
  2501. // const res = response.clone()
  2502. // const result = await res.json().catch((err) => {
  2503. // // 解析出错,忽略报错
  2504. // if (err) {}
  2505. // })
  2506. // console.log('[fetchHooks onResponse json]', result)
  2507. // }
  2508.  
  2509. // handler.next(response)
  2510. // }
  2511. // }, window)
  2512.  
  2513. /*!
  2514. * @name fetch-proxy.js
  2515. * @description fetch请求hook,用法保持跟 https://github.com/wendux/Ajax-hook 的xhr-proxy一致,以便支持fetch请求的监听和修改
  2516. * @version 0.0.1
  2517. * @author xxxily
  2518. * @date 2022/05/20 16:18
  2519. * @github https://github.com/xxxily
  2520. */
  2521.  
  2522. function networkProxy (proxyConf = {}, win) {
  2523. proxy(proxyConf, win);
  2524. fetchProxy(proxyConf, win);
  2525. }
  2526.  
  2527. function unNetworkProxy (win) {
  2528. unProxy(win);
  2529. unFetchProxy(win);
  2530. }
  2531.  
  2532. /*!
  2533. * @name cacheStore.js
  2534. * @description 接口请求缓存存储管理模块
  2535. * @version 0.0.1
  2536. * @author xxxily
  2537. * @date 2022/05/13 09:36
  2538. * @github https://github.com/xxxily
  2539. */
  2540. const localforage = window.localforage;
  2541. const CryptoJS = window.CryptoJS;
  2542.  
  2543. function md5 (str) {
  2544. return CryptoJS.MD5(str).toString()
  2545. }
  2546.  
  2547. function createHash (config) {
  2548. if (config._hash_) {
  2549. return config._hash_
  2550. }
  2551.  
  2552. let url = config.url || '';
  2553.  
  2554. /**
  2555. * 如果检测到url使用了时间戳来防止缓存,则进行替换,进行缓存
  2556. * TODO
  2557. * 注意,这很可能会导致误伤,例如url上的时间戳并不是用来清理缓存的,而是某个时间点的参数
  2558. */
  2559. if (/=\d{13}/.test(url)) {
  2560. url = url.replace(/=\d{13}/, '=cache');
  2561. }
  2562.  
  2563. let hashStr = url + config.method;
  2564.  
  2565. if (config.method.toUpperCase() === 'POST') {
  2566. hashStr += JSON.stringify(config.data) + JSON.stringify(config.body);
  2567. }
  2568.  
  2569. const hash = md5(hashStr);
  2570.  
  2571. // if (url.includes('weixin.qq.com')) {
  2572. // hash = md5(config.url.replace(/\?\S+/, ''))
  2573. // }
  2574.  
  2575. config._hash_ = hash;
  2576.  
  2577. return hash
  2578. }
  2579.  
  2580. class CacheStore {
  2581. constructor (opts = {
  2582. localforageConfig: {}
  2583. }) {
  2584. this.store = localforage.createInstance(Object.assign({
  2585. name: 'vue-debug-helper-cache',
  2586. storeName: 'ajax-cache'
  2587. }, opts.localforageConfig));
  2588.  
  2589. /* 外部应该使用同样的hash生成方法,否则无法正常命中缓存规则 */
  2590. this.createHash = createHash;
  2591. }
  2592.  
  2593. async getCache (config) {
  2594. const hash = createHash(config);
  2595. const data = await this.store.getItem(hash);
  2596. return data
  2597. }
  2598.  
  2599. async setCache (response, isFetch) {
  2600. const headers = response.headers || {};
  2601. let isJsonResult = String(headers['content-type']).includes('application/json');
  2602.  
  2603. let resData = response.response || null;
  2604. if (isFetch && response.clone) {
  2605. const res = response.clone();
  2606. const resJson = await res.json().catch((err) => {
  2607. });
  2608.  
  2609. if (resJson) {
  2610. isJsonResult = true;
  2611. resData = JSON.stringify(resJson);
  2612. }
  2613. }
  2614.  
  2615. if (resData && isJsonResult) {
  2616. const hash = createHash(response.config);
  2617. await this.store.setItem(hash, resData);
  2618.  
  2619. /* 设置缓存的时候顺便更新缓存相关的基础信息,注意,该信息并不能100%被同步到本地 */
  2620. await this.updateCacheInfo(response.config);
  2621.  
  2622. debug.log(`[cacheStore setCache][${hash}] ${response.config.url}`, response);
  2623. }
  2624. }
  2625.  
  2626. async getCacheInfo (config) {
  2627. const hash = config ? this.createHash(config) : '';
  2628. if (this._cacheInfo_) {
  2629. return hash ? this._cacheInfo_[hash] : this._cacheInfo_
  2630. }
  2631.  
  2632. /* 在没将cacheInfo加载到内存前,只能单线程获取cacheInfo,防止多线程获取cacheInfo时出现问题 */
  2633. if (this._takeingCacheInfo_) {
  2634. const getCacheInfoHanderList = this._getCacheInfoHanderList_ || [];
  2635. const P = new Promise((resolve, reject) => {
  2636. getCacheInfoHanderList.push({
  2637. resolve,
  2638. config
  2639. });
  2640. });
  2641. this._getCacheInfoHanderList_ = getCacheInfoHanderList;
  2642. return P
  2643. }
  2644.  
  2645. this._takeingCacheInfo_ = true;
  2646. const cacheInfo = await this.store.getItem('ajaxCacheInfo') || {};
  2647. this._cacheInfo_ = cacheInfo;
  2648.  
  2649. delete this._takeingCacheInfo_;
  2650. if (this._getCacheInfoHanderList_) {
  2651. this._getCacheInfoHanderList_.forEach(async (handler) => {
  2652. handler.resolve(await this.getCacheInfo(handler.config));
  2653. });
  2654. delete this._getCacheInfoHanderList_;
  2655. }
  2656.  
  2657. return hash ? cacheInfo[hash] : cacheInfo
  2658. }
  2659.  
  2660. async updateCacheInfo (config) {
  2661. const cacheInfo = await this.getCacheInfo();
  2662.  
  2663. if (config) {
  2664. const hash = createHash(config);
  2665. if (hash && config) {
  2666. const info = {
  2667. url: config.url,
  2668. cacheTime: Date.now()
  2669. };
  2670.  
  2671. // 增加或更新缓存的基本信息
  2672. cacheInfo[hash] = info;
  2673. }
  2674. }
  2675.  
  2676. if (!this._updateCacheInfoIsWorking_) {
  2677. this._updateCacheInfoIsWorking_ = true;
  2678. await this.store.setItem('ajaxCacheInfo', cacheInfo);
  2679. this._updateCacheInfoIsWorking_ = false;
  2680. }
  2681. }
  2682.  
  2683. /**
  2684. * 清理已过期的缓存数据
  2685. * @param {number} expires 指定过期时间,单位:毫秒
  2686. * @returns
  2687. */
  2688. async cleanCache (expires) {
  2689. if (!expires) {
  2690. return
  2691. }
  2692.  
  2693. const cacheInfo = await this.getCacheInfo();
  2694. const cacheInfoKeys = Object.keys(cacheInfo);
  2695. const now = Date.now();
  2696.  
  2697. const storeKeys = await this.store.keys();
  2698.  
  2699. const needKeepKeys = cacheInfoKeys.filter(key => now - cacheInfo[key].cacheTime < expires);
  2700. needKeepKeys.push('ajaxCacheInfo');
  2701.  
  2702. const clearResult = [];
  2703.  
  2704. /* 清理不需要的数据 */
  2705. storeKeys.forEach((key) => {
  2706. if (!needKeepKeys.includes(key)) {
  2707. clearResult.push(this._cacheInfo_[key] || key);
  2708.  
  2709. this.store.removeItem(key);
  2710. delete this._cacheInfo_[key];
  2711. }
  2712. });
  2713.  
  2714. /* 更新缓存信息 */
  2715. if (clearResult.length) {
  2716. await this.updateCacheInfo();
  2717. debug.log('[cacheStore cleanCache] clearResult:', clearResult);
  2718. }
  2719. }
  2720.  
  2721. async get (key) {
  2722. const data = await this.store.getItem(key);
  2723. debug.log('[cacheStore]', key, data);
  2724. return data
  2725. }
  2726.  
  2727. async set (key, data) {
  2728. await this.store.setItem(key, data);
  2729. debug.log('[cacheStore]', key, data);
  2730. }
  2731.  
  2732. async remove (key) {
  2733. await this.store.removeItem(key);
  2734. debug.log('[cacheStore]', key);
  2735. }
  2736.  
  2737. async clear () {
  2738. await this.store.clear();
  2739. debug.log('[cacheStore] clear');
  2740. }
  2741.  
  2742. async keys () {
  2743. const keys = await this.store.keys();
  2744. debug.log('[cacheStore] keys', keys);
  2745. return keys
  2746. }
  2747. }
  2748.  
  2749. var cacheStore = new CacheStore();
  2750.  
  2751. /*!
  2752. * @name ajaxHooks.js
  2753. * @description 底层请求hook
  2754. * @version 0.0.1
  2755. * @author xxxily
  2756. * @date 2022/05/12 17:46
  2757. * @github https://github.com/xxxily
  2758. */
  2759.  
  2760. /**
  2761. * 判断是否符合进行缓存控制操作的条件
  2762. * @param {object} config
  2763. * @returns {boolean}
  2764. */
  2765. function useCache (config) {
  2766. const ajaxCache = helper.config.ajaxCache;
  2767. if (ajaxCache.enabled) {
  2768. return filtersMatch(ajaxCache.filters, config.url)
  2769. } else {
  2770. return false
  2771. }
  2772. }
  2773.  
  2774. function isNeedBlockAjax (config) {
  2775. const blockAjax = helper.config.blockAjax;
  2776. if (blockAjax.enabled) {
  2777. return filtersMatch(blockAjax.filters, config.url)
  2778. } else {
  2779. return false
  2780. }
  2781. }
  2782.  
  2783. let ajaxHooksWin = window;
  2784.  
  2785. const ajaxHooks = {
  2786. hook (win = ajaxHooksWin) {
  2787. networkProxy({
  2788. onRequest: async (config, handler, isFetch) => {
  2789. const fetchTips = isFetch ? 'fetch ' : '';
  2790.  
  2791. if (isNeedBlockAjax(config)) {
  2792. handler.reject(new Error('ajax blocked'));
  2793. debug.warn(`[ajaxHooks][blocked]${fetchTips}${config.method} ${config.url}`, config);
  2794. return false
  2795. }
  2796.  
  2797. let hitCache = false;
  2798. if (useCache(config)) {
  2799. const cacheInfo = await cacheStore.getCacheInfo(config);
  2800. const cache = await cacheStore.getCache(config);
  2801.  
  2802. if (cache && cacheInfo) {
  2803. const isExpires = Date.now() - cacheInfo.cacheTime > helper.config.ajaxCache.expires;
  2804.  
  2805. if (!isExpires) {
  2806. if (isFetch) {
  2807. const customResponse = new Response(cache, {
  2808. status: 200,
  2809. statusText: 'ok',
  2810. url: config.url,
  2811. headers: new Headers({
  2812. 'Content-Type': 'application/json'
  2813. })
  2814. });
  2815. handler.resolve(customResponse);
  2816. } else {
  2817. handler.resolve({
  2818. config: config,
  2819. status: 200,
  2820. headers: { 'content-type': 'application/json' },
  2821. response: cache
  2822. });
  2823. }
  2824.  
  2825. hitCache = true;
  2826. }
  2827. }
  2828. }
  2829.  
  2830. if (hitCache) {
  2831. debug.warn(`[ajaxHooks] use cache:${fetchTips}${config.method} ${config.url}`, config);
  2832. } else {
  2833. handler.next(config);
  2834. }
  2835. },
  2836.  
  2837. onError: (err, handler, isFetch) => {
  2838. handler.next(err);
  2839. },
  2840.  
  2841. onResponse: async (response, handler, isFetch) => {
  2842. if (useCache(response.config)) {
  2843. // 加入缓存
  2844. cacheStore.setCache(response, isFetch);
  2845. }
  2846.  
  2847. handler.next(response);
  2848. }
  2849. }, win);
  2850. },
  2851.  
  2852. unHook (win = ajaxHooksWin, force = false) {
  2853. if (force === true) {
  2854. unNetworkProxy(win);
  2855. } else {
  2856. if (!helper.config.ajaxCache.enabled && !helper.config.blockAjax.enabled && !helper.config.replaceAjax.enabled) {
  2857. unNetworkProxy(win);
  2858. }
  2859. }
  2860. },
  2861.  
  2862. init (win) {
  2863. ajaxHooksWin = win;
  2864.  
  2865. if (helper.config.ajaxCache.enabled || helper.config.blockAjax.enabled || helper.config.replaceAjax.enabled) {
  2866. ajaxHooks.hook(ajaxHooksWin);
  2867. }
  2868.  
  2869. /* 定时清除接口的缓存数据,防止不断堆积 */
  2870. setTimeout(() => {
  2871. cacheStore.cleanCache(helper.config.ajaxCache.expires);
  2872. }, 1000 * 10);
  2873. }
  2874. };
  2875.  
  2876. /*!
  2877. * @name performanceObserver.js
  2878. * @description 进行性能监测结果的打印
  2879. * @version 0.0.1
  2880. * @author xxxily
  2881. * @date 2022/05/11 10:39
  2882. * @github https://github.com/xxxily
  2883. */
  2884.  
  2885. const performanceObserver = {
  2886. observer: null,
  2887. init () {
  2888. if (typeof PerformanceObserver === 'undefined') {
  2889. debug.log(i18n.t('debugHelper.performanceObserver.notSupport'));
  2890. return false
  2891. }
  2892.  
  2893. if (performanceObserver.observer && performanceObserver.observer.disconnect) {
  2894. performanceObserver.observer.disconnect();
  2895. }
  2896.  
  2897. /* 不进行性能观察 */
  2898. if (!helper.config.performanceObserver.enabled) {
  2899. performanceObserver.observer = null;
  2900. return false
  2901. }
  2902.  
  2903. // https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver/observe
  2904. performanceObserver.observer = new PerformanceObserver(function (list, observer) {
  2905. if (!helper.config.performanceObserver.enabled) {
  2906. return
  2907. }
  2908.  
  2909. const entries = list.getEntries();
  2910. for (let i = 0; i < entries.length; i++) {
  2911. const entry = entries[i];
  2912. debug.info(`[performanceObserver ${entry.entryType}]`, entry);
  2913. }
  2914. });
  2915.  
  2916. // https://runebook.dev/zh-CN/docs/dom/performanceentry/entrytype
  2917. performanceObserver.observer.observe({ entryTypes: helper.config.performanceObserver.entryTypes });
  2918. }
  2919. };
  2920.  
  2921. /*!
  2922. * @name inspect.js
  2923. * @description vue组件审查模块
  2924. * @version 0.0.1
  2925. * @author xxxily
  2926. * @date 2022/05/10 18:25
  2927. * @github https://github.com/xxxily
  2928. */
  2929.  
  2930. const overlaySelector = 'vue-debugger-overlay';
  2931. const $ = window.$;
  2932. let currentComponent = null;
  2933.  
  2934. const inspect = {
  2935. findComponentsByElement (el) {
  2936. let result = null;
  2937. let deep = 0;
  2938. let parent = el;
  2939. while (parent) {
  2940. if (deep >= 50) {
  2941. break
  2942. }
  2943.  
  2944. if (parent.__vue__) {
  2945. result = parent;
  2946. break
  2947. }
  2948.  
  2949. deep++;
  2950. parent = parent.parentNode;
  2951. }
  2952.  
  2953. return result
  2954. },
  2955.  
  2956. getComponentInstance (el) {
  2957. let vueComponent = el && el.__vue__ ? el.__vue__ : null;
  2958.  
  2959. /* 忽略transition */
  2960. if (vueComponent && vueComponent?.$options._componentTag === 'transition' && vueComponent.$parent) {
  2961. vueComponent = vueComponent.$parent;
  2962. }
  2963.  
  2964. return vueComponent
  2965. },
  2966.  
  2967. initContextMenu () {
  2968. if (this._hasInitContextMenu_) {
  2969. return
  2970. }
  2971.  
  2972. function createComponentMenuItem (vueComponent, deep = 0) {
  2973. let componentMenu = {};
  2974. if (vueComponent) {
  2975. helper.methods.initComponentInfo(vueComponent);
  2976.  
  2977. componentMenu = {
  2978. consoleComponent: {
  2979. name: `${i18n.t('contextMenu.consoleComponent')} <${vueComponent._componentName}>`,
  2980. icon: 'fa-eye',
  2981. callback: function (key, options) {
  2982. debug.log(`[vueComponent] ${vueComponent._componentTag}`, vueComponent);
  2983. }
  2984. },
  2985. consoleComponentData: {
  2986. name: `${i18n.t('contextMenu.consoleComponentData')} <${vueComponent._componentName}>`,
  2987. icon: 'fa-eye',
  2988. callback: function (key, options) {
  2989. debug.log(`[vueComponentData] ${vueComponent._componentTag}`, vueComponent.$data);
  2990. }
  2991. },
  2992. consoleComponentProps: {
  2993. name: `${i18n.t('contextMenu.consoleComponentProps')} <${vueComponent._componentName}>`,
  2994. icon: 'fa-eye',
  2995. callback: function (key, options) {
  2996. debug.log(`[vueComponentProps] ${vueComponent._componentTag}`, vueComponent.$props);
  2997. }
  2998. }
  2999. // consoleComponentChain: {
  3000. // name: `${i18n.t('contextMenu.consoleComponentChain')} <${vueComponent._componentName}>`,
  3001. // icon: 'fa-eye',
  3002. // callback: function (key, options) {
  3003. // debug.log(`[vueComponentMethods] ${vueComponent._componentTag}`, vueComponent._componentChain)
  3004. // }
  3005. // }
  3006. };
  3007. }
  3008.  
  3009. if (vueComponent.$parent && deep <= 5) {
  3010. componentMenu.parentComponent = {
  3011. name: `${i18n.t('contextMenu.consoleParentComponent')} <${vueComponent.$parent._componentName}>`,
  3012. icon: 'fa-eye',
  3013. items: createComponentMenuItem(vueComponent.$parent, deep + 1)
  3014. };
  3015. }
  3016.  
  3017. const file = vueComponent.options?.__file || vueComponent.$options?.__file || '';
  3018. let copyFilePath = {};
  3019. if (file) {
  3020. copyFilePath = {
  3021. copyFilePath: {
  3022. name: `${i18n.t('contextMenu.copyFilePath')}`,
  3023. icon: 'fa-copy',
  3024. callback: function (key, options) {
  3025. debug.log(`[componentFilePath ${vueComponent._componentName}] ${file}`);
  3026. copyToClipboard(file);
  3027. }
  3028. }
  3029. };
  3030. }
  3031.  
  3032. componentMenu.componentAction = {
  3033. name: `${i18n.t('contextMenu.componentAction')} <${vueComponent._componentName}>`,
  3034. icon: 'fa-cog',
  3035. items: {
  3036. ...copyFilePath,
  3037. copyComponentName: {
  3038. name: `${i18n.t('contextMenu.copyComponentName')} <${vueComponent._componentName}>`,
  3039. icon: 'fa-copy',
  3040. callback: function (key, options) {
  3041. copyToClipboard(vueComponent._componentName);
  3042. }
  3043. },
  3044. copyComponentData: {
  3045. name: `${i18n.t('contextMenu.copyComponentData')} <${vueComponent._componentName}>`,
  3046. icon: 'fa-copy',
  3047. callback: function (key, options) {
  3048. const data = JSON.stringify(vueComponent.$data, null, 2);
  3049. debug.log(`[vueComponentData] ${vueComponent._componentName}`, JSON.parse(data));
  3050. debug.log(data);
  3051. copyToClipboard(data);
  3052. }
  3053. },
  3054. copyComponentProps: {
  3055. name: `${i18n.t('contextMenu.copyComponentProps')} <${vueComponent._componentName}>`,
  3056. icon: 'fa-copy',
  3057. callback: function (key, options) {
  3058. const props = JSON.stringify(vueComponent.$props, null, 2);
  3059. debug.log(`[vueComponentProps] ${vueComponent._componentName}`, JSON.parse(props));
  3060. debug.log(props);
  3061. copyToClipboard(props);
  3062. }
  3063. },
  3064. // copyComponentTag: {
  3065. // name: `${i18n.t('contextMenu.copyComponentTag')} <${vueComponent._componentName}>`,
  3066. // icon: 'fa-copy',
  3067. // callback: function (key, options) {
  3068. // copyToClipboard(vueComponent._componentTag)
  3069. // }
  3070. // },
  3071. copyComponentUid: {
  3072. name: `${i18n.t('contextMenu.copyComponentUid')} -> ${vueComponent._uid}`,
  3073. icon: 'fa-copy',
  3074. callback: function (key, options) {
  3075. copyToClipboard(vueComponent._uid);
  3076. }
  3077. },
  3078. copyComponentChian: {
  3079. name: `${i18n.t('contextMenu.copyComponentChian')}`,
  3080. icon: 'fa-copy',
  3081. callback: function (key, options) {
  3082. debug.log(`[vueComponentChain] ${vueComponent._componentName}`, vueComponent._componentChain);
  3083. copyToClipboard(vueComponent._componentChain);
  3084. }
  3085. },
  3086. findComponents: {
  3087. name: `${i18n.t('contextMenu.findComponents')} <${vueComponent._componentName}>`,
  3088. icon: 'fa-search',
  3089. callback: function (key, options) {
  3090. functionCall.findComponents(vueComponent._componentName);
  3091. }
  3092. },
  3093. printLifeCycleInfo: {
  3094. name: `${i18n.t('contextMenu.printLifeCycleInfo')} <${vueComponent._componentName}>`,
  3095. icon: 'fa-print',
  3096. callback: function (key, options) {
  3097. functionCall.printLifeCycleInfo(vueComponent._componentName);
  3098. }
  3099. },
  3100. blockComponents: {
  3101. name: `${i18n.t('contextMenu.blockComponents')} <${vueComponent._componentName}>`,
  3102. icon: 'fa-ban',
  3103. callback: function (key, options) {
  3104. functionCall.blockComponents(vueComponent._componentName);
  3105. }
  3106. }
  3107. }
  3108. };
  3109.  
  3110. return componentMenu
  3111. }
  3112.  
  3113. $.contextMenu({
  3114. selector: 'body.vue-debug-helper-inspect-mode',
  3115. zIndex: 2147483647,
  3116. className: 'vue-debug-helper-context-menu',
  3117. build: function ($trigger, e) {
  3118. const conf = helper.config;
  3119. const vueComponent = inspect.getComponentInstance(currentComponent);
  3120.  
  3121. let componentMenu = {};
  3122. if (vueComponent) {
  3123. componentMenu = createComponentMenuItem(vueComponent);
  3124. componentMenu.componentMenuSeparator = '---------';
  3125. }
  3126.  
  3127. const componentsStatisticsInfo = {
  3128. componentsStatistics: {
  3129. name: i18n.t('debugHelper.componentsStatistics'),
  3130. icon: 'fa-thin fa-info-circle',
  3131. callback: functionCall.componentsStatistics
  3132. },
  3133. componentsSummaryStatisticsSort: {
  3134. name: i18n.t('debugHelper.componentsSummaryStatisticsSort'),
  3135. icon: 'fa-thin fa-info-circle',
  3136. callback: functionCall.componentsSummaryStatisticsSort
  3137. },
  3138. destroyStatisticsSort: {
  3139. name: i18n.t('debugHelper.destroyStatisticsSort'),
  3140. icon: 'fa-regular fa-trash',
  3141. callback: functionCall.destroyStatisticsSort
  3142. },
  3143. clearAll: {
  3144. name: i18n.t('debugHelper.clearAll'),
  3145. icon: 'fa-regular fa-close',
  3146. callback: functionCall.clearAll
  3147. }
  3148. };
  3149.  
  3150. const commonMenu = {
  3151. findComponents: {
  3152. name: i18n.t('debugHelper.findComponents'),
  3153. icon: 'fa-regular fa-search',
  3154. callback: () => {
  3155. functionCall.findComponents();
  3156. }
  3157. },
  3158. blockComponents: {
  3159. name: i18n.t('debugHelper.blockComponents'),
  3160. icon: 'fa-regular fa-ban',
  3161. callback: () => {
  3162. functionCall.blockComponents();
  3163. }
  3164. },
  3165. printLifeCycleInfo: {
  3166. name: conf.lifecycle.show ? i18n.t('debugHelper.notPrintLifeCycleInfo') : i18n.t('debugHelper.printLifeCycleInfo'),
  3167. icon: 'fa-regular fa-life-ring',
  3168. callback: () => {
  3169. conf.lifecycle.show ? functionCall.notPrintLifeCycleInfo() : functionCall.printLifeCycleInfo();
  3170. }
  3171. },
  3172. dd: {
  3173. name: conf.dd.enabled ? i18n.t('debugHelper.undd') : i18n.t('debugHelper.dd'),
  3174. icon: 'fa-regular fa-arrows-alt',
  3175. callback: conf.dd.enabled ? functionCall.undd : functionCall.dd
  3176. },
  3177. toggleHackVueComponent: {
  3178. name: conf.hackVueComponent ? i18n.t('debugHelper.hackVueComponent.unhack') : i18n.t('debugHelper.hackVueComponent.hack'),
  3179. icon: 'fa-regular fa-bug',
  3180. callback: functionCall.toggleHackVueComponent
  3181. },
  3182. componentFunSeparator: '---------',
  3183. toggleAjaxCache: {
  3184. name: conf.ajaxCache.enabled ? i18n.t('debugHelper.ajaxCacheStatus.off') : i18n.t('debugHelper.ajaxCacheStatus.on'),
  3185. icon: 'fa-regular fa-database',
  3186. callback: functionCall.toggleAjaxCache
  3187. },
  3188. clearAjaxCache: {
  3189. name: i18n.t('debugHelper.clearAjaxCache'),
  3190. icon: 'fa-regular fa-database',
  3191. callback: functionCall.clearAjaxCache
  3192. },
  3193. toggleBlockAjax: {
  3194. name: conf.blockAjax.enabled ? i18n.t('debugHelper.blockAjax.disable') : i18n.t('debugHelper.blockAjax.enabled'),
  3195. icon: 'fa-regular fa-ban',
  3196. callback: functionCall.toggleBlockAjax
  3197. },
  3198. togglePerformanceObserver: {
  3199. name: conf.performanceObserver.enabled ? i18n.t('debugHelper.performanceObserverStatus.off') : i18n.t('debugHelper.performanceObserverStatus.on'),
  3200. icon: 'fa-regular fa-paint-brush',
  3201. callback: functionCall.togglePerformanceObserver
  3202. },
  3203. measureSelectorInterval: {
  3204. name: i18n.t('debugHelper.measureSelectorInterval'),
  3205. icon: 'fa-regular fa-clock-o',
  3206. callback: functionCall.measureSelectorInterval
  3207. },
  3208. commonEndSeparator: '---------'
  3209. };
  3210.  
  3211. const moreMenu = {
  3212. ...(conf.contextMenu.simplify ? commonMenu : {}),
  3213. help: {
  3214. name: i18n.t('help'),
  3215. icon: 'fa-regular fa-question-circle',
  3216. items: {
  3217. docs: {
  3218. name: i18n.t('docs'),
  3219. icon: 'fa-regular fa-book',
  3220. callback: () => { openInTab('https://github.com/xxxily/vue-debug-helper'); }
  3221. },
  3222. about: {
  3223. name: i18n.t('about'),
  3224. icon: 'fa-regular fa-info-circle',
  3225. callback: () => { openInTab('https://github.com/xxxily/vue-debug-helper'); }
  3226. },
  3227. issues: {
  3228. name: i18n.t('issues'),
  3229. icon: 'fa-regular fa-bug',
  3230. callback: () => { openInTab('https://github.com/xxxily/vue-debug-helper/issues'); }
  3231. },
  3232. update: {
  3233. name: i18n.t('update'),
  3234. icon: 'fa-regular fa-refresh',
  3235. callback: () => { openInTab('https://gf.qytechs.cn/zh-CN/scripts/444075'); }
  3236. }
  3237. }
  3238. },
  3239. toggleSimplifyMode: {
  3240. name: conf.contextMenu.simplify ? i18n.t('debugHelper.simplifyMode.disable') : i18n.t('debugHelper.simplifyMode.enabled'),
  3241. icon: 'fa-regular fa-compress',
  3242. callback: functionCall.toggleSimplifyMode
  3243. },
  3244. toggleInspect: {
  3245. name: conf.inspect.enabled ? i18n.t('debugHelper.inspectStatus.off') : i18n.t('debugHelper.inspectStatus.on'),
  3246. icon: 'fa-regular fa-eye',
  3247. callback: functionCall.toggleInspect
  3248. }
  3249. };
  3250.  
  3251. const menu = {
  3252. callback: function (key, options) {
  3253. debug.log(`[contextMenu] ${key}`);
  3254. },
  3255. items: {
  3256. refresh: {
  3257. name: i18n.t('refreshPage'),
  3258. icon: 'fa-refresh',
  3259. callback: function (key, options) {
  3260. window.location.reload();
  3261. }
  3262. },
  3263. sep0: '---------',
  3264. ...componentMenu,
  3265. ...componentsStatisticsInfo,
  3266. statisticsSeparator: '---------',
  3267. ...(conf.contextMenu.simplify ? {} : commonMenu),
  3268. more: {
  3269. name: i18n.t('contextMenu.more'),
  3270. icon: 'fa-ellipsis-h',
  3271. items: {
  3272. ...moreMenu
  3273. }
  3274. },
  3275. quit: {
  3276. name: i18n.t('quit'),
  3277. icon: 'fa-close',
  3278. callback: function ($element, key, item) {
  3279. return 'context-menu-icon context-menu-icon-quit'
  3280. }
  3281. }
  3282. }
  3283. };
  3284.  
  3285. return menu
  3286. }
  3287. });
  3288.  
  3289. /* 主动触发右键,解决部分右键菜单已被提前注册(不可用)且禁止冒泡导致右键无法触发的问题 */
  3290. document.addEventListener('contextmenu', function (e) {
  3291. helper.config.inspect.enabled && $('body.vue-debug-helper-inspect-mode').contextMenu({ x: e.pageX, y: e.pageY });
  3292. }, true);
  3293.  
  3294. this._hasInitContextMenu_ = true;
  3295. },
  3296.  
  3297. setOverlay (el) {
  3298. let overlay = document.querySelector('#' + overlaySelector);
  3299. if (!overlay) {
  3300. overlay = document.createElement('div');
  3301. overlay.id = overlaySelector;
  3302.  
  3303. const infoBox = document.createElement('div');
  3304. infoBox.className = 'vue-debugger-component-info';
  3305.  
  3306. const styleDom = document.createElement('style');
  3307. styleDom.appendChild(document.createTextNode(`
  3308. .vue-debug-helper-context-menu {
  3309. font-size: 14px;
  3310. }
  3311. #${overlaySelector} {
  3312. position: fixed;
  3313. z-index: 2147483647;
  3314. background-color: rgba(65, 184, 131, 0.15);
  3315. padding: 5px;
  3316. font-size: 11px;
  3317. pointer-events: none;
  3318. box-size: border-box;
  3319. border-radius: 3px;
  3320. overflow: visible;
  3321. }
  3322.  
  3323. #${overlaySelector} .vue-debugger-component-info {
  3324. position: absolute;
  3325. top: -30px;
  3326. left: 0;
  3327. line-height: 1.5;
  3328. display: inline-block;
  3329. padding: 4px 8px;
  3330. border-radius: 3px;
  3331. background-color: #fff;
  3332. font-family: monospace;
  3333. font-size: 11px;
  3334. color: rgb(51, 51, 51);
  3335. text-align: center;
  3336. border: 1px solid rgba(65, 184, 131, 0.5);
  3337. background-clip: padding-box;
  3338. pointer-events: none;
  3339. white-space: nowrap;
  3340. }
  3341. `));
  3342.  
  3343. overlay.appendChild(infoBox);
  3344. document.body.appendChild(styleDom);
  3345. document.body.appendChild(overlay);
  3346. }
  3347.  
  3348. /* 批量设置样式,减少样式扰动 */
  3349. const rect = el.getBoundingClientRect();
  3350. const overlayStyle = [
  3351. `width: ${rect.width}px;`,
  3352. `height: ${rect.height}px;`,
  3353. `top: ${rect.top}px;`,
  3354. `left: ${rect.left}px;`,
  3355. 'display: block;'
  3356. ].join(' ');
  3357. overlay.setAttribute('style', overlayStyle);
  3358.  
  3359. const vm = inspect.getComponentInstance(el);
  3360. if (vm) {
  3361. helper.methods.initComponentInfo(vm);
  3362. const name = vm._componentName || vm._componentTag || vm._uid;
  3363. const infoBox = overlay.querySelector('.vue-debugger-component-info');
  3364.  
  3365. infoBox.innerHTML = [
  3366. '<span style="opacity: 0.6;">&lt;</span>',
  3367. `<span style="font-weight: bold; color: rgb(9, 171, 86);">${name}</span>`,
  3368. '<span style="opacity: 0.6;">&gt;</span>',
  3369. `<span style="opacity: 0.5; margin-left: 6px;">${Math.round(rect.width)}<span style="margin-right: 2px; margin-left: 2px;">×</span>${Math.round(rect.height)}</span>`
  3370. ].join('');
  3371.  
  3372. rect.y < 32 ? (infoBox.style.top = '0') : (infoBox.style.top = '-30px');
  3373. }
  3374.  
  3375. $(document.body).addClass('vue-debug-helper-inspect-mode');
  3376. inspect.initContextMenu();
  3377. },
  3378.  
  3379. clearOverlay () {
  3380. $(document.body).removeClass('vue-debug-helper-inspect-mode');
  3381. const overlay = document.querySelector('#vue-debugger-overlay');
  3382. if (overlay) {
  3383. overlay.style.display = 'none';
  3384. }
  3385. },
  3386.  
  3387. init (Vue) {
  3388. document.body.addEventListener('mouseover', (event) => {
  3389. if (!helper.config.inspect.enabled) {
  3390. return
  3391. }
  3392.  
  3393. const componentEl = inspect.findComponentsByElement(event.target);
  3394.  
  3395. if (componentEl) {
  3396. currentComponent = componentEl;
  3397. inspect.setOverlay(componentEl);
  3398. } else {
  3399. currentComponent = null;
  3400. }
  3401. });
  3402. }
  3403. };
  3404.  
  3405. /**
  3406. * 元素监听器
  3407. * @param selector -必选
  3408. * @param fn -必选,元素存在时的回调
  3409. * @param shadowRoot -可选 指定监听某个shadowRoot下面的DOM元素
  3410. * 参考:https://javascript.ruanyifeng.com/dom/mutationobserver.html
  3411. */
  3412. function ready (selector, fn, shadowRoot) {
  3413. const win = window;
  3414. const docRoot = shadowRoot || win.document.documentElement;
  3415. if (!docRoot) return false
  3416. const MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
  3417. const listeners = docRoot._MutationListeners || [];
  3418.  
  3419. function $ready (selector, fn) {
  3420. // 储存选择器和回调函数
  3421. listeners.push({
  3422. selector: selector,
  3423. fn: fn
  3424. });
  3425.  
  3426. /* 增加监听对象 */
  3427. if (!docRoot._MutationListeners || !docRoot._MutationObserver) {
  3428. docRoot._MutationListeners = listeners;
  3429. docRoot._MutationObserver = new MutationObserver(() => {
  3430. for (let i = 0; i < docRoot._MutationListeners.length; i++) {
  3431. const item = docRoot._MutationListeners[i];
  3432. check(item.selector, item.fn);
  3433. }
  3434. });
  3435.  
  3436. docRoot._MutationObserver.observe(docRoot, {
  3437. childList: true,
  3438. subtree: true
  3439. });
  3440. }
  3441.  
  3442. // 检查节点是否已经在DOM中
  3443. check(selector, fn);
  3444. }
  3445.  
  3446. function check (selector, fn) {
  3447. const elements = docRoot.querySelectorAll(selector);
  3448. for (let i = 0; i < elements.length; i++) {
  3449. const element = elements[i];
  3450. element._MutationReadyList_ = element._MutationReadyList_ || [];
  3451. if (!element._MutationReadyList_.includes(fn)) {
  3452. element._MutationReadyList_.push(fn);
  3453. fn.call(element, element);
  3454. }
  3455. }
  3456. }
  3457.  
  3458. const selectorArr = Array.isArray(selector) ? selector : [selector];
  3459. selectorArr.forEach(selector => $ready(selector, fn));
  3460. }
  3461.  
  3462. /*!
  3463. * @name functionCall.js
  3464. * @description 统一的提供外部功能调用管理模块
  3465. * @version 0.0.1
  3466. * @author xxxily
  3467. * @date 2022/04/27 17:42
  3468. * @github https://github.com/xxxily
  3469. */
  3470.  
  3471. const functionCall = {
  3472. toggleInspect () {
  3473. helper.config.inspect.enabled = !helper.config.inspect.enabled;
  3474. debug.log(`${i18n.t('debugHelper.toggleInspect')} success (${helper.config.inspect.enabled})`);
  3475.  
  3476. if (!helper.config.inspect.enabled) {
  3477. inspect.clearOverlay();
  3478. }
  3479. },
  3480. viewVueDebugHelperObject () {
  3481. debug.log(i18n.t('debugHelper.viewVueDebugHelperObject'), helper);
  3482. },
  3483. componentsStatistics () {
  3484. const result = helper.methods.componentsStatistics();
  3485. let total = 0;
  3486.  
  3487. /* 提供友好的可视化展示方式 */
  3488. console.table && console.table(result.map(item => {
  3489. total += item.componentInstance.length;
  3490. return {
  3491. componentName: item.componentName,
  3492. count: item.componentInstance.length
  3493. }
  3494. }));
  3495.  
  3496. debug.log(`${i18n.t('debugHelper.componentsStatistics')} (total:${total})`, result);
  3497. },
  3498. destroyStatisticsSort () {
  3499. const result = helper.methods.destroyStatisticsSort();
  3500. let total = 0;
  3501.  
  3502. /* 提供友好的可视化展示方式 */
  3503. console.table && console.table(result.map(item => {
  3504. const durationList = item.destroyList.map(item => item.duration);
  3505. const maxDuration = Math.max(...durationList);
  3506. const minDuration = Math.min(...durationList);
  3507. const durationRange = maxDuration - minDuration;
  3508. total += item.destroyList.length;
  3509.  
  3510. return {
  3511. componentName: item.componentName,
  3512. count: item.destroyList.length,
  3513. avgDuration: durationList.reduce((pre, cur) => pre + cur, 0) / durationList.length,
  3514. maxDuration,
  3515. minDuration,
  3516. durationRange,
  3517. durationRangePercent: (1000 - minDuration) / durationRange
  3518. }
  3519. }));
  3520.  
  3521. debug.log(`${i18n.t('debugHelper.destroyStatisticsSort')} (total:${total})`, result);
  3522. },
  3523. componentsSummaryStatisticsSort () {
  3524. const result = helper.methods.componentsSummaryStatisticsSort();
  3525. let total = 0;
  3526.  
  3527. /* 提供友好的可视化展示方式 */
  3528. console.table && console.table(result.map(item => {
  3529. total += item.componentsSummary.length;
  3530. return {
  3531. componentName: item.componentName,
  3532. count: item.componentsSummary.length
  3533. }
  3534. }));
  3535.  
  3536. debug.log(`${i18n.t('debugHelper.componentsSummaryStatisticsSort')} (total:${total})`, result);
  3537. },
  3538. getDestroyByDuration () {
  3539. const destroyInfo = helper.methods.getDestroyByDuration();
  3540. console.table && console.table(destroyInfo.destroyList);
  3541. debug.log(i18n.t('debugHelper.getDestroyByDuration'), destroyInfo);
  3542. },
  3543. clearAll () {
  3544. helper.methods.clearAll();
  3545. debug.log(i18n.t('debugHelper.clearAll'));
  3546. },
  3547.  
  3548. printLifeCycleInfo (str) {
  3549. addToFilters(helper.config.lifecycle, 'componentFilters', str);
  3550.  
  3551. const lifecycleFilters = window.prompt(i18n.t('debugHelper.printLifeCycleInfoPrompt.lifecycleFilters'), helper.config.lifecycle.filters.join(','));
  3552. const componentFilters = window.prompt(i18n.t('debugHelper.printLifeCycleInfoPrompt.componentFilters'), helper.config.lifecycle.componentFilters.join(','));
  3553.  
  3554. if (lifecycleFilters !== null && componentFilters !== null) {
  3555. debug.log(i18n.t('debugHelper.printLifeCycleInfo'));
  3556. helper.methods.printLifeCycleInfo(lifecycleFilters, componentFilters);
  3557. }
  3558. },
  3559.  
  3560. notPrintLifeCycleInfo () {
  3561. debug.log(i18n.t('debugHelper.notPrintLifeCycleInfo'));
  3562. helper.methods.notPrintLifeCycleInfo();
  3563. },
  3564.  
  3565. findComponents (str) {
  3566. addToFilters(helper.config, 'findComponentsFilters', str);
  3567.  
  3568. const filters = window.prompt(i18n.t('debugHelper.findComponentsPrompt.filters'), helper.config.findComponentsFilters.join(','));
  3569. if (filters !== null) {
  3570. debug.log(i18n.t('debugHelper.findComponents'), helper.methods.findComponents(filters));
  3571. }
  3572. },
  3573.  
  3574. findNotContainElementComponents () {
  3575. debug.log(i18n.t('debugHelper.findNotContainElementComponents'), helper.methods.findNotContainElementComponents());
  3576. },
  3577.  
  3578. blockComponents (str) {
  3579. addToFilters(helper.config, 'blockFilters', str);
  3580.  
  3581. const filters = window.prompt(i18n.t('debugHelper.blockComponentsPrompt.filters'), helper.config.blockFilters.join(','));
  3582. if (filters !== null) {
  3583. helper.methods.blockComponents(filters);
  3584. debug.log(i18n.t('debugHelper.blockComponents'), filters);
  3585. }
  3586. },
  3587.  
  3588. dd () {
  3589. const filter = window.prompt(i18n.t('debugHelper.ddPrompt.filter'), helper.config.dd.filters.join(','));
  3590. const count = window.prompt(i18n.t('debugHelper.ddPrompt.count'), helper.config.dd.count);
  3591.  
  3592. if (filter !== null && count !== null) {
  3593. debug.log(i18n.t('debugHelper.dd'));
  3594. helper.methods.dd(filter, Number(count));
  3595. }
  3596. },
  3597.  
  3598. undd () {
  3599. debug.log(i18n.t('debugHelper.undd'));
  3600. helper.methods.undd();
  3601. },
  3602.  
  3603. toggleHackVueComponent () {
  3604. helper.config.hackVueComponent ? vueHooks.unHackVueComponent() : vueHooks.hackVueComponent();
  3605. helper.config.hackVueComponent = !helper.config.hackVueComponent;
  3606. },
  3607.  
  3608. togglePerformanceObserver () {
  3609. helper.config.performanceObserver.enabled = !helper.config.performanceObserver.enabled;
  3610.  
  3611. if (helper.config.performanceObserver.enabled) {
  3612. let entryTypes = window.prompt(i18n.t('debugHelper.performanceObserverPrompt.entryTypes'), helper.config.performanceObserver.entryTypes.join(','));
  3613. if (entryTypes) {
  3614. const entryTypesArr = toArrFilters(entryTypes);
  3615. const supportEntryTypes = ['element', 'navigation', 'resource', 'mark', 'measure', 'paint', 'longtask'];
  3616.  
  3617. /* 过滤出支持的entryTypes */
  3618. entryTypes = entryTypesArr.filter(item => supportEntryTypes.includes(item));
  3619.  
  3620. if (entryTypes.length !== entryTypesArr.length) {
  3621. debug.warn(`some entryTypes not support, only support: ${supportEntryTypes.join(',')}`);
  3622. }
  3623.  
  3624. helper.config.performanceObserver.entryTypes = entryTypes;
  3625.  
  3626. performanceObserver.init();
  3627. } else {
  3628. alert('entryTypes is empty');
  3629. }
  3630. }
  3631.  
  3632. debug.log(`${i18n.t('debugHelper.togglePerformanceObserver')} success (${helper.config.performanceObserver.enabled})`);
  3633. },
  3634.  
  3635. useAjaxCache () {
  3636. helper.config.ajaxCache.enabled = true;
  3637.  
  3638. const filters = window.prompt(i18n.t('debugHelper.jaxCachePrompt.filters'), helper.config.ajaxCache.filters.join(','));
  3639. const expires = window.prompt(i18n.t('debugHelper.jaxCachePrompt.expires'), helper.config.ajaxCache.expires / 1000 / 60);
  3640.  
  3641. if (filters && expires) {
  3642. helper.config.ajaxCache.filters = toArrFilters(filters);
  3643.  
  3644. if (!isNaN(Number(expires))) {
  3645. helper.config.ajaxCache.expires = Number(expires) * 1000 * 60;
  3646. }
  3647.  
  3648. ajaxHooks.hook();
  3649.  
  3650. debug.log(`${i18n.t('debugHelper.enableAjaxCacheTips')}`);
  3651. }
  3652. },
  3653.  
  3654. disableAjaxCache () {
  3655. helper.config.ajaxCache.enabled = false;
  3656. ajaxHooks.unHook();
  3657. debug.log(`${i18n.t('debugHelper.disableAjaxCacheTips')}`);
  3658. },
  3659.  
  3660. toggleAjaxCache () {
  3661. if (helper.config.ajaxCache.enabled) {
  3662. functionCall.disableAjaxCache();
  3663. } else {
  3664. functionCall.useAjaxCache();
  3665. }
  3666. },
  3667.  
  3668. async clearAjaxCache () {
  3669. await cacheStore.store.clear();
  3670. debug.log(`${i18n.t('debugHelper.clearAjaxCacheTips')}`);
  3671. },
  3672.  
  3673. useBlockAjax () {
  3674. helper.config.blockAjax.enabled = true;
  3675.  
  3676. const filters = window.prompt(i18n.t('debugHelper.blockAjax.prompt.filters'), helper.config.blockAjax.filters.join(','));
  3677. if (filters) {
  3678. helper.config.blockAjax.filters = toArrFilters(filters);
  3679. ajaxHooks.hook();
  3680. debug.log(`${i18n.t('debugHelper.blockAjax.enabled')} success (${helper.config.blockAjax.filters.join(',')})`);
  3681. }
  3682. },
  3683.  
  3684. disableBlockAjax () {
  3685. helper.config.blockAjax.enabled = false;
  3686. ajaxHooks.unHook();
  3687. debug.log(`${i18n.t('debugHelper.blockAjax.disable')} success`);
  3688. },
  3689.  
  3690. toggleBlockAjax () {
  3691. if (helper.config.blockAjax.enabled) {
  3692. functionCall.disableBlockAjax();
  3693. } else {
  3694. functionCall.useBlockAjax();
  3695. }
  3696. },
  3697.  
  3698. addMeasureSelectorInterval (selector1, selector2) {
  3699. let result = {};
  3700. if (!functionCall._measureSelectorArr) {
  3701. functionCall._measureSelectorArr = [];
  3702. }
  3703.  
  3704. function measure (element) {
  3705. // debug.log(`[measure] ${i18n.t('debugHelper.measureSelectorInterval')}`, element)
  3706.  
  3707. const selector1 = helper.config.measureSelectorInterval.selector1;
  3708. const selector2 = helper.config.measureSelectorInterval.selector2;
  3709. const selectorArr = [selector1, selector2];
  3710. selectorArr.forEach(selector => {
  3711. if (selector && element.parentElement && element.parentElement.querySelector(selector)) {
  3712. result[selector] = {
  3713. time: Date.now(),
  3714. element: element
  3715. };
  3716.  
  3717. debug.info(`${i18n.t('debugHelper.selectorReadyTips')}: ${selector}`, element);
  3718. }
  3719. });
  3720.  
  3721. if (Object.keys(result).length >= 2) {
  3722. const time = ((result[selector2].time - result[selector1].time) / 1000).toFixed(2);
  3723.  
  3724. debug.info(`[[${selector1}] -> [${selector2}]] time: ${time}s`);
  3725. result = {};
  3726. }
  3727. }
  3728.  
  3729. if (selector1 && selector2) {
  3730. helper.config.measureSelectorInterval.selector1 = selector1;
  3731. helper.config.measureSelectorInterval.selector2 = selector2;
  3732.  
  3733. const selectorArr = [selector1, selector2];
  3734. selectorArr.forEach(selector => {
  3735. if (!functionCall._measureSelectorArr.includes(selector)) {
  3736. // 防止重复注册(不可用)
  3737. functionCall._measureSelectorArr.push(selector);
  3738.  
  3739. ready(selector, measure);
  3740. }
  3741. });
  3742. } else {
  3743. debug.log('selector is empty, please input selector');
  3744. }
  3745. },
  3746.  
  3747. initMeasureSelectorInterval () {
  3748. const selector1 = helper.config.measureSelectorInterval.selector1;
  3749. const selector2 = helper.config.measureSelectorInterval.selector2;
  3750. if (selector1 && selector2) {
  3751. functionCall.addMeasureSelectorInterval(selector1, selector2);
  3752. debug.log('[measureSelectorInterval] init success');
  3753. }
  3754. },
  3755.  
  3756. measureSelectorInterval () {
  3757. const selector1 = window.prompt(i18n.t('debugHelper.measureSelectorIntervalPrompt.selector1'), helper.config.measureSelectorInterval.selector1);
  3758. const selector2 = window.prompt(i18n.t('debugHelper.measureSelectorIntervalPrompt.selector2'), helper.config.measureSelectorInterval.selector2);
  3759.  
  3760. if (!selector1 && !selector2) {
  3761. helper.config.measureSelectorInterval.selector1 = '';
  3762. helper.config.measureSelectorInterval.selector2 = '';
  3763. }
  3764.  
  3765. functionCall.addMeasureSelectorInterval(selector1, selector2);
  3766. },
  3767.  
  3768. toggleSimplifyMode () {
  3769. helper.config.contextMenu.simplify = !helper.config.contextMenu.simplify;
  3770. const msg = helper.config.contextMenu.simplify ? i18n.t('debugHelper.simplifyMode.enabled') : i18n.t('debugHelper.simplifyMode.disable');
  3771. debug.log(`${msg} success`);
  3772. }
  3773. };
  3774.  
  3775. /*!
  3776. * @name menu.js
  3777. * @description vue-debug-helper的菜单配置
  3778. * @version 0.0.1
  3779. * @author xxxily
  3780. * @date 2022/04/25 22:28
  3781. * @github https://github.com/xxxily
  3782. */
  3783.  
  3784. const vueStatus = {
  3785. status: ''
  3786. };
  3787.  
  3788. function menuRegister (vueDetectStatus) {
  3789. vueStatus.status = vueDetectStatus;
  3790.  
  3791. function menuBuilder () {
  3792. const conf = helper.config;
  3793. let menuList = [];
  3794.  
  3795. if (vueStatus.status) {
  3796. if (vueStatus.status === 'initing') {
  3797. menuList.push({
  3798. title: 'Vue Detecting...',
  3799. fn: () => { debug.log('Vue Detecting...'); }
  3800. });
  3801. } else if (vueStatus.status === 'failed') {
  3802. menuList.push({
  3803. title: 'Vue not detected',
  3804. fn: () => { debug.log('Vue not detected'); }
  3805. });
  3806. } else if (vueStatus.status === 'success') {
  3807. const vueMenu = [
  3808. {
  3809. title: conf.inspect.enabled ? i18n.t('debugHelper.inspectStatus.off') : i18n.t('debugHelper.inspectStatus.on'),
  3810. fn: () => { functionCall.toggleInspect(); }
  3811. },
  3812. {
  3813. title: i18n.t('debugHelper.viewVueDebugHelperObject'),
  3814. fn: () => { functionCall.viewVueDebugHelperObject(); }
  3815. },
  3816. {
  3817. title: i18n.t('debugHelper.componentsStatistics'),
  3818. fn: () => { functionCall.componentsStatistics(); }
  3819. },
  3820. {
  3821. title: i18n.t('debugHelper.componentsSummaryStatisticsSort'),
  3822. fn: () => { functionCall.componentsSummaryStatisticsSort(); }
  3823. },
  3824. {
  3825. title: i18n.t('debugHelper.destroyStatisticsSort'),
  3826. fn: () => { functionCall.destroyStatisticsSort(); }
  3827. },
  3828. {
  3829. title: i18n.t('debugHelper.clearAll'),
  3830. fn: () => { functionCall.clearAll(); }
  3831. },
  3832. {
  3833. title: i18n.t('debugHelper.getDestroyByDuration'),
  3834. fn: () => { functionCall.getDestroyByDuration(); }
  3835. },
  3836. {
  3837. title: i18n.t('debugHelper.findComponents'),
  3838. fn: () => { functionCall.findComponents(); }
  3839. },
  3840. {
  3841. title: i18n.t('debugHelper.blockComponents'),
  3842. fn: () => { functionCall.blockComponents(); }
  3843. },
  3844. {
  3845. title: conf.lifecycle.show ? i18n.t('debugHelper.notPrintLifeCycleInfo') : i18n.t('debugHelper.printLifeCycleInfo'),
  3846. fn: () => { conf.lifecycle.show ? functionCall.notPrintLifeCycleInfo() : functionCall.printLifeCycleInfo(); }
  3847. },
  3848. {
  3849. title: conf.dd.enabled ? i18n.t('debugHelper.undd') : i18n.t('debugHelper.dd'),
  3850. fn: () => { conf.dd.enabled ? functionCall.undd() : functionCall.dd(); }
  3851. },
  3852. {
  3853. title: conf.hackVueComponent ? i18n.t('debugHelper.hackVueComponent.unhack') : i18n.t('debugHelper.hackVueComponent.hack'),
  3854. fn: () => { functionCall.toggleHackVueComponent(); }
  3855. },
  3856. {
  3857. title: helper.config.devtools ? i18n.t('debugHelper.devtools.disable') : i18n.t('debugHelper.devtools.enabled'),
  3858. fn: () => { helper.methods.toggleDevtools(); }
  3859. }
  3860. ];
  3861. menuList = menuList.concat(vueMenu);
  3862. }
  3863. }
  3864.  
  3865. const commonMenu = [
  3866. {
  3867. title: conf.ajaxCache.enabled ? i18n.t('debugHelper.ajaxCacheStatus.off') : i18n.t('debugHelper.ajaxCacheStatus.on'),
  3868. fn: () => { functionCall.toggleAjaxCache(); }
  3869. },
  3870. {
  3871. title: i18n.t('debugHelper.clearAjaxCache'),
  3872. fn: () => { functionCall.clearAjaxCache(); }
  3873. },
  3874. {
  3875. title: conf.blockAjax.enabled ? i18n.t('debugHelper.blockAjax.disable') : i18n.t('debugHelper.blockAjax.enabled'),
  3876. fn: () => { functionCall.toggleBlockAjax(); }
  3877. },
  3878. {
  3879. title: conf.performanceObserver.enabled ? i18n.t('debugHelper.performanceObserverStatus.off') : i18n.t('debugHelper.performanceObserverStatus.on'),
  3880. fn: () => { functionCall.togglePerformanceObserver(); }
  3881. },
  3882. {
  3883. title: i18n.t('debugHelper.measureSelectorInterval'),
  3884. fn: () => { functionCall.measureSelectorInterval(); }
  3885. },
  3886. {
  3887. title: i18n.t('issues'),
  3888. fn: () => { openInTab('https://github.com/xxxily/vue-debug-helper/issues'); }
  3889. },
  3890. {
  3891. disable: true,
  3892. title: i18n.t('donate'),
  3893. fn: () => { openInTab('https://cdn.jsdelivr.net/gh/xxxily/vue-debug-helper@main/donate.png'); }
  3894. }
  3895. ];
  3896.  
  3897. menuList = menuList.concat(commonMenu);
  3898. return menuList
  3899. }
  3900.  
  3901. /* 注册(不可用)动态菜单 */
  3902. monkeyMenu.build(menuBuilder);
  3903. }
  3904.  
  3905. const isff = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase().indexOf('firefox') > 0 : false;
  3906.  
  3907. // 绑定事件
  3908. function addEvent (object, event, method) {
  3909. if (object.addEventListener) {
  3910. object.addEventListener(event, method, false);
  3911. } else if (object.attachEvent) {
  3912. object.attachEvent(`on${event}`, () => { method(window.event); });
  3913. }
  3914. }
  3915.  
  3916. // 修饰键转换成对应的键码
  3917. function getMods (modifier, key) {
  3918. const mods = key.slice(0, key.length - 1);
  3919. for (let i = 0; i < mods.length; i++) mods[i] = modifier[mods[i].toLowerCase()];
  3920. return mods
  3921. }
  3922.  
  3923. // 处理传的key字符串转换成数组
  3924. function getKeys (key) {
  3925. if (typeof key !== 'string') key = '';
  3926. key = key.replace(/\s/g, ''); // 匹配任何空白字符,包括空格、制表符、换页符等等
  3927. const keys = key.split(','); // 同时设置多个快捷键,以','分割
  3928. let index = keys.lastIndexOf('');
  3929.  
  3930. // 快捷键可能包含',',需特殊处理
  3931. for (; index >= 0;) {
  3932. keys[index - 1] += ',';
  3933. keys.splice(index, 1);
  3934. index = keys.lastIndexOf('');
  3935. }
  3936.  
  3937. return keys
  3938. }
  3939.  
  3940. // 比较修饰键的数组
  3941. function compareArray (a1, a2) {
  3942. const arr1 = a1.length >= a2.length ? a1 : a2;
  3943. const arr2 = a1.length >= a2.length ? a2 : a1;
  3944. let isIndex = true;
  3945.  
  3946. for (let i = 0; i < arr1.length; i++) {
  3947. if (arr2.indexOf(arr1[i]) === -1) isIndex = false;
  3948. }
  3949. return isIndex
  3950. }
  3951.  
  3952. // Special Keys
  3953. const _keyMap = {
  3954. backspace: 8,
  3955. tab: 9,
  3956. clear: 12,
  3957. enter: 13,
  3958. return: 13,
  3959. esc: 27,
  3960. escape: 27,
  3961. space: 32,
  3962. left: 37,
  3963. up: 38,
  3964. right: 39,
  3965. down: 40,
  3966. del: 46,
  3967. delete: 46,
  3968. ins: 45,
  3969. insert: 45,
  3970. home: 36,
  3971. end: 35,
  3972. pageup: 33,
  3973. pagedown: 34,
  3974. capslock: 20,
  3975. num_0: 96,
  3976. num_1: 97,
  3977. num_2: 98,
  3978. num_3: 99,
  3979. num_4: 100,
  3980. num_5: 101,
  3981. num_6: 102,
  3982. num_7: 103,
  3983. num_8: 104,
  3984. num_9: 105,
  3985. num_multiply: 106,
  3986. num_add: 107,
  3987. num_enter: 108,
  3988. num_subtract: 109,
  3989. num_decimal: 110,
  3990. num_divide: 111,
  3991. '⇪': 20,
  3992. ',': 188,
  3993. '.': 190,
  3994. '/': 191,
  3995. '`': 192,
  3996. '-': isff ? 173 : 189,
  3997. '=': isff ? 61 : 187,
  3998. ';': isff ? 59 : 186,
  3999. '\'': 222,
  4000. '[': 219,
  4001. ']': 221,
  4002. '\\': 220
  4003. };
  4004.  
  4005. // Modifier Keys
  4006. const _modifier = {
  4007. // shiftKey
  4008. '⇧': 16,
  4009. shift: 16,
  4010. // altKey
  4011. '⌥': 18,
  4012. alt: 18,
  4013. option: 18,
  4014. // ctrlKey
  4015. '⌃': 17,
  4016. ctrl: 17,
  4017. control: 17,
  4018. // metaKey
  4019. '⌘': 91,
  4020. cmd: 91,
  4021. command: 91
  4022. };
  4023. const modifierMap = {
  4024. 16: 'shiftKey',
  4025. 18: 'altKey',
  4026. 17: 'ctrlKey',
  4027. 91: 'metaKey',
  4028.  
  4029. shiftKey: 16,
  4030. ctrlKey: 17,
  4031. altKey: 18,
  4032. metaKey: 91
  4033. };
  4034. const _mods = {
  4035. 16: false,
  4036. 18: false,
  4037. 17: false,
  4038. 91: false
  4039. };
  4040. const _handlers = {};
  4041.  
  4042. // F1~F12 special key
  4043. for (let k = 1; k < 20; k++) {
  4044. _keyMap[`f${k}`] = 111 + k;
  4045. }
  4046.  
  4047. // https://github.com/jaywcjlove/hotkeys
  4048.  
  4049. let _downKeys = []; // 记录摁下的绑定键
  4050. let winListendFocus = false; // window是否已经监听了focus事件
  4051. let _scope = 'all'; // 默认热键范围
  4052. const elementHasBindEvent = []; // 已绑定事件的节点记录
  4053.  
  4054. // 返回键码
  4055. const code = (x) => _keyMap[x.toLowerCase()] ||
  4056. _modifier[x.toLowerCase()] ||
  4057. x.toUpperCase().charCodeAt(0);
  4058.  
  4059. // 设置获取当前范围(默认为'所有')
  4060. function setScope (scope) {
  4061. _scope = scope || 'all';
  4062. }
  4063. // 获取当前范围
  4064. function getScope () {
  4065. return _scope || 'all'
  4066. }
  4067. // 获取摁下绑定键的键值
  4068. function getPressedKeyCodes () {
  4069. return _downKeys.slice(0)
  4070. }
  4071.  
  4072. // 表单控件控件判断 返回 Boolean
  4073. // hotkey is effective only when filter return true
  4074. function filter (event) {
  4075. const target = event.target || event.srcElement;
  4076. const { tagName } = target;
  4077. let flag = true;
  4078. // ignore: isContentEditable === 'true', <input> and <textarea> when readOnly state is false, <select>
  4079. if (
  4080. target.isContentEditable ||
  4081. ((tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') && !target.readOnly)
  4082. ) {
  4083. flag = false;
  4084. }
  4085. return flag
  4086. }
  4087.  
  4088. // 判断摁下的键是否为某个键,返回true或者false
  4089. function isPressed (keyCode) {
  4090. if (typeof keyCode === 'string') {
  4091. keyCode = code(keyCode); // 转换成键码
  4092. }
  4093. return _downKeys.indexOf(keyCode) !== -1
  4094. }
  4095.  
  4096. // 循环删除handlers中的所有 scope(范围)
  4097. function deleteScope (scope, newScope) {
  4098. let handlers;
  4099. let i;
  4100.  
  4101. // 没有指定scope,获取scope
  4102. if (!scope) scope = getScope();
  4103.  
  4104. for (const key in _handlers) {
  4105. if (Object.prototype.hasOwnProperty.call(_handlers, key)) {
  4106. handlers = _handlers[key];
  4107. for (i = 0; i < handlers.length;) {
  4108. if (handlers[i].scope === scope) handlers.splice(i, 1);
  4109. else i++;
  4110. }
  4111. }
  4112. }
  4113.  
  4114. // 如果scope被删除,将scope重置为all
  4115. if (getScope() === scope) setScope(newScope || 'all');
  4116. }
  4117.  
  4118. // 清除修饰键
  4119. function clearModifier (event) {
  4120. let key = event.keyCode || event.which || event.charCode;
  4121. const i = _downKeys.indexOf(key);
  4122.  
  4123. // 从列表中清除按压过的键
  4124. if (i >= 0) {
  4125. _downKeys.splice(i, 1);
  4126. }
  4127. // 特殊处理 cmmand 键,在 cmmand 组合快捷键 keyup 只执行一次的问题
  4128. if (event.key && event.key.toLowerCase() === 'meta') {
  4129. _downKeys.splice(0, _downKeys.length);
  4130. }
  4131.  
  4132. // 修饰键 shiftKey altKey ctrlKey (command||metaKey) 清除
  4133. if (key === 93 || key === 224) key = 91;
  4134. if (key in _mods) {
  4135. _mods[key] = false;
  4136.  
  4137. // 将修饰键重置为false
  4138. for (const k in _modifier) if (_modifier[k] === key) hotkeys[k] = false;
  4139. }
  4140. }
  4141.  
  4142. function unbind (keysInfo, ...args) {
  4143. // unbind(), unbind all keys
  4144. if (!keysInfo) {
  4145. Object.keys(_handlers).forEach((key) => delete _handlers[key]);
  4146. } else if (Array.isArray(keysInfo)) {
  4147. // support like : unbind([{key: 'ctrl+a', scope: 's1'}, {key: 'ctrl-a', scope: 's2', splitKey: '-'}])
  4148. keysInfo.forEach((info) => {
  4149. if (info.key) eachUnbind(info);
  4150. });
  4151. } else if (typeof keysInfo === 'object') {
  4152. // support like unbind({key: 'ctrl+a, ctrl+b', scope:'abc'})
  4153. if (keysInfo.key) eachUnbind(keysInfo);
  4154. } else if (typeof keysInfo === 'string') {
  4155. // support old method
  4156. // eslint-disable-line
  4157. let [scope, method] = args;
  4158. if (typeof scope === 'function') {
  4159. method = scope;
  4160. scope = '';
  4161. }
  4162. eachUnbind({
  4163. key: keysInfo,
  4164. scope,
  4165. method,
  4166. splitKey: '+'
  4167. });
  4168. }
  4169. }
  4170.  
  4171. // 解除绑定某个范围的快捷键
  4172. const eachUnbind = ({
  4173. key, scope, method, splitKey = '+'
  4174. }) => {
  4175. const multipleKeys = getKeys(key);
  4176. multipleKeys.forEach((originKey) => {
  4177. const unbindKeys = originKey.split(splitKey);
  4178. const len = unbindKeys.length;
  4179. const lastKey = unbindKeys[len - 1];
  4180. const keyCode = lastKey === '*' ? '*' : code(lastKey);
  4181. if (!_handlers[keyCode]) return
  4182. // 判断是否传入范围,没有就获取范围
  4183. if (!scope) scope = getScope();
  4184. const mods = len > 1 ? getMods(_modifier, unbindKeys) : [];
  4185. _handlers[keyCode] = _handlers[keyCode].filter((record) => {
  4186. // 通过函数判断,是否解除绑定,函数相等直接返回
  4187. const isMatchingMethod = method ? record.method === method : true;
  4188. return !(
  4189. isMatchingMethod &&
  4190. record.scope === scope &&
  4191. compareArray(record.mods, mods)
  4192. )
  4193. });
  4194. });
  4195. };
  4196.  
  4197. // 对监听对应快捷键的回调函数进行处理
  4198. function eventHandler (event, handler, scope, element) {
  4199. if (handler.element !== element) {
  4200. return
  4201. }
  4202. let modifiersMatch;
  4203.  
  4204. // 看它是否在当前范围
  4205. if (handler.scope === scope || handler.scope === 'all') {
  4206. // 检查是否匹配修饰符(如果有返回true)
  4207. modifiersMatch = handler.mods.length > 0;
  4208.  
  4209. for (const y in _mods) {
  4210. if (Object.prototype.hasOwnProperty.call(_mods, y)) {
  4211. if (
  4212. (!_mods[y] && handler.mods.indexOf(+y) > -1) ||
  4213. (_mods[y] && handler.mods.indexOf(+y) === -1)
  4214. ) {
  4215. modifiersMatch = false;
  4216. }
  4217. }
  4218. }
  4219.  
  4220. // 调用处理程序,如果是修饰键不做处理
  4221. if (
  4222. (handler.mods.length === 0 &&
  4223. !_mods[16] &&
  4224. !_mods[18] &&
  4225. !_mods[17] &&
  4226. !_mods[91]) ||
  4227. modifiersMatch ||
  4228. handler.shortcut === '*'
  4229. ) {
  4230. if (handler.method(event, handler) === false) {
  4231. if (event.preventDefault) event.preventDefault();
  4232. else event.returnValue = false;
  4233. if (event.stopPropagation) event.stopPropagation();
  4234. if (event.cancelBubble) event.cancelBubble = true;
  4235. }
  4236. }
  4237. }
  4238. }
  4239.  
  4240. // 处理keydown事件
  4241. function dispatch (event, element) {
  4242. const asterisk = _handlers['*'];
  4243. let key = event.keyCode || event.which || event.charCode;
  4244.  
  4245. // 表单控件过滤 默认表单控件不触发快捷键
  4246. if (!hotkeys.filter.call(this, event)) return
  4247.  
  4248. // Gecko(Firefox)的command键值224,在Webkit(Chrome)中保持一致
  4249. // Webkit左右 command 键值不一样
  4250. if (key === 93 || key === 224) key = 91;
  4251.  
  4252. /**
  4253. * Collect bound keys
  4254. * If an Input Method Editor is processing key input and the event is keydown, return 229.
  4255. * https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229
  4256. * http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
  4257. */
  4258. if (_downKeys.indexOf(key) === -1 && key !== 229) _downKeys.push(key);
  4259. /**
  4260. * Jest test cases are required.
  4261. * ===============================
  4262. */
  4263. ['ctrlKey', 'altKey', 'shiftKey', 'metaKey'].forEach((keyName) => {
  4264. const keyNum = modifierMap[keyName];
  4265. if (event[keyName] && _downKeys.indexOf(keyNum) === -1) {
  4266. _downKeys.push(keyNum);
  4267. } else if (!event[keyName] && _downKeys.indexOf(keyNum) > -1) {
  4268. _downKeys.splice(_downKeys.indexOf(keyNum), 1);
  4269. } else if (keyName === 'metaKey' && event[keyName] && _downKeys.length === 3) {
  4270. /**
  4271. * Fix if Command is pressed:
  4272. * ===============================
  4273. */
  4274. if (!(event.ctrlKey || event.shiftKey || event.altKey)) {
  4275. _downKeys = _downKeys.slice(_downKeys.indexOf(keyNum));
  4276. }
  4277. }
  4278. });
  4279. /**
  4280. * -------------------------------
  4281. */
  4282.  
  4283. if (key in _mods) {
  4284. _mods[key] = true;
  4285.  
  4286. // 将特殊字符的key注册(不可用)到 hotkeys 上
  4287. for (const k in _modifier) {
  4288. if (_modifier[k] === key) hotkeys[k] = true;
  4289. }
  4290.  
  4291. if (!asterisk) return
  4292. }
  4293.  
  4294. // 将 modifierMap 里面的修饰键绑定到 event 中
  4295. for (const e in _mods) {
  4296. if (Object.prototype.hasOwnProperty.call(_mods, e)) {
  4297. _mods[e] = event[modifierMap[e]];
  4298. }
  4299. }
  4300. /**
  4301. * https://github.com/jaywcjlove/hotkeys/pull/129
  4302. * This solves the issue in Firefox on Windows where hotkeys corresponding to special characters would not trigger.
  4303. * An example of this is ctrl+alt+m on a Swedish keyboard which is used to type μ.
  4304. * Browser support: https://caniuse.com/#feat=keyboardevent-getmodifierstate
  4305. */
  4306. if (event.getModifierState && (!(event.altKey && !event.ctrlKey) && event.getModifierState('AltGraph'))) {
  4307. if (_downKeys.indexOf(17) === -1) {
  4308. _downKeys.push(17);
  4309. }
  4310.  
  4311. if (_downKeys.indexOf(18) === -1) {
  4312. _downKeys.push(18);
  4313. }
  4314.  
  4315. _mods[17] = true;
  4316. _mods[18] = true;
  4317. }
  4318.  
  4319. // 获取范围 默认为 `all`
  4320. const scope = getScope();
  4321. // 对任何快捷键都需要做的处理
  4322. if (asterisk) {
  4323. for (let i = 0; i < asterisk.length; i++) {
  4324. if (
  4325. asterisk[i].scope === scope &&
  4326. ((event.type === 'keydown' && asterisk[i].keydown) ||
  4327. (event.type === 'keyup' && asterisk[i].keyup))
  4328. ) {
  4329. eventHandler(event, asterisk[i], scope, element);
  4330. }
  4331. }
  4332. }
  4333. // key 不在 _handlers 中返回
  4334. if (!(key in _handlers)) return
  4335.  
  4336. for (let i = 0; i < _handlers[key].length; i++) {
  4337. if (
  4338. (event.type === 'keydown' && _handlers[key][i].keydown) ||
  4339. (event.type === 'keyup' && _handlers[key][i].keyup)
  4340. ) {
  4341. if (_handlers[key][i].key) {
  4342. const record = _handlers[key][i];
  4343. const { splitKey } = record;
  4344. const keyShortcut = record.key.split(splitKey);
  4345. const _downKeysCurrent = []; // 记录当前按键键值
  4346. for (let a = 0; a < keyShortcut.length; a++) {
  4347. _downKeysCurrent.push(code(keyShortcut[a]));
  4348. }
  4349. if (_downKeysCurrent.sort().join('') === _downKeys.sort().join('')) {
  4350. // 找到处理内容
  4351. eventHandler(event, record, scope, element);
  4352. }
  4353. }
  4354. }
  4355. }
  4356. }
  4357.  
  4358. // 判断 element 是否已经绑定事件
  4359. function isElementBind (element) {
  4360. return elementHasBindEvent.indexOf(element) > -1
  4361. }
  4362.  
  4363. function hotkeys (key, option, method) {
  4364. _downKeys = [];
  4365. const keys = getKeys(key); // 需要处理的快捷键列表
  4366. let mods = [];
  4367. let scope = 'all'; // scope默认为all,所有范围都有效
  4368. let element = document; // 快捷键事件绑定节点
  4369. let i = 0;
  4370. let keyup = false;
  4371. let keydown = true;
  4372. let splitKey = '+';
  4373.  
  4374. // 对为设定范围的判断
  4375. if (method === undefined && typeof option === 'function') {
  4376. method = option;
  4377. }
  4378.  
  4379. if (Object.prototype.toString.call(option) === '[object Object]') {
  4380. if (option.scope) scope = option.scope; // eslint-disable-line
  4381. if (option.element) element = option.element; // eslint-disable-line
  4382. if (option.keyup) keyup = option.keyup; // eslint-disable-line
  4383. if (option.keydown !== undefined) keydown = option.keydown; // eslint-disable-line
  4384. if (typeof option.splitKey === 'string') splitKey = option.splitKey; // eslint-disable-line
  4385. }
  4386.  
  4387. if (typeof option === 'string') scope = option;
  4388.  
  4389. // 对于每个快捷键进行处理
  4390. for (; i < keys.length; i++) {
  4391. key = keys[i].split(splitKey); // 按键列表
  4392. mods = [];
  4393.  
  4394. // 如果是组合快捷键取得组合快捷键
  4395. if (key.length > 1) mods = getMods(_modifier, key);
  4396.  
  4397. // 将非修饰键转化为键码
  4398. key = key[key.length - 1];
  4399. key = key === '*' ? '*' : code(key); // *表示匹配所有快捷键
  4400.  
  4401. // 判断key是否在_handlers中,不在就赋一个空数组
  4402. if (!(key in _handlers)) _handlers[key] = [];
  4403. _handlers[key].push({
  4404. keyup,
  4405. keydown,
  4406. scope,
  4407. mods,
  4408. shortcut: keys[i],
  4409. method,
  4410. key: keys[i],
  4411. splitKey,
  4412. element
  4413. });
  4414. }
  4415. // 在全局document上设置快捷键
  4416. if (typeof element !== 'undefined' && !isElementBind(element) && window) {
  4417. elementHasBindEvent.push(element);
  4418. addEvent(element, 'keydown', (e) => {
  4419. dispatch(e, element);
  4420. });
  4421. if (!winListendFocus) {
  4422. winListendFocus = true;
  4423. addEvent(window, 'focus', () => {
  4424. _downKeys = [];
  4425. });
  4426. }
  4427. addEvent(element, 'keyup', (e) => {
  4428. dispatch(e, element);
  4429. clearModifier(e);
  4430. });
  4431. }
  4432. }
  4433.  
  4434. function trigger (shortcut, scope = 'all') {
  4435. Object.keys(_handlers).forEach((key) => {
  4436. const data = _handlers[key].find((item) => item.scope === scope && item.shortcut === shortcut);
  4437. if (data && data.method) {
  4438. data.method();
  4439. }
  4440. });
  4441. }
  4442.  
  4443. const _api = {
  4444. setScope,
  4445. getScope,
  4446. deleteScope,
  4447. getPressedKeyCodes,
  4448. isPressed,
  4449. filter,
  4450. trigger,
  4451. unbind,
  4452. keyMap: _keyMap,
  4453. modifier: _modifier,
  4454. modifierMap
  4455. };
  4456. for (const a in _api) {
  4457. if (Object.prototype.hasOwnProperty.call(_api, a)) {
  4458. hotkeys[a] = _api[a];
  4459. }
  4460. }
  4461.  
  4462. if (typeof window !== 'undefined') {
  4463. const _hotkeys = window.hotkeys;
  4464. hotkeys.noConflict = (deep) => {
  4465. if (deep && window.hotkeys === hotkeys) {
  4466. window.hotkeys = _hotkeys;
  4467. }
  4468. return hotkeys
  4469. };
  4470. window.hotkeys = hotkeys;
  4471. }
  4472.  
  4473. /*!
  4474. * @name hotKeyRegister.js
  4475. * @description vue-debug-helper的快捷键配置
  4476. * @version 0.0.1
  4477. * @author xxxily
  4478. * @date 2022/04/26 14:37
  4479. * @github https://github.com/xxxily
  4480. */
  4481.  
  4482. function hotKeyRegister () {
  4483. const hotKeyMap = {
  4484. 'shift+alt+i': functionCall.toggleInspect,
  4485. 'shift+alt+a,shift+alt+ctrl+a': functionCall.componentsSummaryStatisticsSort,
  4486. 'shift+alt+l': functionCall.componentsStatistics,
  4487. 'shift+alt+d': functionCall.destroyStatisticsSort,
  4488. 'shift+alt+c': functionCall.clearAll,
  4489. 'shift+alt+e': function (event, handler) {
  4490. if (helper.config.dd.enabled) {
  4491. functionCall.undd();
  4492. } else {
  4493. functionCall.dd();
  4494. }
  4495. }
  4496. };
  4497.  
  4498. Object.keys(hotKeyMap).forEach(key => {
  4499. hotkeys(key, hotKeyMap[key]);
  4500. });
  4501. }
  4502.  
  4503. /*!
  4504. * @name vueDetector.js
  4505. * @description 检测页面是否存在Vue对象
  4506. * @version 0.0.1
  4507. * @author xxxily
  4508. * @date 2022/04/27 11:43
  4509. * @github https://github.com/xxxily
  4510. */
  4511.  
  4512. function mutationDetector (callback, shadowRoot) {
  4513. const win = window;
  4514. const MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
  4515. const docRoot = shadowRoot || win.document.documentElement;
  4516. const maxDetectTries = 1500;
  4517. const timeout = 1000 * 10;
  4518. const startTime = Date.now();
  4519. let detectCount = 0;
  4520. let detectStatus = false;
  4521.  
  4522. if (!MutationObserver) {
  4523. debug.warn('MutationObserver is not supported in this browser');
  4524. return false
  4525. }
  4526.  
  4527. let mObserver = null;
  4528. const mObserverCallback = (mutationsList, observer) => {
  4529. if (detectStatus) {
  4530. return
  4531. }
  4532.  
  4533. /* 超时或检测次数过多,取消监听 */
  4534. if (Date.now() - startTime > timeout || detectCount > maxDetectTries) {
  4535. debug.warn('mutationDetector timeout or detectCount > maxDetectTries, stop detect');
  4536. if (mObserver && mObserver.disconnect) {
  4537. mObserver.disconnect();
  4538. mObserver = null;
  4539. }
  4540. }
  4541.  
  4542. for (let i = 0; i < mutationsList.length; i++) {
  4543. detectCount++;
  4544. const mutation = mutationsList[i];
  4545. if (mutation.target && mutation.target.__vue__) {
  4546. let Vue = Object.getPrototypeOf(mutation.target.__vue__).constructor;
  4547. while (Vue.super) {
  4548. Vue = Vue.super;
  4549. }
  4550.  
  4551. /* 检测成功后销毁观察对象 */
  4552. if (mObserver && mObserver.disconnect) {
  4553. mObserver.disconnect();
  4554. mObserver = null;
  4555. }
  4556.  
  4557. detectStatus = true;
  4558. callback && callback(Vue);
  4559. break
  4560. }
  4561. }
  4562. };
  4563.  
  4564. mObserver = new MutationObserver(mObserverCallback);
  4565. mObserver.observe(docRoot, {
  4566. attributes: true,
  4567. childList: true,
  4568. subtree: true
  4569. });
  4570. }
  4571.  
  4572. /**
  4573. * 检测页面是否存在Vue对象,方法参考:https://github.com/vuejs/devtools/blob/main/packages/shell-chrome/src/detector.js
  4574. * @param {window} win windwod对象
  4575. * @param {function} callback 检测到Vue对象后的回调函数
  4576. */
  4577. function vueDetect (win, callback) {
  4578. let delay = 1000;
  4579. let detectRemainingTries = 10;
  4580. let detectSuc = false;
  4581.  
  4582. // Method 1: MutationObserver detector
  4583. mutationDetector((Vue) => {
  4584. if (!detectSuc) {
  4585. debug.info(`------------- Vue mutation detected (${Vue.version}) -------------`);
  4586. detectSuc = true;
  4587. callback(Vue);
  4588. }
  4589. });
  4590.  
  4591. function runDetect () {
  4592. if (detectSuc) {
  4593. return false
  4594. }
  4595.  
  4596. // Method 2: Check Vue 3
  4597. const vueDetected = !!(win.__VUE__);
  4598. if (vueDetected) {
  4599. debug.info(`------------- Vue global detected (${win.__VUE__.version}) -------------`);
  4600. detectSuc = true;
  4601. callback(win.__VUE__);
  4602. return
  4603. }
  4604.  
  4605. // Method 3: Scan all elements inside document
  4606. const all = document.querySelectorAll('*');
  4607. let el;
  4608. for (let i = 0; i < all.length; i++) {
  4609. if (all[i].__vue__) {
  4610. el = all[i];
  4611. break
  4612. }
  4613. }
  4614. if (el) {
  4615. let Vue = Object.getPrototypeOf(el.__vue__).constructor;
  4616. while (Vue.super) {
  4617. Vue = Vue.super;
  4618. }
  4619. debug.info(`------------- Vue dom detected (${Vue.version}) -------------`);
  4620. detectSuc = true;
  4621. callback(Vue);
  4622. return
  4623. }
  4624.  
  4625. if (detectRemainingTries > 0) {
  4626. detectRemainingTries--;
  4627.  
  4628. if (detectRemainingTries >= 7) {
  4629. setTimeout(() => {
  4630. runDetect();
  4631. }, 40);
  4632. } else {
  4633. setTimeout(() => {
  4634. runDetect();
  4635. }, delay);
  4636. delay *= 5;
  4637. }
  4638. }
  4639. }
  4640.  
  4641. setTimeout(() => {
  4642. runDetect();
  4643. }, 40);
  4644. }
  4645.  
  4646. /*!
  4647. * @name vueConfig.js
  4648. * @description 对Vue的配置进行修改
  4649. * @version 0.0.1
  4650. * @author xxxily
  4651. * @date 2022/05/10 15:15
  4652. * @github https://github.com/xxxily
  4653. */
  4654.  
  4655. function vueConfigInit (Vue, config) {
  4656. if (Vue.config) {
  4657. /* 自动开启Vue的调试模式 */
  4658. if (config.devtools) {
  4659. Vue.config.debug = true;
  4660. Vue.config.devtools = true;
  4661. Vue.config.performance = true;
  4662.  
  4663. setTimeout(() => {
  4664. const devtools = getVueDevtools();
  4665. if (devtools) {
  4666. if (!devtools.enabled) {
  4667. if (/^3\.*/.test(Vue.version)) {
  4668. // https://github.com/vuejs/core/blob/main/packages/runtime-core/src/devtools.ts
  4669. devtools.emit('app:init', Vue, Vue.version, {
  4670. Fragment: 'Fragment',
  4671. Text: 'Text',
  4672. Comment: 'Comment',
  4673. Static: 'Static'
  4674. });
  4675.  
  4676. const unmount = Vue.unmount.bind(Vue);
  4677. Vue.unmount = function () {
  4678. devtools.emit('app:unmount', Vue);
  4679. unmount();
  4680. };
  4681. } else {
  4682. // https://github.com/vuejs/vue/blob/dev/src/platforms/web/runtime/index.js
  4683. devtools.emit('init', Vue);
  4684.  
  4685. // 注册(不可用)vuex store,参考vuex源码
  4686. if (Vue.$store) {
  4687. Vue.$store._devtoolHook = devtools;
  4688. devtools.emit('vuex:init', Vue.$store);
  4689. devtools.on('vuex:travel-to-state', function (targetState) {
  4690. Vue.$store.replaceState(targetState);
  4691. });
  4692. Vue.$store.subscribe(function (mutation, state) {
  4693. devtools.emit('vuex:mutation', mutation, state);
  4694. });
  4695. }
  4696. }
  4697.  
  4698. debug.info('vue devtools init emit.');
  4699. }
  4700. } else {
  4701. debug.info('vue devtools check failed.');
  4702. }
  4703. }, 200);
  4704. } else {
  4705. Vue.config.debug = false;
  4706. Vue.config.devtools = false;
  4707. Vue.config.performance = false;
  4708. }
  4709. } else {
  4710. debug.log('Vue.config is not defined');
  4711. }
  4712. }
  4713.  
  4714. /**
  4715. * 判断是否处于Iframe中
  4716. * @returns {boolean}
  4717. */
  4718. function isInIframe () {
  4719. return window !== window.top
  4720. }
  4721.  
  4722. /**
  4723. * 由于tampermonkey对window对象进行了封装,我们实际访问到的window并非页面真实的window
  4724. * 这就导致了如果我们需要将某些对象挂载到页面的window进行调试的时候就无法挂载了
  4725. * 所以必须使用特殊手段才能访问到页面真实的window对象,于是就有了下面这个函数
  4726. * @returns {Promise<void>}
  4727. */
  4728. async function getPageWindow () {
  4729. return new Promise(function (resolve, reject) {
  4730. if (window._pageWindow) {
  4731. return resolve(window._pageWindow)
  4732. }
  4733.  
  4734. const listenEventList = ['load', 'mousemove', 'scroll', 'get-page-window-event'];
  4735.  
  4736. function getWin (event) {
  4737. window._pageWindow = this;
  4738. // debug.log('getPageWindow succeed', event)
  4739. listenEventList.forEach(eventType => {
  4740. window.removeEventListener(eventType, getWin, true);
  4741. });
  4742. resolve(window._pageWindow);
  4743. }
  4744.  
  4745. listenEventList.forEach(eventType => {
  4746. window.addEventListener(eventType, getWin, true);
  4747. });
  4748.  
  4749. /* 自行派发事件以便用最短的时候获得pageWindow对象 */
  4750. window.dispatchEvent(new window.Event('get-page-window-event'));
  4751. })
  4752. }
  4753. // getPageWindow()
  4754.  
  4755. /**
  4756. * 通过同步的方式获取pageWindow
  4757. * 注意同步获取的方式需要将脚本写入head,部分网站由于安全策略会导致写入失败,而无法正常获取
  4758. * @returns {*}
  4759. */
  4760. function getPageWindowSync () {
  4761. if (document._win_) return document._win_
  4762.  
  4763. const head = document.head || document.querySelector('head');
  4764. const script = document.createElement('script');
  4765. script.appendChild(document.createTextNode('document._win_ = window'));
  4766. head.appendChild(script);
  4767.  
  4768. return document._win_
  4769. }
  4770.  
  4771. let registerStatus = 'init';
  4772. window._debugMode_ = true;
  4773.  
  4774. /* 注入相关样式到页面 */
  4775. if (window.GM_getResourceText && window.GM_addStyle) {
  4776. const contextMenuCss = window.GM_getResourceText('contextMenuCss');
  4777. window.GM_addStyle(contextMenuCss);
  4778. }
  4779.  
  4780. function init (win) {
  4781. /* 注册(不可用)接口拦截功能和接口数据缓存功能 */
  4782. ajaxHooks.init(win);
  4783.  
  4784. if (isInIframe()) {
  4785. debug.log('running in iframe, skip init', window.location.href);
  4786. return false
  4787. }
  4788.  
  4789. if (registerStatus === 'initing') {
  4790. return false
  4791. }
  4792.  
  4793. /* 注册(不可用)性能观察的功能 */
  4794. performanceObserver.init();
  4795.  
  4796. /* 注册(不可用)选择器测量辅助功能 */
  4797. functionCall.initMeasureSelectorInterval();
  4798.  
  4799. registerStatus = 'initing';
  4800.  
  4801. /* 首次菜单注册(不可用) */
  4802. menuRegister('initing');
  4803.  
  4804. vueDetect(win, function (Vue) {
  4805. /* 挂载到window上,方便通过控制台调用调试 */
  4806. helper.Vue = Vue;
  4807. win.vueDebugHelper = helper;
  4808.  
  4809. /* 注册(不可用)阻断Vue组件的功能 */
  4810. vueHooks.blockComponents(Vue, helper.config);
  4811.  
  4812. /* 注册(不可用)打印全局组件注册(不可用)信息的功能 */
  4813. if (helper.config.hackVueComponent) {
  4814. vueHooks.hackVueComponent(Vue);
  4815. }
  4816.  
  4817. /* 对Vue相关配置进行初始化 */
  4818. vueConfigInit(Vue, helper.config);
  4819.  
  4820. mixinRegister(Vue);
  4821. menuRegister('success');
  4822. hotKeyRegister();
  4823.  
  4824. inspect.init(Vue);
  4825.  
  4826. debug.log('vue debug helper register success');
  4827. registerStatus = 'success';
  4828. });
  4829.  
  4830. setTimeout(() => {
  4831. if (registerStatus !== 'success') {
  4832. menuRegister('failed');
  4833. debug.warn('vue debug helper register failed, please check if vue is loaded .', win.location.href);
  4834. }
  4835. }, 1000 * 10);
  4836. }
  4837.  
  4838. let win$1 = null;
  4839. try {
  4840. win$1 = getPageWindowSync();
  4841. if (win$1) {
  4842. init(win$1);
  4843. }
  4844. } catch (e) {
  4845. debug.error('getPageWindowSync failed', e);
  4846. }
  4847. (async function () {
  4848. if (!win$1) {
  4849. win$1 = await getPageWindow();
  4850. init(win$1);
  4851. }
  4852. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址