vue-debug-helper

Vue components debug helper

As of 2022-05-13. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

  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.13
  10. // @description Vue components debug helper
  11. // @description:en Vue components debug helper
  12. // @description:zh Vue组件探测、统计、分析辅助脚本
  13. // @description:zh-TW Vue組件探測、統計、分析輔助腳本
  14. // @description:ja Vueコンポーネントの検出、統計、分析補助スクリプト
  15. // @author ankvps
  16. // @icon https://cdn.jsdelivr.net/gh/xxxily/vue-debug-helper@main/logo.png
  17. // @match http://*/*
  18. // @match https://*/*
  19. // @grant unsafeWindow
  20. // @grant GM_addStyle
  21. // @grant GM_setValue
  22. // @grant GM_getValue
  23. // @grant GM_deleteValue
  24. // @grant GM_listValues
  25. // @grant GM_addValueChangeListener
  26. // @grant GM_removeValueChangeListener
  27. // @grant GM_registerMenuCommand
  28. // @grant GM_unregisterMenuCommand
  29. // @grant GM_getTab
  30. // @grant GM_saveTab
  31. // @grant GM_getTabs
  32. // @grant GM_openInTab
  33. // @grant GM_download
  34. // @grant GM_xmlhttpRequest
  35. // @require https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js
  36. // @require https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/core.js
  37. // @require https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/md5.js
  38. // @run-at document-start
  39. // @connect 127.0.0.1
  40. // @license GPL
  41. // ==/UserScript==
  42. (function (w) { if (w) { w._vueDebugHelper_ = 'https://github.com/xxxily/vue-debug-helper'; } })();
  43.  
  44. class AssertionError extends Error {}
  45. AssertionError.prototype.name = 'AssertionError';
  46.  
  47. /**
  48. * Minimal assert function
  49. * @param {any} t Value to check if falsy
  50. * @param {string=} m Optional assertion error message
  51. * @throws {AssertionError}
  52. */
  53. function assert (t, m) {
  54. if (!t) {
  55. var err = new AssertionError(m);
  56. if (Error.captureStackTrace) Error.captureStackTrace(err, assert);
  57. throw err
  58. }
  59. }
  60.  
  61. /* eslint-env browser */
  62.  
  63. let ls;
  64. if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
  65. // A simple localStorage interface so that lsp works in SSR contexts. Not for persistant storage in node.
  66. const _nodeStorage = {};
  67. ls = {
  68. getItem (name) {
  69. return _nodeStorage[name] || null
  70. },
  71. setItem (name, value) {
  72. if (arguments.length < 2) throw new Error('Failed to execute \'setItem\' on \'Storage\': 2 arguments required, but only 1 present.')
  73. _nodeStorage[name] = (value).toString();
  74. },
  75. removeItem (name) {
  76. delete _nodeStorage[name];
  77. }
  78. };
  79. } else {
  80. ls = window.localStorage;
  81. }
  82.  
  83. var localStorageProxy = (name, opts = {}) => {
  84. assert(name, 'namepace required');
  85. const {
  86. defaults = {},
  87. lspReset = false,
  88. storageEventListener = true
  89. } = opts;
  90.  
  91. const state = new EventTarget();
  92. try {
  93. const restoredState = JSON.parse(ls.getItem(name)) || {};
  94. if (restoredState.lspReset !== lspReset) {
  95. ls.removeItem(name);
  96. for (const [k, v] of Object.entries({
  97. ...defaults
  98. })) {
  99. state[k] = v;
  100. }
  101. } else {
  102. for (const [k, v] of Object.entries({
  103. ...defaults,
  104. ...restoredState
  105. })) {
  106. state[k] = v;
  107. }
  108. }
  109. } catch (e) {
  110. console.error(e);
  111. ls.removeItem(name);
  112. }
  113.  
  114. state.lspReset = lspReset;
  115.  
  116. if (storageEventListener && typeof window !== 'undefined' && typeof window.addEventListener !== 'undefined') {
  117. state.addEventListener('storage', (ev) => {
  118. // Replace state with whats stored on localStorage... it is newer.
  119. for (const k of Object.keys(state)) {
  120. delete state[k];
  121. }
  122. const restoredState = JSON.parse(ls.getItem(name)) || {};
  123. for (const [k, v] of Object.entries({
  124. ...defaults,
  125. ...restoredState
  126. })) {
  127. state[k] = v;
  128. }
  129. opts.lspReset = restoredState.lspReset;
  130. state.dispatchEvent(new Event('update'));
  131. });
  132. }
  133.  
  134. function boundHandler (rootRef) {
  135. return {
  136. get (obj, prop) {
  137. if (typeof obj[prop] === 'object' && obj[prop] !== null) {
  138. return new Proxy(obj[prop], boundHandler(rootRef))
  139. } else if (typeof obj[prop] === 'function' && obj === rootRef && prop !== 'constructor') {
  140. // this returns bound EventTarget functions
  141. return obj[prop].bind(obj)
  142. } else {
  143. return obj[prop]
  144. }
  145. },
  146. set (obj, prop, value) {
  147. obj[prop] = value;
  148. try {
  149. ls.setItem(name, JSON.stringify(rootRef));
  150. rootRef.dispatchEvent(new Event('update'));
  151. return true
  152. } catch (e) {
  153. console.error(e);
  154. return false
  155. }
  156. }
  157. }
  158. }
  159.  
  160. return new Proxy(state, boundHandler(state))
  161. };
  162.  
  163. /**
  164. * 对特定数据结构的对象进行排序
  165. * @param {object} obj 一个对象,其结构应该类似于:{key1: [], key2: []}
  166. * @param {boolean} reverse -可选 是否反转、降序排列,默认为false
  167. * @param {object} opts -可选 指定数组的配置项,默认为{key: 'key', value: 'value'}
  168. * @param {object} opts.key -可选 指定对象键名的别名,默认为'key'
  169. * @param {object} opts.value -可选 指定对象值的别名,默认为'value'
  170. * @returns {array} 返回一个数组,其结构应该类似于:[{key: key1, value: []}, {key: key2, value: []}]
  171. */
  172. const objSort = (obj, reverse, opts = { key: 'key', value: 'value' }) => {
  173. const arr = [];
  174. for (const key in obj) {
  175. if (Object.prototype.hasOwnProperty.call(obj, key) && Array.isArray(obj[key])) {
  176. const tmpObj = {};
  177. tmpObj[opts.key] = key;
  178. tmpObj[opts.value] = obj[key];
  179. arr.push(tmpObj);
  180. }
  181. }
  182.  
  183. arr.sort((a, b) => {
  184. return a[opts.value].length - b[opts.value].length
  185. });
  186.  
  187. reverse && arr.reverse();
  188. return arr
  189. };
  190.  
  191. /**
  192. * 根据指定长度创建空白数据
  193. * @param {number} size -可选 指str的重复次数,默认为1024次,如果str为单个单字节字符,则意味着默认产生1Mb的空白数据
  194. * @param {string|number|any} str - 可选 指定数据的字符串,默认为'd'
  195. */
  196. function createEmptyData (count = 1024, str = 'd') {
  197. const arr = [];
  198. arr.length = count + 1;
  199. return arr.join(str)
  200. }
  201.  
  202. /**
  203. * 将字符串分隔的过滤器转换为数组形式的过滤器
  204. * @param {string|array} filter - 必选 字符串或数组,字符串支持使用 , |符号对多个项进行分隔
  205. * @returns {array}
  206. */
  207. function toArrFilters (filter) {
  208. filter = filter || [];
  209.  
  210. /* 如果是字符串,则支持通过, | 两个符号来指定多个组件名称的过滤器 */
  211. if (typeof filter === 'string') {
  212. /* 移除前后的, |分隔符,防止出现空字符的过滤规则 */
  213. filter.replace(/^(,|\|)/, '').replace(/(,|\|)$/, '');
  214.  
  215. if (/\|/.test(filter)) {
  216. filter = filter.split('|');
  217. } else {
  218. filter = filter.split(',');
  219. }
  220. }
  221.  
  222. filter = filter.map(item => item.trim());
  223.  
  224. return filter
  225. }
  226.  
  227. /**
  228. * 字符串过滤器和字符串的匹配方法
  229. * @param {string} filter -必选 过滤器的字符串
  230. * @param {string} str -必选 要跟过滤字符串进行匹配的字符串
  231. * @returns
  232. */
  233. function stringMatch (filter, str) {
  234. let isMatch = false;
  235.  
  236. if (!filter || !str) {
  237. return isMatch
  238. }
  239.  
  240. filter = String(filter);
  241. str = String(str);
  242.  
  243. /* 带星表示进行模糊匹配,且不区分大小写 */
  244. if (/\*/.test(filter)) {
  245. filter = filter.replace(/\*/g, '').toLocaleLowerCase();
  246. if (str.toLocaleLowerCase().indexOf(filter) > -1) {
  247. isMatch = true;
  248. }
  249. } else if (str.includes(filter)) {
  250. isMatch = true;
  251. }
  252.  
  253. return isMatch
  254. }
  255.  
  256. /**
  257. * 判断某个字符串是否跟filters相匹配
  258. * @param {array|string} filters - 必选 字符串或数组,字符串支持使用 , |符号对多个项进行分隔
  259. * @param {string|number} str - 必选 一个字符串或数字,用于跟过滤器进行匹配判断
  260. */
  261. function filtersMatch (filters, str) {
  262. if (!filters || !str) {
  263. return false
  264. }
  265.  
  266. filters = Array.isArray(filters) ? filters : toArrFilters(filters);
  267. str = String(str);
  268.  
  269. let result = false;
  270. for (let i = 0; i < filters.length; i++) {
  271. const filter = String(filters[i]);
  272.  
  273. if (stringMatch(filter, str)) {
  274. result = true;
  275. break
  276. }
  277. }
  278.  
  279. return result
  280. }
  281.  
  282. const inBrowser = typeof window !== 'undefined';
  283.  
  284. function getVueDevtools () {
  285. return inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__
  286. }
  287.  
  288. window.vueDebugHelper = {
  289. /* 存储全部未被销毁的组件对象 */
  290. components: {},
  291. /* 存储全部创建过的组件的概要信息,即使销毁了概要信息依然存在 */
  292. componentsSummary: {},
  293. /* 基于componentsSummary的组件情况统计 */
  294. componentsSummaryStatistics: {},
  295. /* 已销毁的组件概要信息列表 */
  296. destroyList: [],
  297. /* 基于destroyList的组件情况统计 */
  298. destroyStatistics: {},
  299.  
  300. config: {
  301. inspect: {
  302. enabled: false
  303. },
  304.  
  305. performanceObserver: {
  306. enabled: false,
  307. // https://runebook.dev/zh-CN/docs/dom/performanceentry/entrytype
  308. entryTypes: ['element', 'navigation', 'resource', 'mark', 'measure', 'paint', 'longtask']
  309. },
  310.  
  311. /* 控制接口缓存 */
  312. ajaxCache: {
  313. enabled: false,
  314. filters: ['*'],
  315.  
  316. /* 设置缓存多久失效,默认为1天 */
  317. expires: 1000 * 60 * 60 * 24
  318. },
  319.  
  320. /* 是否在控制台打印组件生命周期的相关信息 */
  321. lifecycle: {
  322. show: false,
  323. filters: ['created'],
  324. componentFilters: []
  325. },
  326.  
  327. /* 查找组件的过滤器配置 */
  328. findComponentsFilters: [],
  329.  
  330. /* 阻止组件创建的过滤器 */
  331. blockFilters: [],
  332.  
  333. devtools: true,
  334.  
  335. /* 改写Vue.component */
  336. hackVueComponent: false,
  337.  
  338. /* 给组件注入空白数据的配置信息 */
  339. dd: {
  340. enabled: false,
  341. filters: [],
  342. count: 1024
  343. }
  344. }
  345. };
  346.  
  347. const helper = window.vueDebugHelper;
  348.  
  349. /* 配置信息跟localStorage联动 */
  350. const state = localStorageProxy('vueDebugHelperConfig', {
  351. defaults: helper.config,
  352. lspReset: false,
  353. storageEventListener: false
  354. });
  355. helper.config = state;
  356.  
  357. const methods = {
  358. objSort,
  359. createEmptyData,
  360. /* 清除全部helper的全部记录数据,以便重新统计 */
  361. clearAll () {
  362. helper.components = {};
  363. helper.componentsSummary = {};
  364. helper.componentsSummaryStatistics = {};
  365. helper.destroyList = [];
  366. helper.destroyStatistics = {};
  367. },
  368.  
  369. /**
  370. * 对当前的helper.components进行统计与排序
  371. * 如果一直没运行过清理函数,则表示统计页面创建至今依然存活的组件对象
  372. * 运行过清理函数,则表示统计清理后新创建且至今依然存活的组件对象
  373. */
  374. componentsStatistics (reverse = true) {
  375. const tmpObj = {};
  376.  
  377. Object.keys(helper.components).forEach(key => {
  378. const component = helper.components[key];
  379.  
  380. tmpObj[component._componentName]
  381. ? tmpObj[component._componentName].push(component)
  382. : (tmpObj[component._componentName] = [component]);
  383. });
  384.  
  385. return objSort(tmpObj, reverse, {
  386. key: 'componentName',
  387. value: 'componentInstance'
  388. })
  389. },
  390.  
  391. /**
  392. * 对componentsSummaryStatistics进行排序输出,以便可以直观查看组件的创建情况
  393. */
  394. componentsSummaryStatisticsSort (reverse = true) {
  395. return objSort(helper.componentsSummaryStatistics, reverse, {
  396. key: 'componentName',
  397. value: 'componentsSummary'
  398. })
  399. },
  400.  
  401. /**
  402. * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况
  403. */
  404. destroyStatisticsSort (reverse = true) {
  405. return objSort(helper.destroyStatistics, reverse, {
  406. key: 'componentName',
  407. value: 'destroyList'
  408. })
  409. },
  410.  
  411. /**
  412. * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况
  413. */
  414. getDestroyByDuration (duration = 1000) {
  415. const destroyList = helper.destroyList;
  416. const destroyListLength = destroyList.length;
  417. const destroyListDuration = destroyList.map(item => item.duration).sort();
  418. const maxDuration = Math.max(...destroyListDuration);
  419. const minDuration = Math.min(...destroyListDuration);
  420. const avgDuration = destroyListDuration.reduce((a, b) => a + b, 0) / destroyListLength;
  421. const durationRange = maxDuration - minDuration;
  422. const durationRangePercent = (duration - minDuration) / durationRange;
  423.  
  424. return {
  425. destroyList,
  426. destroyListLength,
  427. destroyListDuration,
  428. maxDuration,
  429. minDuration,
  430. avgDuration,
  431. durationRange,
  432. durationRangePercent
  433. }
  434. },
  435.  
  436. /**
  437. * 获取组件的调用链信息
  438. */
  439. getComponentChain (component, moreDetail = false) {
  440. const result = [];
  441. let current = component;
  442. let deep = 0;
  443.  
  444. while (current && deep < 50) {
  445. deep++;
  446.  
  447. /**
  448. * 由于脚本注入的运行时间会比应用创建时间晚,所以会导致部分先创建的组件缺少相关信息
  449. * 这里尝试对部分信息进行修复,以便更好的查看组件的创建情况
  450. */
  451. if (!current._componentTag) {
  452. const tag = current.$vnode?.tag || current.$options?._componentTag || current._uid;
  453. current._componentTag = tag;
  454. current._componentName = isNaN(Number(tag)) ? tag.replace(/^vue-component-\d+-/, '') : 'anonymous-component';
  455. }
  456.  
  457. if (moreDetail) {
  458. result.push({
  459. tag: current._componentTag,
  460. name: current._componentName,
  461. componentsSummary: helper.componentsSummary[current._uid] || null
  462. });
  463. } else {
  464. result.push(current._componentName);
  465. }
  466.  
  467. current = current.$parent;
  468. }
  469.  
  470. if (moreDetail) {
  471. return result
  472. } else {
  473. return result.join(' -> ')
  474. }
  475. },
  476.  
  477. printLifeCycleInfo (lifecycleFilters, componentFilters) {
  478. lifecycleFilters = toArrFilters(lifecycleFilters);
  479. componentFilters = toArrFilters(componentFilters);
  480.  
  481. helper.config.lifecycle = {
  482. show: true,
  483. filters: lifecycleFilters,
  484. componentFilters: componentFilters
  485. };
  486. },
  487. notPrintLifeCycleInfo () {
  488. helper.config.lifecycle.show = false;
  489. },
  490.  
  491. /**
  492. * 查找组件
  493. * @param {string|array} filters 组件名称或组件uid的过滤器,可以是字符串或者数组,如果是字符串多个过滤选可用,或|分隔
  494. * 如果过滤项是数字,则跟组件的id进行精确匹配,如果是字符串,则跟组件的tag信息进行模糊匹配
  495. * @returns {object} {components: [], componentNames: []}
  496. */
  497. findComponents (filters) {
  498. filters = toArrFilters(filters);
  499.  
  500. /* 对filters进行预处理,如果为纯数字则表示通过id查找组件 */
  501. filters = filters.map(filter => {
  502. if (/^\d+$/.test(filter)) {
  503. return Number(filter)
  504. } else {
  505. return filter
  506. }
  507. });
  508.  
  509. helper.config.findComponentsFilters = filters;
  510.  
  511. const result = {
  512. components: [],
  513. globalComponents: [],
  514. destroyedComponents: []
  515. };
  516.  
  517. /* 在helper.components里进行组件查找 */
  518. const components = helper.components;
  519. const keys = Object.keys(components);
  520. for (let i = 0; i < keys.length; i++) {
  521. const component = components[keys[i]];
  522.  
  523. for (let j = 0; j < filters.length; j++) {
  524. const filter = filters[j];
  525.  
  526. if (typeof filter === 'number' && component._uid === filter) {
  527. result.components.push(component);
  528. break
  529. } else if (typeof filter === 'string') {
  530. const { _componentTag, _componentName } = component;
  531.  
  532. if (stringMatch(filter, _componentTag) || stringMatch(filter, _componentName)) {
  533. result.components.push(component);
  534. break
  535. }
  536. }
  537. }
  538. }
  539.  
  540. /* 进行全局组件查找 */
  541. const globalComponentsKeys = Object.keys(helper.Vue.options.components);
  542. for (let i = 0; i < globalComponentsKeys.length; i++) {
  543. const key = String(globalComponentsKeys[i]);
  544. const component = helper.Vue.options.components[globalComponentsKeys[i]];
  545.  
  546. if (filtersMatch(filters, key)) {
  547. const tmpObj = {};
  548. tmpObj[key] = component;
  549. result.globalComponents.push(tmpObj);
  550. }
  551. }
  552.  
  553. helper.destroyList.forEach(item => {
  554. for (let j = 0; j < filters.length; j++) {
  555. const filter = filters[j];
  556.  
  557. if (typeof filter === 'number' && item.uid === filter) {
  558. result.destroyedComponents.push(item);
  559. break
  560. } else if (typeof filter === 'string') {
  561. if (stringMatch(filter, item.tag) || stringMatch(filter, item.name)) {
  562. result.destroyedComponents.push(item);
  563. break
  564. }
  565. }
  566. }
  567. });
  568.  
  569. return result
  570. },
  571.  
  572. findNotContainElementComponents () {
  573. const result = [];
  574. const keys = Object.keys(helper.components);
  575. keys.forEach(key => {
  576. const component = helper.components[key];
  577. const elStr = Object.prototype.toString.call(component.$el);
  578. if (!/(HTML|Comment)/.test(elStr)) {
  579. result.push(component);
  580. }
  581. });
  582.  
  583. return result
  584. },
  585.  
  586. /**
  587. * 阻止组件的创建
  588. * @param {string|array} filters 组件名称过滤器,可以是字符串或者数组,如果是字符串多个过滤选可用,或|分隔
  589. */
  590. blockComponents (filters) {
  591. filters = toArrFilters(filters);
  592. helper.config.blockFilters = filters;
  593. },
  594.  
  595. /**
  596. * 给指定组件注入大量空数据,以便观察组件的内存泄露情况
  597. * @param {Array|string} filter -必选 指定组件的名称,如果为空则表示注入所有组件
  598. * @param {number} count -可选 指定注入空数据的大小,单位Kb,默认为1024Kb,即1Mb
  599. * @returns
  600. */
  601. dd (filter, count = 1024) {
  602. filter = toArrFilters(filter);
  603. helper.config.dd = {
  604. enabled: true,
  605. filters: filter,
  606. count
  607. };
  608. },
  609. /* 禁止给组件注入空数据 */
  610. undd () {
  611. helper.config.dd = {
  612. enabled: false,
  613. filters: [],
  614. count: 1024
  615. };
  616.  
  617. /* 删除之前注入的数据 */
  618. Object.keys(helper.components).forEach(key => {
  619. const component = helper.components[key];
  620. component.$data && delete component.$data.__dd__;
  621. });
  622. },
  623.  
  624. toggleDevtools () {
  625. helper.config.devtools = !helper.config.devtools;
  626. }
  627. };
  628.  
  629. helper.methods = methods;
  630.  
  631. class Debug {
  632. constructor (msg, printTime = false) {
  633. const t = this;
  634. msg = msg || 'debug message:';
  635. t.log = t.createDebugMethod('log', null, msg);
  636. t.error = t.createDebugMethod('error', null, msg);
  637. t.info = t.createDebugMethod('info', null, msg);
  638. t.warn = t.createDebugMethod('warn', null, msg);
  639. }
  640.  
  641. create (msg) {
  642. return new Debug(msg)
  643. }
  644.  
  645. createDebugMethod (name, color, tipsMsg) {
  646. name = name || 'info';
  647.  
  648. const bgColorMap = {
  649. info: '#2274A5',
  650. log: '#95B46A',
  651. warn: '#F5A623',
  652. error: '#D33F49'
  653. };
  654.  
  655. const printTime = this.printTime;
  656.  
  657. return function () {
  658. if (!window._debugMode_) {
  659. return false
  660. }
  661.  
  662. const msg = tipsMsg || 'debug message:';
  663.  
  664. const arg = Array.from(arguments);
  665. arg.unshift(`color: white; background-color: ${color || bgColorMap[name] || '#95B46A'}`);
  666.  
  667. if (printTime) {
  668. const curTime = new Date();
  669. const H = curTime.getHours();
  670. const M = curTime.getMinutes();
  671. const S = curTime.getSeconds();
  672. arg.unshift(`%c [${H}:${M}:${S}] ${msg} `);
  673. } else {
  674. arg.unshift(`%c ${msg} `);
  675. }
  676.  
  677. window.console[name].apply(window.console, arg);
  678. }
  679. }
  680.  
  681. isDebugMode () {
  682. return Boolean(window._debugMode_)
  683. }
  684. }
  685.  
  686. var Debug$1 = new Debug();
  687.  
  688. var debug = Debug$1.create('vueDebugHelper:');
  689.  
  690. /**
  691. * 打印生命周期信息
  692. * @param {Vue} vm vue组件实例
  693. * @param {string} lifeCycle vue生命周期名称
  694. * @returns
  695. */
  696. function printLifeCycle (vm, lifeCycle) {
  697. const lifeCycleConf = helper.config.lifecycle || { show: false, filters: ['created'], componentFilters: [] };
  698.  
  699. if (!vm || !lifeCycle || !lifeCycleConf.show) {
  700. return false
  701. }
  702.  
  703. const file = vm.options?.__file || vm.$options?.__file || '';
  704.  
  705. const { _componentTag, _componentName, _componentChain, _createdHumanTime, _uid } = vm;
  706. let info = `[${lifeCycle}] tag: ${_componentTag}, uid: ${_uid}, createdTime: ${_createdHumanTime}, chain: ${_componentChain}`;
  707.  
  708. if (file) {
  709. info += `, file: ${file}`;
  710. }
  711.  
  712. const matchComponentFilters = lifeCycleConf.componentFilters.length === 0 || filtersMatch(lifeCycleConf.componentFilters, _componentName);
  713. if (lifeCycleConf.filters.includes(lifeCycle) && matchComponentFilters) {
  714. debug.log(info);
  715. }
  716. }
  717.  
  718. function mixinRegister (Vue) {
  719. if (!Vue || !Vue.mixin) {
  720. debug.error('未检查到VUE对象,请检查是否引入了VUE,且将VUE对象挂载到全局变量window.Vue上');
  721. return false
  722. }
  723.  
  724. Vue.mixin({
  725. beforeCreate: function () {
  726. // const tag = this.$options?._componentTag || this.$vnode?.tag || this._uid
  727. const tag = this.$vnode?.tag || this.$options?._componentTag || this._uid;
  728. const chain = helper.methods.getComponentChain(this);
  729. this._componentTag = tag;
  730. this._componentChain = chain;
  731. this._componentName = isNaN(Number(tag)) ? tag.replace(/^vue-component-\d+-/, '') : 'anonymous-component';
  732. this._createdTime = Date.now();
  733.  
  734. /* 增加人类方便查看的时间信息 */
  735. const timeObj = new Date(this._createdTime);
  736. this._createdHumanTime = `${timeObj.getHours()}:${timeObj.getMinutes()}:${timeObj.getSeconds()}`;
  737.  
  738. /* 判断是否为函数式组件,函数式组件无状态 (没有响应式数据),也没有实例,也没生命周期概念 */
  739. if (this._componentName === 'anonymous-component' && !this.$parent && !this.$vnode) {
  740. this._componentName = 'functional-component';
  741. }
  742.  
  743. helper.components[this._uid] = this;
  744.  
  745. /**
  746. * 收集所有创建过的组件信息,此处只存储组件的基础信息,没销毁的组件会包含组件实例
  747. * 严禁对组件内其它对象进行引用,否则会导致组件实列无法被正常回收
  748. */
  749. const componentSummary = {
  750. uid: this._uid,
  751. name: this._componentName,
  752. tag: this._componentTag,
  753. createdTime: this._createdTime,
  754. createdHumanTime: this._createdHumanTime,
  755. // 0 表示还没被销毁
  756. destroyTime: 0,
  757. // 0 表示还没被销毁,duration可持续当当前查看时间
  758. duration: 0,
  759. component: this,
  760. chain
  761. };
  762. helper.componentsSummary[this._uid] = componentSummary;
  763.  
  764. /* 添加到componentsSummaryStatistics里,生成统计信息 */
  765. Array.isArray(helper.componentsSummaryStatistics[this._componentName])
  766. ? helper.componentsSummaryStatistics[this._componentName].push(componentSummary)
  767. : (helper.componentsSummaryStatistics[this._componentName] = [componentSummary]);
  768.  
  769. printLifeCycle(this, 'beforeCreate');
  770. },
  771. created: function () {
  772. /* 增加空白数据,方便观察内存泄露情况 */
  773. if (helper.config.dd.enabled) {
  774. let needDd = false;
  775.  
  776. if (helper.config.dd.filters.length === 0) {
  777. needDd = true;
  778. } else {
  779. for (let index = 0; index < helper.config.dd.filters.length; index++) {
  780. const filter = helper.config.dd.filters[index];
  781. if (filter === this._componentName || String(this._componentName).endsWith(filter)) {
  782. needDd = true;
  783. break
  784. }
  785. }
  786. }
  787.  
  788. if (needDd) {
  789. const count = helper.config.dd.count * 1024;
  790. const componentInfo = `tag: ${this._componentTag}, uid: ${this._uid}, createdTime: ${this._createdHumanTime}`;
  791.  
  792. /* 此处必须使用JSON.stringify对产生的字符串进行消费,否则没法将内存占用上去 */
  793. this.$data.__dd__ = JSON.stringify(componentInfo + ' ' + helper.methods.createEmptyData(count, this._uid));
  794.  
  795. console.log(`[dd success] ${componentInfo} chain: ${this._componentChain}`);
  796. }
  797. }
  798.  
  799. printLifeCycle(this, 'created');
  800. },
  801. beforeMount: function () {
  802. printLifeCycle(this, 'beforeMount');
  803. },
  804. mounted: function () {
  805. printLifeCycle(this, 'mounted');
  806. },
  807. beforeUpdate: function () {
  808. printLifeCycle(this, 'beforeUpdate');
  809. },
  810. activated: function () {
  811. printLifeCycle(this, 'activated');
  812. },
  813. deactivated: function () {
  814. printLifeCycle(this, 'deactivated');
  815. },
  816. updated: function () {
  817. printLifeCycle(this, 'updated');
  818. },
  819. beforeDestroy: function () {
  820. printLifeCycle(this, 'beforeDestroy');
  821. },
  822. destroyed: function () {
  823. printLifeCycle(this, 'destroyed');
  824.  
  825. if (this._componentTag) {
  826. const uid = this._uid;
  827. const name = this._componentName;
  828. const destroyTime = Date.now();
  829.  
  830. /* helper里的componentSummary有可能通过调用clear函数而被清除掉,所以需进行判断再更新赋值 */
  831. const componentSummary = helper.componentsSummary[this._uid];
  832. if (componentSummary) {
  833. /* 补充/更新组件信息 */
  834. componentSummary.destroyTime = destroyTime;
  835. componentSummary.duration = destroyTime - this._createdTime;
  836.  
  837. helper.destroyList.push(componentSummary);
  838.  
  839. /* 统计被销毁的组件信息 */
  840. Array.isArray(helper.destroyStatistics[name])
  841. ? helper.destroyStatistics[name].push(componentSummary)
  842. : (helper.destroyStatistics[name] = [componentSummary]);
  843.  
  844. /* 删除已销毁的组件实例 */
  845. delete componentSummary.component;
  846. }
  847.  
  848. // 解除引用关系
  849. delete this._componentTag;
  850. delete this._componentChain;
  851. delete this._componentName;
  852. delete this._createdTime;
  853. delete this._createdHumanTime;
  854. delete this.$data.__dd__;
  855. delete helper.components[uid];
  856. } else {
  857. console.error('存在未被正常标记的组件,请检查组件采集逻辑是否需完善', this);
  858. }
  859. }
  860. });
  861. }
  862.  
  863. /*!
  864. * @name menuCommand.js
  865. * @version 0.0.1
  866. * @author Blaze
  867. * @date 2019/9/21 14:22
  868. */
  869.  
  870. const monkeyMenu = {
  871. on (title, fn, accessKey) {
  872. return window.GM_registerMenuCommand && window.GM_registerMenuCommand(title, fn, accessKey)
  873. },
  874. off (id) {
  875. return window.GM_unregisterMenuCommand && window.GM_unregisterMenuCommand(id)
  876. },
  877. /* 切换类型的菜单功能 */
  878. switch (title, fn, defVal) {
  879. const t = this;
  880. t.on(title, fn);
  881. }
  882. };
  883.  
  884. /**
  885. * 简单的i18n库
  886. */
  887.  
  888. class I18n {
  889. constructor (config) {
  890. this._languages = {};
  891. this._locale = this.getClientLang();
  892. this._defaultLanguage = '';
  893. this.init(config);
  894. }
  895.  
  896. init (config) {
  897. if (!config) return false
  898.  
  899. const t = this;
  900. t._locale = config.locale || t._locale;
  901. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  902. t._languages = config.languages || t._languages;
  903. t._defaultLanguage = config.defaultLanguage || t._defaultLanguage;
  904. }
  905.  
  906. use () {}
  907.  
  908. t (path) {
  909. const t = this;
  910. let result = t.getValByPath(t._languages[t._locale] || {}, path);
  911.  
  912. /* 版本回退 */
  913. if (!result && t._locale !== t._defaultLanguage) {
  914. result = t.getValByPath(t._languages[t._defaultLanguage] || {}, path);
  915. }
  916.  
  917. return result || ''
  918. }
  919.  
  920. /* 当前语言值 */
  921. language () {
  922. return this._locale
  923. }
  924.  
  925. languages () {
  926. return this._languages
  927. }
  928.  
  929. changeLanguage (locale) {
  930. if (this._languages[locale]) {
  931. this._languages = locale;
  932. return locale
  933. } else {
  934. return false
  935. }
  936. }
  937.  
  938. /**
  939. * 根据文本路径获取对象里面的值
  940. * @param obj {Object} -必选 要操作的对象
  941. * @param path {String} -必选 路径信息
  942. * @returns {*}
  943. */
  944. getValByPath (obj, path) {
  945. path = path || '';
  946. const pathArr = path.split('.');
  947. let result = obj;
  948.  
  949. /* 递归提取结果值 */
  950. for (let i = 0; i < pathArr.length; i++) {
  951. if (!result) break
  952. result = result[pathArr[i]];
  953. }
  954.  
  955. return result
  956. }
  957.  
  958. /* 获取客户端当前的语言环境 */
  959. getClientLang () {
  960. return navigator.languages ? navigator.languages[0] : navigator.language
  961. }
  962. }
  963.  
  964. var zhCN = {
  965. about: '关于',
  966. issues: '反馈',
  967. setting: '设置',
  968. hotkeys: '快捷键',
  969. donate: '赞赏',
  970. debugHelper: {
  971. viewVueDebugHelperObject: 'vueDebugHelper对象',
  972. componentsStatistics: '当前存活组件统计',
  973. destroyStatisticsSort: '已销毁组件统计',
  974. componentsSummaryStatisticsSort: '全部组件混合统计',
  975. getDestroyByDuration: '组件存活时间信息',
  976. clearAll: '清空统计信息',
  977. printLifeCycleInfo: '打印组件生命周期信息',
  978. notPrintLifeCycleInfo: '取消组件生命周期信息打印',
  979. printLifeCycleInfoPrompt: {
  980. lifecycleFilters: '输入要打印的生命周期名称,多个可用,或|分隔,支持的值:beforeCreate|created|beforeMount|mounted|beforeUpdate|updated|activated|deactivated|beforeDestroy|destroyed',
  981. componentFilters: '输入要打印的组件名称,多个可用,或|分隔,不输入则打印所有组件,字符串后面加*可执行模糊匹配'
  982. },
  983. findComponents: '查找组件',
  984. findComponentsPrompt: {
  985. filters: '输入要查找的组件名称,或uid,多个可用,或|分隔,字符串后面加*可执行模糊匹配'
  986. },
  987. findNotContainElementComponents: '查找不包含DOM对象的组件',
  988. blockComponents: '阻断组件的创建',
  989. blockComponentsPrompt: {
  990. filters: '输入要阻断的组件名称,多个可用,或|分隔,输入为空则取消阻断,字符串后面加*可执行模糊匹配'
  991. },
  992. dd: '数据注入(dd)',
  993. undd: '取消数据注入(undd)',
  994. ddPrompt: {
  995. filter: '组件过滤器(如果为空,则对所有组件注入)',
  996. count: '指定注入数据的重复次数(默认1024)'
  997. },
  998. toggleHackVueComponent: '改写/还原Vue.component',
  999. hackVueComponent: {
  1000. hack: '改写Vue.component',
  1001. unhack: '还原Vue.component'
  1002. },
  1003. toggleInspect: '切换Inspect',
  1004. togglePerformanceObserver: '开启/关闭性能观察',
  1005. performanceObserverPrompt: {
  1006. entryTypes: '输入要观察的类型,多个类型可用,或|分隔,支持的类型有:element,navigation,resource,mark,measure,paint,longtask',
  1007. notSupport: '当前浏览器不支持性能观察'
  1008. },
  1009. enableAjaxCacheTips: '接口缓存功能已开启',
  1010. disableAjaxCacheTips: '接口缓存功能已关闭',
  1011. toggleAjaxCache: '开启/关闭接口缓存',
  1012. clearAjaxCache: '清空接口缓存数据',
  1013. clearAjaxCacheTips: '接口缓存数据已清空',
  1014. jaxCachePrompt: {
  1015. filters: '输入要缓存的接口地址,多个可用,或|分隔,字符串后面加*可执行模糊匹配',
  1016. expires: '输入缓存过期时间,单位为分钟,默认为1440分钟(即24小时)'
  1017. },
  1018. devtools: {
  1019. enabled: '自动开启vue-devtools',
  1020. disable: '禁止开启vue-devtools'
  1021. }
  1022. }
  1023. };
  1024.  
  1025. var enUS = {
  1026. about: 'about',
  1027. issues: 'feedback',
  1028. setting: 'settings',
  1029. hotkeys: 'Shortcut keys',
  1030. donate: 'donate',
  1031. debugHelper: {
  1032. viewVueDebugHelperObject: 'vueDebugHelper object',
  1033. componentsStatistics: 'Current surviving component statistics',
  1034. destroyStatisticsSort: 'Destroyed component statistics',
  1035. componentsSummaryStatisticsSort: 'All components mixed statistics',
  1036. getDestroyByDuration: 'Component survival time information',
  1037. clearAll: 'Clear statistics',
  1038. dd: 'Data injection (dd)',
  1039. undd: 'Cancel data injection (undd)',
  1040. ddPrompt: {
  1041. filter: 'Component filter (if empty, inject all components)',
  1042. count: 'Specify the number of repetitions of injected data (default 1024)'
  1043. }
  1044. }
  1045. };
  1046.  
  1047. var zhTW = {
  1048. about: '關於',
  1049. issues: '反饋',
  1050. setting: '設置',
  1051. hotkeys: '快捷鍵',
  1052. donate: '讚賞',
  1053. debugHelper: {
  1054. viewVueDebugHelperObject: 'vueDebugHelper對象',
  1055. componentsStatistics: '當前存活組件統計',
  1056. destroyStatisticsSort: '已銷毀組件統計',
  1057. componentsSummaryStatisticsSort: '全部組件混合統計',
  1058. getDestroyByDuration: '組件存活時間信息',
  1059. clearAll: '清空統計信息',
  1060. dd: '數據注入(dd)',
  1061. undd: '取消數據注入(undd)',
  1062. ddPrompt: {
  1063. filter: '組件過濾器(如果為空,則對所有組件注入)',
  1064. count: '指定注入數據的重複次數(默認1024)'
  1065. }
  1066. }
  1067. };
  1068.  
  1069. const messages = {
  1070. 'zh-CN': zhCN,
  1071. zh: zhCN,
  1072. 'zh-HK': zhTW,
  1073. 'zh-TW': zhTW,
  1074. 'en-US': enUS,
  1075. en: enUS,
  1076. };
  1077.  
  1078. /*!
  1079. * @name i18n.js
  1080. * @description vue-debug-helper的国际化配置
  1081. * @version 0.0.1
  1082. * @author xxxily
  1083. * @date 2022/04/26 14:56
  1084. * @github https://github.com/xxxily
  1085. */
  1086.  
  1087. const i18n = new I18n({
  1088. defaultLanguage: 'en',
  1089. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  1090. // locale: 'zh-TW',
  1091. languages: messages
  1092. });
  1093.  
  1094. /*!
  1095. * @name index.js
  1096. * @description hookJs JS AOP切面编程辅助库
  1097. * @version 0.0.1
  1098. * @author Blaze
  1099. * @date 2020/10/22 17:40
  1100. * @github https://github.com/xxxily
  1101. */
  1102.  
  1103. const win = typeof window === 'undefined' ? global : window;
  1104. const toStr = Function.prototype.call.bind(Object.prototype.toString);
  1105. /* 特殊场景,如果把Boolean也hook了,很容易导致调用溢出,所以是需要使用原生Boolean */
  1106. const toBoolean = Boolean.originMethod ? Boolean.originMethod : Boolean;
  1107. const util = {
  1108. toStr,
  1109. isObj: obj => toStr(obj) === '[object Object]',
  1110. /* 判断是否为引用类型,用于更宽泛的场景 */
  1111. isRef: obj => typeof obj === 'object',
  1112. isReg: obj => toStr(obj) === '[object RegExp]',
  1113. isFn: obj => obj instanceof Function,
  1114. isAsyncFn: fn => toStr(fn) === '[object AsyncFunction]',
  1115. isPromise: obj => toStr(obj) === '[object Promise]',
  1116. firstUpperCase: str => str.replace(/^\S/, s => s.toUpperCase()),
  1117. toArr: arg => Array.from(Array.isArray(arg) ? arg : [arg]),
  1118.  
  1119. debug: {
  1120. log () {
  1121. let log = win.console.log;
  1122. /* 如果log也被hook了,则使用未被hook前的log函数 */
  1123. if (log.originMethod) { log = log.originMethod; }
  1124. if (win._debugMode_) {
  1125. log.apply(win.console, arguments);
  1126. }
  1127. }
  1128. },
  1129. /* 获取包含自身、继承、可枚举、不可枚举的键名 */
  1130. getAllKeys (obj) {
  1131. const tmpArr = [];
  1132. for (const key in obj) { tmpArr.push(key); }
  1133. const allKeys = Array.from(new Set(tmpArr.concat(Reflect.ownKeys(obj))));
  1134. return allKeys
  1135. }
  1136. };
  1137.  
  1138. class HookJs {
  1139. constructor (useProxy) {
  1140. this.useProxy = useProxy || false;
  1141. this.hookPropertiesKeyName = '_hookProperties' + Date.now();
  1142. }
  1143.  
  1144. hookJsPro () {
  1145. return new HookJs(true)
  1146. }
  1147.  
  1148. _addHook (hookMethod, fn, type, classHook) {
  1149. const hookKeyName = type + 'Hooks';
  1150. const hookMethodProperties = hookMethod[this.hookPropertiesKeyName];
  1151. if (!hookMethodProperties[hookKeyName]) {
  1152. hookMethodProperties[hookKeyName] = [];
  1153. }
  1154.  
  1155. /* 注册(储存)要被调用的hook函数,同时防止重复注册 */
  1156. let hasSameHook = false;
  1157. for (let i = 0; i < hookMethodProperties[hookKeyName].length; i++) {
  1158. if (fn === hookMethodProperties[hookKeyName][i]) {
  1159. hasSameHook = true;
  1160. break
  1161. }
  1162. }
  1163.  
  1164. if (!hasSameHook) {
  1165. fn.classHook = classHook || false;
  1166. hookMethodProperties[hookKeyName].push(fn);
  1167. }
  1168. }
  1169.  
  1170. _runHooks (parentObj, methodName, originMethod, hookMethod, target, ctx, args, classHook, hookPropertiesKeyName) {
  1171. const hookMethodProperties = hookMethod[hookPropertiesKeyName];
  1172. const beforeHooks = hookMethodProperties.beforeHooks || [];
  1173. const afterHooks = hookMethodProperties.afterHooks || [];
  1174. const errorHooks = hookMethodProperties.errorHooks || [];
  1175. const hangUpHooks = hookMethodProperties.hangUpHooks || [];
  1176. const replaceHooks = hookMethodProperties.replaceHooks || [];
  1177. const execInfo = {
  1178. result: null,
  1179. error: null,
  1180. args: args,
  1181. type: ''
  1182. };
  1183.  
  1184. function runHooks (hooks, type) {
  1185. let hookResult = null;
  1186. execInfo.type = type || '';
  1187. if (Array.isArray(hooks)) {
  1188. hooks.forEach(fn => {
  1189. if (util.isFn(fn) && classHook === fn.classHook) {
  1190. hookResult = fn(args, parentObj, methodName, originMethod, execInfo, ctx);
  1191. }
  1192. });
  1193. }
  1194. return hookResult
  1195. }
  1196.  
  1197. const runTarget = (function () {
  1198. if (classHook) {
  1199. return function () {
  1200. // eslint-disable-next-line new-cap
  1201. return new target(...args)
  1202. }
  1203. } else {
  1204. return function () {
  1205. return target.apply(ctx, args)
  1206. }
  1207. }
  1208. })();
  1209.  
  1210. const beforeHooksResult = runHooks(beforeHooks, 'before');
  1211. /* 支持终止后续调用的指令 */
  1212. if (beforeHooksResult && beforeHooksResult === 'STOP-INVOKE') {
  1213. return beforeHooksResult
  1214. }
  1215.  
  1216. if (hangUpHooks.length || replaceHooks.length) {
  1217. /**
  1218. * 当存在hangUpHooks或replaceHooks的时候是不会触发原来函数的
  1219. * 本质上来说hangUpHooks和replaceHooks是一样的,只是外部的定义描述不一致和分类不一致而已
  1220. */
  1221. runHooks(hangUpHooks, 'hangUp');
  1222. runHooks(replaceHooks, 'replace');
  1223. } else {
  1224. if (errorHooks.length) {
  1225. try {
  1226. execInfo.result = runTarget();
  1227. } catch (err) {
  1228. execInfo.error = err;
  1229. const errorHooksResult = runHooks(errorHooks, 'error');
  1230. /* 支持执行错误后不抛出异常的指令 */
  1231. if (errorHooksResult && errorHooksResult === 'SKIP-ERROR') ; else {
  1232. throw err
  1233. }
  1234. }
  1235. } else {
  1236. execInfo.result = runTarget();
  1237. }
  1238. }
  1239.  
  1240. /**
  1241. * 执行afterHooks,如果返回的是Promise,理论上应该进行进一步的细分处理
  1242. * 但添加细分处理逻辑后发现性能下降得比较厉害,且容易出现各种异常,所以决定不在hook里处理Promise情况
  1243. * 下面是原Promise处理逻辑,添加后会导致以下网站卡死或无法访问:
  1244. * wenku.baidu.com
  1245. * https://pubs.rsc.org/en/content/articlelanding/2021/sc/d1sc01881g#!divAbstract
  1246. * https://www.elsevier.com/connect/coronavirus-information-center
  1247. */
  1248. // if (execInfo.result && execInfo.result.then && util.isPromise(execInfo.result)) {
  1249. // execInfo.result.then(function (data) {
  1250. // execInfo.result = data
  1251. // runHooks(afterHooks, 'after')
  1252. // return Promise.resolve.apply(ctx, arguments)
  1253. // }).catch(function (err) {
  1254. // execInfo.error = err
  1255. // runHooks(errorHooks, 'error')
  1256. // return Promise.reject.apply(ctx, arguments)
  1257. // })
  1258. // }
  1259.  
  1260. runHooks(afterHooks, 'after');
  1261.  
  1262. return execInfo.result
  1263. }
  1264.  
  1265. _proxyMethodcGenerator (parentObj, methodName, originMethod, classHook, context, proxyHandler) {
  1266. const t = this;
  1267. const useProxy = t.useProxy;
  1268. let hookMethod = null;
  1269.  
  1270. /* 存在缓存则使用缓存的hookMethod */
  1271. if (t.isHook(originMethod)) {
  1272. hookMethod = originMethod;
  1273. } else if (originMethod[t.hookPropertiesKeyName] && t.isHook(originMethod[t.hookPropertiesKeyName].hookMethod)) {
  1274. hookMethod = originMethod[t.hookPropertiesKeyName].hookMethod;
  1275. }
  1276.  
  1277. if (hookMethod) {
  1278. if (!hookMethod[t.hookPropertiesKeyName].isHook) {
  1279. /* 重新标注被hook状态 */
  1280. hookMethod[t.hookPropertiesKeyName].isHook = true;
  1281. util.debug.log(`[hook method] ${util.toStr(parentObj)} ${methodName}`);
  1282. }
  1283. return hookMethod
  1284. }
  1285.  
  1286. /* 使用Proxy模式进行hook可以获得更多特性,但性能也会稍差一些 */
  1287. if (useProxy && Proxy) {
  1288. /* 注意:使用Proxy代理,hookMethod和originMethod将共用同一对象 */
  1289. const handler = { ...proxyHandler };
  1290.  
  1291. /* 下面的写法确定了proxyHandler是无法覆盖construct和apply操作的 */
  1292. if (classHook) {
  1293. handler.construct = function (target, args, newTarget) {
  1294. context = context || this;
  1295. return t._runHooks(parentObj, methodName, originMethod, hookMethod, target, context, args, true, t.hookPropertiesKeyName)
  1296. };
  1297. } else {
  1298. handler.apply = function (target, ctx, args) {
  1299. ctx = context || ctx;
  1300. return t._runHooks(parentObj, methodName, originMethod, hookMethod, target, ctx, args, false, t.hookPropertiesKeyName)
  1301. };
  1302. }
  1303.  
  1304. hookMethod = new Proxy(originMethod, handler);
  1305. } else {
  1306. hookMethod = function () {
  1307. /**
  1308. * 注意此处不能通过 context = context || this
  1309. * 然后通过把context当ctx传递过去
  1310. * 这将导致ctx引用错误
  1311. */
  1312. const ctx = context || this;
  1313. return t._runHooks(parentObj, methodName, originMethod, hookMethod, originMethod, ctx, arguments, classHook, t.hookPropertiesKeyName)
  1314. };
  1315.  
  1316. /* 确保子对象和原型链跟originMethod保持一致 */
  1317. const keys = Reflect.ownKeys(originMethod);
  1318. keys.forEach(keyName => {
  1319. try {
  1320. Object.defineProperty(hookMethod, keyName, {
  1321. get: function () {
  1322. return originMethod[keyName]
  1323. },
  1324. set: function (val) {
  1325. originMethod[keyName] = val;
  1326. }
  1327. });
  1328. } catch (err) {
  1329. // 设置defineProperty的时候出现异常,可能导致hookMethod部分功能确实,也可能不受影响
  1330. util.debug.log(`[proxyMethodcGenerator] hookMethod defineProperty abnormal. hookMethod:${methodName}, definePropertyName:${keyName}`, err);
  1331. }
  1332. });
  1333. hookMethod.prototype = originMethod.prototype;
  1334. }
  1335.  
  1336. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName] = {};
  1337.  
  1338. hookMethodProperties.originMethod = originMethod;
  1339. hookMethodProperties.hookMethod = hookMethod;
  1340. hookMethodProperties.isHook = true;
  1341. hookMethodProperties.classHook = classHook;
  1342.  
  1343. util.debug.log(`[hook method] ${util.toStr(parentObj)} ${methodName}`);
  1344.  
  1345. return hookMethod
  1346. }
  1347.  
  1348. _getObjKeysByRule (obj, rule) {
  1349. let excludeRule = null;
  1350. let result = rule;
  1351.  
  1352. if (util.isObj(rule) && rule.include) {
  1353. excludeRule = rule.exclude;
  1354. rule = rule.include;
  1355. result = rule;
  1356. }
  1357.  
  1358. /**
  1359. * for in、Object.keys与Reflect.ownKeys的区别见:
  1360. * https://es6.ruanyifeng.com/#docs/object#%E5%B1%9E%E6%80%A7%E7%9A%84%E9%81%8D%E5%8E%86
  1361. */
  1362. if (rule === '*') {
  1363. result = Object.keys(obj);
  1364. } else if (rule === '**') {
  1365. result = Reflect.ownKeys(obj);
  1366. } else if (rule === '***') {
  1367. result = util.getAllKeys(obj);
  1368. } else if (util.isReg(rule)) {
  1369. result = util.getAllKeys(obj).filter(keyName => rule.test(keyName));
  1370. }
  1371.  
  1372. /* 如果存在排除规则,则需要进行排除 */
  1373. if (excludeRule) {
  1374. result = Array.isArray(result) ? result : [result];
  1375. if (util.isReg(excludeRule)) {
  1376. result = result.filter(keyName => !excludeRule.test(keyName));
  1377. } else if (Array.isArray(excludeRule)) {
  1378. result = result.filter(keyName => !excludeRule.includes(keyName));
  1379. } else {
  1380. result = result.filter(keyName => excludeRule !== keyName);
  1381. }
  1382. }
  1383.  
  1384. return util.toArr(result)
  1385. }
  1386.  
  1387. /**
  1388. * 判断某个函数是否已经被hook
  1389. * @param fn {Function} -必选 要判断的函数
  1390. * @returns {boolean}
  1391. */
  1392. isHook (fn) {
  1393. if (!fn || !fn[this.hookPropertiesKeyName]) {
  1394. return false
  1395. }
  1396. const hookMethodProperties = fn[this.hookPropertiesKeyName];
  1397. return util.isFn(hookMethodProperties.originMethod) && fn !== hookMethodProperties.originMethod
  1398. }
  1399.  
  1400. /**
  1401. * 判断对象下的某个值是否具备hook的条件
  1402. * 注意:具备hook条件和能否直接修改值是两回事,
  1403. * 在进行hook的时候还要检查descriptor.writable是否为false
  1404. * 如果为false则要修改成true才能hook成功
  1405. * @param parentObj
  1406. * @param keyName
  1407. * @returns {boolean}
  1408. */
  1409. isAllowHook (parentObj, keyName) {
  1410. /* 有些对象会设置getter,让读取值的时候就抛错,所以需要try catch 判断能否正常读取属性 */
  1411. try { if (!parentObj[keyName]) return false } catch (e) { return false }
  1412. const descriptor = Object.getOwnPropertyDescriptor(parentObj, keyName);
  1413. return !(descriptor && descriptor.configurable === false)
  1414. }
  1415.  
  1416. /**
  1417. * hook 核心函数
  1418. * @param parentObj {Object} -必选 被hook函数依赖的父对象
  1419. * @param hookMethods {Object|Array|RegExp|string} -必选 被hook函数的函数名或函数名的匹配规则
  1420. * @param fn {Function} -必选 hook之后的回调方法
  1421. * @param type {String} -可选 默认before,指定运行hook函数回调的时机,可选字符串:before、after、replace、error、hangUp
  1422. * @param classHook {Boolean} -可选 默认false,指定是否为针对new(class)操作的hook
  1423. * @param context {Object} -可选 指定运行被hook函数时的上下文对象
  1424. * @param proxyHandler {Object} -可选 仅当用Proxy进行hook时有效,默认使用的是Proxy的apply handler进行hook,如果你有特殊需求也可以配置自己的handler以实现更复杂的功能
  1425. * 附注:不使用Proxy进行hook,可以获得更高性能,但也意味着通用性更差些,对于要hook HTMLElement.prototype、EventTarget.prototype这些对象里面的非实例的函数往往会失败而导致被hook函数执行出错
  1426. * @returns {boolean}
  1427. */
  1428. hook (parentObj, hookMethods, fn, type, classHook, context, proxyHandler) {
  1429. classHook = toBoolean(classHook);
  1430. type = type || 'before';
  1431.  
  1432. if ((!util.isRef(parentObj) && !util.isFn(parentObj)) || !util.isFn(fn) || !hookMethods) {
  1433. return false
  1434. }
  1435.  
  1436. const t = this;
  1437.  
  1438. hookMethods = t._getObjKeysByRule(parentObj, hookMethods);
  1439. hookMethods.forEach(methodName => {
  1440. if (!t.isAllowHook(parentObj, methodName)) {
  1441. util.debug.log(`${util.toStr(parentObj)} [${methodName}] does not support modification`);
  1442. return false
  1443. }
  1444.  
  1445. const descriptor = Object.getOwnPropertyDescriptor(parentObj, methodName);
  1446. if (descriptor && descriptor.writable === false) {
  1447. Object.defineProperty(parentObj, methodName, { writable: true });
  1448. }
  1449.  
  1450. const originMethod = parentObj[methodName];
  1451. let hookMethod = null;
  1452.  
  1453. /* 非函数无法进行hook操作 */
  1454. if (!util.isFn(originMethod)) {
  1455. return false
  1456. }
  1457.  
  1458. hookMethod = t._proxyMethodcGenerator(parentObj, methodName, originMethod, classHook, context, proxyHandler);
  1459.  
  1460. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName];
  1461. if (hookMethodProperties.classHook !== classHook) {
  1462. util.debug.log(`${util.toStr(parentObj)} [${methodName}] Cannot support functions hook and classes hook at the same time `);
  1463. return false
  1464. }
  1465.  
  1466. /* 使用hookMethod接管需要被hook的方法 */
  1467. if (parentObj[methodName] !== hookMethod) {
  1468. parentObj[methodName] = hookMethod;
  1469. }
  1470.  
  1471. t._addHook(hookMethod, fn, type, classHook);
  1472. });
  1473. }
  1474.  
  1475. /* 专门针对new操作的hook,本质上是hook函数的别名,可以少传classHook这个参数,并且明确语义 */
  1476. hookClass (parentObj, hookMethods, fn, type, context, proxyHandler) {
  1477. return this.hook(parentObj, hookMethods, fn, type, true, context, proxyHandler)
  1478. }
  1479.  
  1480. /**
  1481. * 取消对某个函数的hook
  1482. * @param parentObj {Object} -必选 要取消被hook函数依赖的父对象
  1483. * @param hookMethods {Object|Array|RegExp|string} -必选 要取消被hook函数的函数名或函数名的匹配规则
  1484. * @param type {String} -可选 默认before,指定要取消的hook类型,可选字符串:before、after、replace、error、hangUp,如果不指定该选项则取消所有类型下的所有回调
  1485. * @param fn {Function} -必选 取消指定的hook回调函数,如果不指定该选项则取消对应type类型下的所有回调
  1486. * @returns {boolean}
  1487. */
  1488. unHook (parentObj, hookMethods, type, fn) {
  1489. if (!util.isRef(parentObj) || !hookMethods) {
  1490. return false
  1491. }
  1492.  
  1493. const t = this;
  1494. hookMethods = t._getObjKeysByRule(parentObj, hookMethods);
  1495. hookMethods.forEach(methodName => {
  1496. if (!t.isAllowHook(parentObj, methodName)) {
  1497. return false
  1498. }
  1499.  
  1500. const hookMethod = parentObj[methodName];
  1501.  
  1502. if (!t.isHook(hookMethod)) {
  1503. return false
  1504. }
  1505.  
  1506. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName];
  1507. const originMethod = hookMethodProperties.originMethod;
  1508.  
  1509. if (type) {
  1510. const hookKeyName = type + 'Hooks';
  1511. const hooks = hookMethodProperties[hookKeyName] || [];
  1512.  
  1513. if (fn) {
  1514. /* 删除指定类型下的指定hook函数 */
  1515. for (let i = 0; i < hooks.length; i++) {
  1516. if (fn === hooks[i]) {
  1517. hookMethodProperties[hookKeyName].splice(i, 1);
  1518. util.debug.log(`[unHook ${hookKeyName} func] ${util.toStr(parentObj)} ${methodName}`, fn);
  1519. break
  1520. }
  1521. }
  1522. } else {
  1523. /* 删除指定类型下的所有hook函数 */
  1524. if (Array.isArray(hookMethodProperties[hookKeyName])) {
  1525. hookMethodProperties[hookKeyName] = [];
  1526. util.debug.log(`[unHook all ${hookKeyName}] ${util.toStr(parentObj)} ${methodName}`);
  1527. }
  1528. }
  1529. } else {
  1530. /* 彻底还原被hook的函数 */
  1531. if (util.isFn(originMethod)) {
  1532. parentObj[methodName] = originMethod;
  1533. delete parentObj[methodName][t.hookPropertiesKeyName];
  1534.  
  1535. // Object.keys(hookMethod).forEach(keyName => {
  1536. // if (/Hooks$/.test(keyName) && Array.isArray(hookMethod[keyName])) {
  1537. // hookMethod[keyName] = []
  1538. // }
  1539. // })
  1540. //
  1541. // hookMethod.isHook = false
  1542. // parentObj[methodName] = originMethod
  1543. // delete parentObj[methodName].originMethod
  1544. // delete parentObj[methodName].hookMethod
  1545. // delete parentObj[methodName].isHook
  1546. // delete parentObj[methodName].isClassHook
  1547.  
  1548. util.debug.log(`[unHook method] ${util.toStr(parentObj)} ${methodName}`);
  1549. }
  1550. }
  1551. });
  1552. }
  1553.  
  1554. /* 源函数运行前的hook */
  1555. before (obj, hookMethods, fn, classHook, context, proxyHandler) {
  1556. return this.hook(obj, hookMethods, fn, 'before', classHook, context, proxyHandler)
  1557. }
  1558.  
  1559. /* 源函数运行后的hook */
  1560. after (obj, hookMethods, fn, classHook, context, proxyHandler) {
  1561. return this.hook(obj, hookMethods, fn, 'after', classHook, context, proxyHandler)
  1562. }
  1563.  
  1564. /* 替换掉要hook的函数,不再运行源函数,换成运行其他逻辑 */
  1565. replace (obj, hookMethods, fn, classHook, context, proxyHandler) {
  1566. return this.hook(obj, hookMethods, fn, 'replace', classHook, context, proxyHandler)
  1567. }
  1568.  
  1569. /* 源函数运行出错时的hook */
  1570. error (obj, hookMethods, fn, classHook, context, proxyHandler) {
  1571. return this.hook(obj, hookMethods, fn, 'error', classHook, context, proxyHandler)
  1572. }
  1573.  
  1574. /* 底层实现逻辑与replace一样,都是替换掉要hook的函数,不再运行源函数,只不过是为了明确语义,将源函数挂起不再执行,原则上也不再执行其他逻辑,如果要执行其他逻辑请使用replace hook */
  1575. hangUp (obj, hookMethods, fn, classHook, context, proxyHandler) {
  1576. return this.hook(obj, hookMethods, fn, 'hangUp', classHook, context, proxyHandler)
  1577. }
  1578. }
  1579.  
  1580. var hookJs = new HookJs();
  1581.  
  1582. /*!
  1583. * @name vueHooks.js
  1584. * @description 对Vue对象进行的hooks封装
  1585. * @version 0.0.1
  1586. * @author xxxily
  1587. * @date 2022/05/10 14:11
  1588. * @github https://github.com/xxxily
  1589. */
  1590.  
  1591. const hookJsPro = hookJs.hookJsPro();
  1592.  
  1593. let vueComponentHook = null;
  1594.  
  1595. const vueHooks = {
  1596. /* 对extend进行hooks封装,以便进行组件阻断 */
  1597. blockComponents (Vue, config) {
  1598. hookJsPro.before(Vue, 'extend', (args, parentObj, methodName, originMethod, execInfo, ctx) => {
  1599. const extendOpts = args[0];
  1600. // extendOpts.__file && debug.info(`[extendOptions:${extendOpts.name}]`, extendOpts.__file)
  1601.  
  1602. const hasBlockFilter = config.blockFilters && config.blockFilters.length;
  1603. if (hasBlockFilter && extendOpts.name && filtersMatch(config.blockFilters, extendOpts.name)) {
  1604. debug.info(`[block component]: name: ${extendOpts.name}`);
  1605. return 'STOP-INVOKE'
  1606. }
  1607. });
  1608.  
  1609. /* 禁止因为阻断组件的创建而导致的错误提示输出,减少不必要的信息噪音 */
  1610. hookJsPro.before(Vue.util, 'warn', (args) => {
  1611. const msg = args[0];
  1612. if (msg.includes('STOP-INVOKE')) {
  1613. return 'STOP-INVOKE'
  1614. }
  1615. });
  1616. },
  1617.  
  1618. hackVueComponent (Vue, callback) {
  1619. if (vueComponentHook) {
  1620. debug.warn('[Vue.component] you have already hacked');
  1621. return
  1622. }
  1623.  
  1624. vueComponentHook = (args, parentObj, methodName, originMethod, execInfo, ctx) => {
  1625. const name = args[0];
  1626. const opts = args[1];
  1627.  
  1628. if (callback instanceof Function) {
  1629. callback.apply(Vue, args);
  1630. } else {
  1631. /* 打印全局组件的注册信息 */
  1632. if (Vue.options.components[name]) {
  1633. debug.warn(`[Vue.component][REPEAT][old-cid:${Vue.options.components[name].cid}]`, name, opts);
  1634. } else {
  1635. debug.log('[Vue.component]', name, opts);
  1636. }
  1637. }
  1638. };
  1639.  
  1640. hookJsPro.before(Vue, 'component', vueComponentHook);
  1641. debug.log(i18n.t('debugHelper.hackVueComponent.hack') + ' (success)');
  1642. },
  1643.  
  1644. unHackVueComponent (Vue) {
  1645. if (vueComponentHook) {
  1646. hookJsPro.unHook(Vue, 'component', 'before', vueComponentHook);
  1647. vueComponentHook = null;
  1648. debug.log(i18n.t('debugHelper.hackVueComponent.unhack') + ' (success)');
  1649. } else {
  1650. debug.warn('[Vue.component] you have not hack vue component, not need to unhack');
  1651. }
  1652. },
  1653.  
  1654. hackVueUpdate () {
  1655. //
  1656. }
  1657. };
  1658.  
  1659. /*
  1660. * author: wendux
  1661. * email: 824783146@qq.com
  1662. * source code: https://github.com/wendux/Ajax-hook
  1663. */
  1664.  
  1665. // Save original XMLHttpRequest as _rxhr
  1666. var realXhr = '_rxhr';
  1667.  
  1668. var events = ['load', 'loadend', 'timeout', 'error', 'readystatechange', 'abort'];
  1669.  
  1670. function configEvent (event, xhrProxy) {
  1671. var e = {};
  1672. for (var attr in event) e[attr] = event[attr];
  1673. // xhrProxy instead
  1674. e.target = e.currentTarget = xhrProxy;
  1675. return e
  1676. }
  1677.  
  1678. function hook (proxy, win) {
  1679. win = win || window;
  1680. // Avoid double hookAjax
  1681. win[realXhr] = win[realXhr] || win.XMLHttpRequest;
  1682.  
  1683. win.XMLHttpRequest = function () {
  1684. // We shouldn't hookAjax XMLHttpRequest.prototype because we can't
  1685. // guarantee that all attributes are on the prototype。
  1686. // Instead, hooking XMLHttpRequest instance can avoid this problem.
  1687.  
  1688. var xhr = new win[realXhr]();
  1689.  
  1690. // Generate all callbacks(eg. onload) are enumerable (not undefined).
  1691. for (var i = 0; i < events.length; ++i) {
  1692. if (xhr[events[i]] === undefined) xhr[events[i]] = null;
  1693. }
  1694.  
  1695. for (var attr in xhr) {
  1696. var type = '';
  1697. try {
  1698. type = typeof xhr[attr]; // May cause exception on some browser
  1699. } catch (e) {
  1700. }
  1701. if (type === 'function') {
  1702. // hookAjax methods of xhr, such as `open`、`send` ...
  1703. this[attr] = hookFunction(attr);
  1704. } else {
  1705. Object.defineProperty(this, attr, {
  1706. get: getterFactory(attr),
  1707. set: setterFactory(attr),
  1708. enumerable: true
  1709. });
  1710. }
  1711. }
  1712. var that = this;
  1713. xhr.getProxy = function () {
  1714. return that
  1715. };
  1716. this.xhr = xhr;
  1717. };
  1718.  
  1719. Object.assign(win.XMLHttpRequest, { UNSENT: 0, OPENED: 1, HEADERS_RECEIVED: 2, LOADING: 3, DONE: 4 });
  1720.  
  1721. // Generate getter for attributes of xhr
  1722. function getterFactory (attr) {
  1723. return function () {
  1724. var v = this.hasOwnProperty(attr + '_') ? this[attr + '_'] : this.xhr[attr];
  1725. var attrGetterHook = (proxy[attr] || {}).getter;
  1726. return attrGetterHook && attrGetterHook(v, this) || v
  1727. }
  1728. }
  1729.  
  1730. // Generate setter for attributes of xhr; by this we have an opportunity
  1731. // to hookAjax event callbacks (eg: `onload`) of xhr;
  1732. function setterFactory (attr) {
  1733. return function (v) {
  1734. var xhr = this.xhr;
  1735. var that = this;
  1736. var hook = proxy[attr];
  1737. // hookAjax event callbacks such as `onload`、`onreadystatechange`...
  1738. if (attr.substring(0, 2) === 'on') {
  1739. that[attr + '_'] = v;
  1740. xhr[attr] = function (e) {
  1741. e = configEvent(e, that);
  1742. var ret = proxy[attr] && proxy[attr].call(that, xhr, e);
  1743. ret || v.call(that, e);
  1744. };
  1745. } else {
  1746. // If the attribute isn't writable, generate proxy attribute
  1747. var attrSetterHook = (hook || {}).setter;
  1748. v = attrSetterHook && attrSetterHook(v, that) || v;
  1749. this[attr + '_'] = v;
  1750. try {
  1751. // Not all attributes of xhr are writable(setter may undefined).
  1752. xhr[attr] = v;
  1753. } catch (e) {
  1754. }
  1755. }
  1756. }
  1757. }
  1758.  
  1759. // Hook methods of xhr.
  1760. function hookFunction (fun) {
  1761. return function () {
  1762. var args = [].slice.call(arguments);
  1763. if (proxy[fun]) {
  1764. var ret = proxy[fun].call(this, args, this.xhr);
  1765. // If the proxy return value exists, return it directly,
  1766. // otherwise call the function of xhr.
  1767. if (ret) return ret
  1768. }
  1769. return this.xhr[fun].apply(this.xhr, args)
  1770. }
  1771. }
  1772.  
  1773. // Return the real XMLHttpRequest
  1774. return win[realXhr]
  1775. }
  1776.  
  1777. function unHook (win) {
  1778. win = win || window;
  1779. if (win[realXhr]) win.XMLHttpRequest = win[realXhr];
  1780. win[realXhr] = undefined;
  1781. }
  1782.  
  1783. /*
  1784. * author: wendux
  1785. * email: 824783146@qq.com
  1786. * source code: https://github.com/wendux/Ajax-hook
  1787. */
  1788.  
  1789. var eventLoad = events[0];
  1790. var eventLoadEnd = events[1];
  1791. var eventTimeout = events[2];
  1792. var eventError = events[3];
  1793. var eventReadyStateChange = events[4];
  1794. var eventAbort = events[5];
  1795.  
  1796. var singleton;
  1797. var prototype = 'prototype';
  1798.  
  1799. function proxy (proxy, win) {
  1800. if (singleton) {
  1801. throw new Error('Proxy already exists')
  1802. }
  1803.  
  1804. singleton = new Proxy$1(proxy, win);
  1805. return singleton
  1806. }
  1807.  
  1808. function unProxy (win) {
  1809. singleton = null;
  1810. unHook(win);
  1811. }
  1812.  
  1813. function trim (str) {
  1814. return str.replace(/^\s+|\s+$/g, '')
  1815. }
  1816.  
  1817. function getEventTarget (xhr) {
  1818. return xhr.watcher || (xhr.watcher = document.createElement('a'))
  1819. }
  1820.  
  1821. function triggerListener (xhr, name) {
  1822. var xhrProxy = xhr.getProxy();
  1823. var callback = 'on' + name + '_';
  1824. var event = configEvent({ type: name }, xhrProxy);
  1825. xhrProxy[callback] && xhrProxy[callback](event);
  1826. var evt;
  1827. if (typeof (Event) === 'function') {
  1828. evt = new Event(name, { bubbles: false });
  1829. } else {
  1830. // https://stackoverflow.com/questions/27176983/dispatchevent-not-working-in-ie11
  1831. evt = document.createEvent('Event');
  1832. evt.initEvent(name, false, true);
  1833. }
  1834. getEventTarget(xhr).dispatchEvent(evt);
  1835. }
  1836.  
  1837. function Handler (xhr) {
  1838. this.xhr = xhr;
  1839. this.xhrProxy = xhr.getProxy();
  1840. }
  1841.  
  1842. Handler[prototype] = Object.create({
  1843. resolve: function resolve (response) {
  1844. var xhrProxy = this.xhrProxy;
  1845. var xhr = this.xhr;
  1846. xhrProxy.readyState = 4;
  1847. xhr.resHeader = response.headers;
  1848. xhrProxy.response = xhrProxy.responseText = response.response;
  1849. xhrProxy.statusText = response.statusText;
  1850. xhrProxy.status = response.status;
  1851. triggerListener(xhr, eventReadyStateChange);
  1852. triggerListener(xhr, eventLoad);
  1853. triggerListener(xhr, eventLoadEnd);
  1854. },
  1855. reject: function reject (error) {
  1856. this.xhrProxy.status = 0;
  1857. triggerListener(this.xhr, error.type);
  1858. triggerListener(this.xhr, eventLoadEnd);
  1859. }
  1860. });
  1861.  
  1862. function makeHandler (next) {
  1863. function sub (xhr) {
  1864. Handler.call(this, xhr);
  1865. }
  1866.  
  1867. sub[prototype] = Object.create(Handler[prototype]);
  1868. sub[prototype].next = next;
  1869. return sub
  1870. }
  1871.  
  1872. var RequestHandler = makeHandler(function (rq) {
  1873. var xhr = this.xhr;
  1874. rq = rq || xhr.config;
  1875. xhr.withCredentials = rq.withCredentials;
  1876. xhr.open(rq.method, rq.url, rq.async !== false, rq.user, rq.password);
  1877. for (var key in rq.headers) {
  1878. xhr.setRequestHeader(key, rq.headers[key]);
  1879. }
  1880. xhr.send(rq.body);
  1881. });
  1882.  
  1883. var ResponseHandler = makeHandler(function (response) {
  1884. this.resolve(response);
  1885. });
  1886.  
  1887. var ErrorHandler = makeHandler(function (error) {
  1888. this.reject(error);
  1889. });
  1890.  
  1891. function Proxy$1 (proxy, win) {
  1892. var onRequest = proxy.onRequest;
  1893. var onResponse = proxy.onResponse;
  1894. var onError = proxy.onError;
  1895.  
  1896. function handleResponse (xhr, xhrProxy) {
  1897. var handler = new ResponseHandler(xhr);
  1898. var ret = {
  1899. response: xhrProxy.response,
  1900. status: xhrProxy.status,
  1901. statusText: xhrProxy.statusText,
  1902. config: xhr.config,
  1903. headers: xhr.resHeader || xhr.getAllResponseHeaders().split('\r\n').reduce(function (ob, str) {
  1904. if (str === '') return ob
  1905. var m = str.split(':');
  1906. ob[m.shift()] = trim(m.join(':'));
  1907. return ob
  1908. }, {})
  1909. };
  1910. if (!onResponse) return handler.resolve(ret)
  1911. onResponse(ret, handler);
  1912. }
  1913.  
  1914. function onerror (xhr, xhrProxy, error, errorType) {
  1915. var handler = new ErrorHandler(xhr);
  1916. error = { config: xhr.config, error: error, type: errorType };
  1917. if (onError) {
  1918. onError(error, handler);
  1919. } else {
  1920. handler.next(error);
  1921. }
  1922. }
  1923.  
  1924. function preventXhrProxyCallback () {
  1925. return true
  1926. }
  1927.  
  1928. function errorCallback (errorType) {
  1929. return function (xhr, e) {
  1930. onerror(xhr, this, e, errorType);
  1931. return true
  1932. }
  1933. }
  1934.  
  1935. function stateChangeCallback (xhr, xhrProxy) {
  1936. if (xhr.readyState === 4 && xhr.status !== 0) {
  1937. handleResponse(xhr, xhrProxy);
  1938. } else if (xhr.readyState !== 4) {
  1939. triggerListener(xhr, eventReadyStateChange);
  1940. }
  1941. return true
  1942. }
  1943.  
  1944. return hook({
  1945. onload: preventXhrProxyCallback,
  1946. onloadend: preventXhrProxyCallback,
  1947. onerror: errorCallback(eventError),
  1948. ontimeout: errorCallback(eventTimeout),
  1949. onabort: errorCallback(eventAbort),
  1950. onreadystatechange: function (xhr) {
  1951. return stateChangeCallback(xhr, this)
  1952. },
  1953. open: function open (args, xhr) {
  1954. var _this = this;
  1955. var config = xhr.config = { headers: {} };
  1956. config.method = args[0];
  1957. config.url = args[1];
  1958. config.async = args[2];
  1959. config.user = args[3];
  1960. config.password = args[4];
  1961. config.xhr = xhr;
  1962. var evName = 'on' + eventReadyStateChange;
  1963. if (!xhr[evName]) {
  1964. xhr[evName] = function () {
  1965. return stateChangeCallback(xhr, _this)
  1966. };
  1967. }
  1968.  
  1969. // 如果有请求拦截器,则在调用onRequest后再打开链接。因为onRequest最佳调用时机是在send前,
  1970. // 所以我们在send拦截函数中再手动调用open,因此返回true阻止xhr.open调用。
  1971. //
  1972. // 如果没有请求拦截器,则不用阻断xhr.open调用
  1973. if (onRequest) return true
  1974. },
  1975. send: function (args, xhr) {
  1976. var config = xhr.config;
  1977. config.withCredentials = xhr.withCredentials;
  1978. config.body = args[0];
  1979. if (onRequest) {
  1980. // In 'onRequest', we may call XHR's event handler, such as `xhr.onload`.
  1981. // However, XHR's event handler may not be set until xhr.send is called in
  1982. // the user's code, so we use `setTimeout` to avoid this situation
  1983. var req = function () {
  1984. onRequest(config, new RequestHandler(xhr));
  1985. };
  1986. config.async === false ? req() : setTimeout(req);
  1987. return true
  1988. }
  1989. },
  1990. setRequestHeader: function (args, xhr) {
  1991. // Collect request headers
  1992. xhr.config.headers[args[0].toLowerCase()] = args[1];
  1993. return true
  1994. },
  1995. addEventListener: function (args, xhr) {
  1996. var _this = this;
  1997. if (events.indexOf(args[0]) !== -1) {
  1998. var handler = args[1];
  1999. getEventTarget(xhr).addEventListener(args[0], function (e) {
  2000. var event = configEvent(e, _this);
  2001. event.type = args[0];
  2002. event.isTrusted = true;
  2003. handler.call(_this, event);
  2004. });
  2005. return true
  2006. }
  2007. },
  2008. getAllResponseHeaders: function (_, xhr) {
  2009. var headers = xhr.resHeader;
  2010. if (headers) {
  2011. var header = '';
  2012. for (var key in headers) {
  2013. header += key + ': ' + headers[key] + '\r\n';
  2014. }
  2015. return header
  2016. }
  2017. },
  2018. getResponseHeader: function (args, xhr) {
  2019. var headers = xhr.resHeader;
  2020. if (headers) {
  2021. return headers[(args[0] || '').toLowerCase()]
  2022. }
  2023. }
  2024. }, win)
  2025. }
  2026.  
  2027. /*!
  2028. * @name cacheStore.js
  2029. * @description 接口请求缓存存储管理模块
  2030. * @version 0.0.1
  2031. * @author xxxily
  2032. * @date 2022/05/13 09:36
  2033. * @github https://github.com/xxxily
  2034. */
  2035. const localforage = window.localforage;
  2036. const CryptoJS = window.CryptoJS;
  2037.  
  2038. function md5 (str) {
  2039. return CryptoJS.MD5(str).toString()
  2040. }
  2041.  
  2042. function createHash (config) {
  2043. if (config._hash_) {
  2044. return config._hash_
  2045. }
  2046.  
  2047. let url = config.url || '';
  2048.  
  2049. /**
  2050. * 如果检测到url使用了时间戳来防止缓存,则进行替换,进行缓存
  2051. * TODO
  2052. * 注意,这很可能会导致误伤,例如url上的时间戳并不是用来清理缓存的,而是某个时间点的参数
  2053. */
  2054. if (/=\d{13}/.test(url)) {
  2055. url = url.replace(/=\d{13}/, '=cache');
  2056. }
  2057.  
  2058. const hash = md5(url);
  2059. config._hash_ = hash;
  2060.  
  2061. return hash
  2062. }
  2063.  
  2064. class CacheStore {
  2065. constructor (opts = {
  2066. localforageConfig: {}
  2067. }) {
  2068. this.store = localforage.createInstance(Object.assign({
  2069. name: 'vue-debug-helper-cache',
  2070. storeName: 'ajax-cache'
  2071. }, opts.localforageConfig));
  2072.  
  2073. /* 外部应该使用同样的hash生成方法,否则无法正常命中缓存规则 */
  2074. this.createHash = createHash;
  2075. }
  2076.  
  2077. async getCache (config) {
  2078. const hash = createHash(config);
  2079. const data = await this.store.getItem(hash);
  2080. return data
  2081. }
  2082.  
  2083. async setCache (response, filter) {
  2084. const headers = response.headers || {};
  2085. if (String(headers['content-type']).includes(filter || 'application/json')) {
  2086. const hash = createHash(response.config);
  2087. await this.store.setItem(hash, response.response);
  2088.  
  2089. /* 设置缓存的时候顺便更新缓存相关的基础信息,注意,该信息并不能100%被同步到本地 */
  2090. await this.updateCacheInfo(response.config);
  2091.  
  2092. debug.log(`[cacheStore setCache] ${response.config.url}`);
  2093. }
  2094. }
  2095.  
  2096. async getCacheInfo (config) {
  2097. const hash = config ? this.createHash(config) : '';
  2098. if (this._cacheInfo_) {
  2099. return hash ? this._cacheInfo_[hash] : this._cacheInfo_
  2100. }
  2101.  
  2102. /* 在没将cacheInfo加载到内存前,只能单线程获取cacheInfo,防止多线程获取cacheInfo时出现问题 */
  2103. if (this._takeingCacheInfo_) {
  2104. const getCacheInfoHanderList = this._getCacheInfoHanderList_ || [];
  2105. const P = new Promise((resolve, reject) => {
  2106. getCacheInfoHanderList.push({
  2107. resolve,
  2108. config
  2109. });
  2110. });
  2111. this._getCacheInfoHanderList_ = getCacheInfoHanderList;
  2112. return P
  2113. }
  2114.  
  2115. this._takeingCacheInfo_ = true;
  2116. const cacheInfo = await this.store.getItem('ajaxCacheInfo') || {};
  2117. this._cacheInfo_ = cacheInfo;
  2118.  
  2119. delete this._takeingCacheInfo_;
  2120. if (this._getCacheInfoHanderList_) {
  2121. this._getCacheInfoHanderList_.forEach(async (handler) => {
  2122. handler.resolve(await this.getCacheInfo(handler.config));
  2123. });
  2124. delete this._getCacheInfoHanderList_;
  2125. }
  2126.  
  2127. return hash ? cacheInfo[hash] : cacheInfo
  2128. }
  2129.  
  2130. async updateCacheInfo (config) {
  2131. const cacheInfo = await this.getCacheInfo();
  2132.  
  2133. const hash = createHash(config);
  2134. if (hash && config) {
  2135. const info = {
  2136. url: config.url,
  2137. cacheTime: Date.now()
  2138. };
  2139.  
  2140. // 增加或更新缓存的基本信息
  2141. cacheInfo[hash] = info;
  2142. }
  2143.  
  2144. if (!this._updateCacheInfoIsWorking_) {
  2145. this._updateCacheInfoIsWorking_ = true;
  2146. await this.store.setItem('ajaxCacheInfo', cacheInfo);
  2147. this._updateCacheInfoIsWorking_ = false;
  2148. }
  2149. }
  2150.  
  2151. /**
  2152. * 清理已过期的缓存数据
  2153. * @param {number} expires 指定过期时间,单位:毫秒
  2154. * @returns
  2155. */
  2156. async cleanCache (expires) {
  2157. if (!expires) {
  2158. return
  2159. }
  2160.  
  2161. const cacheInfo = await this.getCacheInfo();
  2162. const cacheInfoKeys = Object.keys(cacheInfo);
  2163. const now = Date.now();
  2164.  
  2165. const storeKeys = await this.store.keys();
  2166.  
  2167. const needKeepKeys = cacheInfoKeys.filter(key => now - cacheInfo[key].cacheTime < expires);
  2168. needKeepKeys.push('ajaxCacheInfo');
  2169.  
  2170. /* 清理不需要的数据 */
  2171. storeKeys.forEach(async (key) => {
  2172. if (!needKeepKeys.includes(key)) {
  2173. this.store.removeItem(key);
  2174. }
  2175. });
  2176. }
  2177.  
  2178. async get (key) {
  2179. const data = await this.store.getItem(key);
  2180. debug.log('[cacheStore]', key, data);
  2181. return data
  2182. }
  2183.  
  2184. async set (key, data) {
  2185. await this.store.setItem(key, data);
  2186. debug.log('[cacheStore]', key, data);
  2187. }
  2188.  
  2189. async remove (key) {
  2190. await this.store.removeItem(key);
  2191. debug.log('[cacheStore]', key);
  2192. }
  2193.  
  2194. async clear () {
  2195. await this.store.clear();
  2196. debug.log('[cacheStore] clear');
  2197. }
  2198.  
  2199. async keys () {
  2200. const keys = await this.store.keys();
  2201. debug.log('[cacheStore] keys', keys);
  2202. return keys
  2203. }
  2204. }
  2205.  
  2206. var cacheStore = new CacheStore();
  2207.  
  2208. /*!
  2209. * @name ajaxHooks.js
  2210. * @description 底层请求hook
  2211. * @version 0.0.1
  2212. * @author xxxily
  2213. * @date 2022/05/12 17:46
  2214. * @github https://github.com/xxxily
  2215. */
  2216.  
  2217. /**
  2218. * 判断是否符合进行缓存控制操作的条件
  2219. * @param {object} config
  2220. * @returns {boolean}
  2221. */
  2222. function useCache (config) {
  2223. const ajaxCache = helper.config.ajaxCache;
  2224. if (ajaxCache.enabled) {
  2225. return filtersMatch(ajaxCache.filters, config.url)
  2226. } else {
  2227. return false
  2228. }
  2229. }
  2230.  
  2231. let ajaxHooksWin = window;
  2232.  
  2233. const ajaxHooks = {
  2234. hook (win = ajaxHooksWin) {
  2235. proxy({
  2236. onRequest: async (config, handler) => {
  2237. let hitCache = false;
  2238.  
  2239. if (useCache(config)) {
  2240. const cacheInfo = await cacheStore.getCacheInfo(config);
  2241. const cache = await cacheStore.getCache(config);
  2242.  
  2243. if (cache && cacheInfo) {
  2244. const isExpires = Date.now() - cacheInfo.cacheTime > helper.config.ajaxCache.expires;
  2245.  
  2246. if (!isExpires) {
  2247. handler.resolve({
  2248. config: config,
  2249. status: 200,
  2250. headers: { 'content-type': 'application/json' },
  2251. response: cache
  2252. });
  2253.  
  2254. hitCache = true;
  2255. }
  2256. }
  2257. }
  2258.  
  2259. if (hitCache) {
  2260. debug.warn(`[ajaxHooks] use cache:${config.url}`);
  2261. } else {
  2262. handler.next(config);
  2263. }
  2264. },
  2265.  
  2266. onError: (err, handler) => {
  2267. handler.next(err);
  2268. },
  2269.  
  2270. onResponse: async (response, handler) => {
  2271. if (useCache(response.config)) {
  2272. // 加入缓存
  2273. cacheStore.setCache(response, 'application/json');
  2274. }
  2275.  
  2276. handler.next(response);
  2277. }
  2278. }, win);
  2279. },
  2280.  
  2281. unHook (win = ajaxHooksWin) {
  2282. unProxy(win);
  2283. },
  2284.  
  2285. init (win) {
  2286. ajaxHooksWin = win;
  2287.  
  2288. if (helper.config.ajaxCache.enabled) {
  2289. ajaxHooks.hook(ajaxHooksWin);
  2290. }
  2291.  
  2292. /* 定时清除接口的缓存数据,防止不断堆积 */
  2293. setTimeout(() => {
  2294. cacheStore.cleanCache();
  2295. }, 1000 * 10);
  2296. }
  2297. };
  2298.  
  2299. /*!
  2300. * @name performanceObserver.js
  2301. * @description 进行性能监测结果的打印
  2302. * @version 0.0.1
  2303. * @author xxxily
  2304. * @date 2022/05/11 10:39
  2305. * @github https://github.com/xxxily
  2306. */
  2307.  
  2308. const performanceObserver = {
  2309. observer: null,
  2310. init () {
  2311. if (typeof PerformanceObserver === 'undefined') {
  2312. debug.log(i18n.t('debugHelper.performanceObserver.notSupport'));
  2313. return false
  2314. }
  2315.  
  2316. if (performanceObserver.observer && performanceObserver.observer.disconnect) {
  2317. performanceObserver.observer.disconnect();
  2318. }
  2319.  
  2320. /* 不进行性能观察 */
  2321. if (!helper.config.performanceObserver.enabled) {
  2322. performanceObserver.observer = null;
  2323. return false
  2324. }
  2325.  
  2326. // https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver/observe
  2327. performanceObserver.observer = new PerformanceObserver(function (list, observer) {
  2328. if (!helper.config.performanceObserver.enabled) {
  2329. return
  2330. }
  2331.  
  2332. const entries = list.getEntries();
  2333. for (let i = 0; i < entries.length; i++) {
  2334. const entry = entries[i];
  2335. debug.info(`[performanceObserver ${entry.entryType}]`, entry);
  2336. }
  2337. });
  2338.  
  2339. // https://runebook.dev/zh-CN/docs/dom/performanceentry/entrytype
  2340. performanceObserver.observer.observe({ entryTypes: helper.config.performanceObserver.entryTypes });
  2341. }
  2342. };
  2343.  
  2344. /*!
  2345. * @name functionCall.js
  2346. * @description 统一的提供外部功能调用管理模块
  2347. * @version 0.0.1
  2348. * @author xxxily
  2349. * @date 2022/04/27 17:42
  2350. * @github https://github.com/xxxily
  2351. */
  2352.  
  2353. const functionCall = {
  2354. viewVueDebugHelperObject () {
  2355. debug.log(i18n.t('debugHelper.viewVueDebugHelperObject'), helper);
  2356. },
  2357. componentsStatistics () {
  2358. const result = helper.methods.componentsStatistics();
  2359. let total = 0;
  2360.  
  2361. /* 提供友好的可视化展示方式 */
  2362. console.table && console.table(result.map(item => {
  2363. total += item.componentInstance.length;
  2364. return {
  2365. componentName: item.componentName,
  2366. count: item.componentInstance.length
  2367. }
  2368. }));
  2369.  
  2370. debug.log(`${i18n.t('debugHelper.componentsStatistics')} (total:${total})`, result);
  2371. },
  2372. destroyStatisticsSort () {
  2373. const result = helper.methods.destroyStatisticsSort();
  2374. let total = 0;
  2375.  
  2376. /* 提供友好的可视化展示方式 */
  2377. console.table && console.table(result.map(item => {
  2378. const durationList = item.destroyList.map(item => item.duration);
  2379. const maxDuration = Math.max(...durationList);
  2380. const minDuration = Math.min(...durationList);
  2381. const durationRange = maxDuration - minDuration;
  2382. total += item.destroyList.length;
  2383.  
  2384. return {
  2385. componentName: item.componentName,
  2386. count: item.destroyList.length,
  2387. avgDuration: durationList.reduce((pre, cur) => pre + cur, 0) / durationList.length,
  2388. maxDuration,
  2389. minDuration,
  2390. durationRange,
  2391. durationRangePercent: (1000 - minDuration) / durationRange
  2392. }
  2393. }));
  2394.  
  2395. debug.log(`${i18n.t('debugHelper.destroyStatisticsSort')} (total:${total})`, result);
  2396. },
  2397. componentsSummaryStatisticsSort () {
  2398. const result = helper.methods.componentsSummaryStatisticsSort();
  2399. let total = 0;
  2400.  
  2401. /* 提供友好的可视化展示方式 */
  2402. console.table && console.table(result.map(item => {
  2403. total += item.componentsSummary.length;
  2404. return {
  2405. componentName: item.componentName,
  2406. count: item.componentsSummary.length
  2407. }
  2408. }));
  2409.  
  2410. debug.log(`${i18n.t('debugHelper.componentsSummaryStatisticsSort')} (total:${total})`, result);
  2411. },
  2412. getDestroyByDuration () {
  2413. const destroyInfo = helper.methods.getDestroyByDuration();
  2414. console.table && console.table(destroyInfo.destroyList);
  2415. debug.log(i18n.t('debugHelper.getDestroyByDuration'), destroyInfo);
  2416. },
  2417. clearAll () {
  2418. helper.methods.clearAll();
  2419. debug.log(i18n.t('debugHelper.clearAll'));
  2420. },
  2421.  
  2422. printLifeCycleInfo () {
  2423. const lifecycleFilters = window.prompt(i18n.t('debugHelper.printLifeCycleInfoPrompt.lifecycleFilters'), helper.config.lifecycle.filters.join(','));
  2424. const componentFilters = window.prompt(i18n.t('debugHelper.printLifeCycleInfoPrompt.componentFilters'), helper.config.lifecycle.componentFilters.join(','));
  2425.  
  2426. if (lifecycleFilters !== null && componentFilters !== null) {
  2427. debug.log(i18n.t('debugHelper.printLifeCycleInfo'));
  2428. helper.methods.printLifeCycleInfo(lifecycleFilters, componentFilters);
  2429. }
  2430. },
  2431.  
  2432. notPrintLifeCycleInfo () {
  2433. debug.log(i18n.t('debugHelper.notPrintLifeCycleInfo'));
  2434. helper.methods.notPrintLifeCycleInfo();
  2435. },
  2436.  
  2437. findComponents () {
  2438. const filters = window.prompt(i18n.t('debugHelper.findComponentsPrompt.filters'), helper.config.findComponentsFilters.join(','));
  2439. if (filters !== null) {
  2440. debug.log(i18n.t('debugHelper.findComponents'), helper.methods.findComponents(filters));
  2441. }
  2442. },
  2443.  
  2444. findNotContainElementComponents () {
  2445. debug.log(i18n.t('debugHelper.findNotContainElementComponents'), helper.methods.findNotContainElementComponents());
  2446. },
  2447.  
  2448. blockComponents () {
  2449. const filters = window.prompt(i18n.t('debugHelper.blockComponentsPrompt.filters'), helper.config.blockFilters.join(','));
  2450. if (filters !== null) {
  2451. helper.methods.blockComponents(filters);
  2452. debug.log(i18n.t('debugHelper.blockComponents'), filters);
  2453. }
  2454. },
  2455.  
  2456. dd () {
  2457. const filter = window.prompt(i18n.t('debugHelper.ddPrompt.filter'), helper.config.dd.filters.join(','));
  2458. const count = window.prompt(i18n.t('debugHelper.ddPrompt.count'), helper.config.dd.count);
  2459.  
  2460. if (filter !== null && count !== null) {
  2461. debug.log(i18n.t('debugHelper.dd'));
  2462. helper.methods.dd(filter, Number(count));
  2463. }
  2464. },
  2465.  
  2466. undd () {
  2467. debug.log(i18n.t('debugHelper.undd'));
  2468. helper.methods.undd();
  2469. },
  2470.  
  2471. toggleHackVueComponent () {
  2472. helper.config.hackVueComponent ? vueHooks.unHackVueComponent() : vueHooks.hackVueComponent();
  2473. helper.config.hackVueComponent = !helper.config.hackVueComponent;
  2474. },
  2475.  
  2476. toggleInspect () {
  2477. helper.config.inspect.enabled = !helper.config.inspect.enabled;
  2478. debug.log(`${i18n.t('debugHelper.toggleInspect')} success (${helper.config.inspect.enabled})`);
  2479. },
  2480.  
  2481. togglePerformanceObserver () {
  2482. helper.config.performanceObserver.enabled = !helper.config.performanceObserver.enabled;
  2483.  
  2484. if (helper.config.performanceObserver.enabled) {
  2485. let entryTypes = window.prompt(i18n.t('debugHelper.performanceObserverPrompt.entryTypes'), helper.config.performanceObserver.entryTypes.join(','));
  2486. if (entryTypes) {
  2487. const entryTypesArr = toArrFilters(entryTypes);
  2488. const supportEntryTypes = ['element', 'navigation', 'resource', 'mark', 'measure', 'paint', 'longtask'];
  2489.  
  2490. /* 过滤出支持的entryTypes */
  2491. entryTypes = entryTypesArr.filter(item => supportEntryTypes.includes(item));
  2492.  
  2493. if (entryTypes.length !== entryTypesArr.length) {
  2494. debug.warn(`some entryTypes not support, only support: ${supportEntryTypes.join(',')}`);
  2495. }
  2496.  
  2497. helper.config.performanceObserver.entryTypes = entryTypes;
  2498.  
  2499. performanceObserver.init();
  2500. } else {
  2501. alert('entryTypes is empty');
  2502. }
  2503. }
  2504.  
  2505. debug.log(`${i18n.t('debugHelper.togglePerformanceObserver')} success (${helper.config.performanceObserver.enabled})`);
  2506. },
  2507.  
  2508. useAjaxCache () {
  2509. helper.config.ajaxCache.enabled = true;
  2510.  
  2511. const filters = window.prompt(i18n.t('debugHelper.jaxCachePrompt.filters'), helper.config.ajaxCache.filters.join(','));
  2512. const expires = window.prompt(i18n.t('debugHelper.jaxCachePrompt.expires'), helper.config.ajaxCache.expires / 1000 / 60);
  2513.  
  2514. if (filters && expires) {
  2515. helper.config.ajaxCache.filters = toArrFilters(filters);
  2516.  
  2517. if (!isNaN(Number(expires))) {
  2518. helper.config.ajaxCache.expires = Number(expires) * 1000 * 60;
  2519. }
  2520.  
  2521. ajaxHooks.hook();
  2522.  
  2523. debug.log(`${i18n.t('debugHelper.enableAjaxCacheTips')}`);
  2524. }
  2525. },
  2526.  
  2527. disableAjaxCache () {
  2528. helper.config.ajaxCache.enabled = false;
  2529. ajaxHooks.unHook();
  2530. debug.log(`${i18n.t('debugHelper.disableAjaxCacheTips')}`);
  2531. },
  2532.  
  2533. toggleAjaxCache () {
  2534. if (helper.config.ajaxCache.enabled) {
  2535. functionCall.disableAjaxCache();
  2536. } else {
  2537. functionCall.useAjaxCache();
  2538. }
  2539. },
  2540.  
  2541. async clearAjaxCache () {
  2542. await cacheStore.store.clear();
  2543. debug.log(`${i18n.t('debugHelper.clearAjaxCacheTips')}`);
  2544. }
  2545. };
  2546.  
  2547. /*!
  2548. * @name menu.js
  2549. * @description vue-debug-helper的菜单配置
  2550. * @version 0.0.1
  2551. * @author xxxily
  2552. * @date 2022/04/25 22:28
  2553. * @github https://github.com/xxxily
  2554. */
  2555.  
  2556. function menuRegister (Vue) {
  2557. if (!Vue) {
  2558. monkeyMenu.on('not detected ' + i18n.t('issues'), () => {
  2559. window.GM_openInTab('https://github.com/xxxily/vue-debug-helper/issues', {
  2560. active: true,
  2561. insert: true,
  2562. setParent: true
  2563. });
  2564. });
  2565. return false
  2566. }
  2567.  
  2568. /* 批量注册菜单 */
  2569. Object.keys(functionCall).forEach(key => {
  2570. const text = i18n.t(`debugHelper.${key}`);
  2571. if (text && functionCall[key] instanceof Function) {
  2572. monkeyMenu.on(text, functionCall[key]);
  2573. }
  2574. });
  2575.  
  2576. /* 是否开启vue-devtools的菜单 */
  2577. const devtoolsText = helper.config.devtools ? i18n.t('debugHelper.devtools.disable') : i18n.t('debugHelper.devtools.enabled');
  2578. monkeyMenu.on(devtoolsText, helper.methods.toggleDevtools);
  2579.  
  2580. // monkeyMenu.on('i18n.t('setting')', () => {
  2581. // window.alert('功能开发中,敬请期待...')
  2582. // })
  2583.  
  2584. monkeyMenu.on(i18n.t('issues'), () => {
  2585. window.GM_openInTab('https://github.com/xxxily/vue-debug-helper/issues', {
  2586. active: true,
  2587. insert: true,
  2588. setParent: true
  2589. });
  2590. });
  2591.  
  2592. // monkeyMenu.on(i18n.t('donate'), () => {
  2593. // window.GM_openInTab('https://cdn.jsdelivr.net/gh/xxxily/vue-debug-helper@main/donate.png', {
  2594. // active: true,
  2595. // insert: true,
  2596. // setParent: true
  2597. // })
  2598. // })
  2599. }
  2600.  
  2601. const isff = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase().indexOf('firefox') > 0 : false;
  2602.  
  2603. // 绑定事件
  2604. function addEvent (object, event, method) {
  2605. if (object.addEventListener) {
  2606. object.addEventListener(event, method, false);
  2607. } else if (object.attachEvent) {
  2608. object.attachEvent(`on${event}`, () => { method(window.event); });
  2609. }
  2610. }
  2611.  
  2612. // 修饰键转换成对应的键码
  2613. function getMods (modifier, key) {
  2614. const mods = key.slice(0, key.length - 1);
  2615. for (let i = 0; i < mods.length; i++) mods[i] = modifier[mods[i].toLowerCase()];
  2616. return mods
  2617. }
  2618.  
  2619. // 处理传的key字符串转换成数组
  2620. function getKeys (key) {
  2621. if (typeof key !== 'string') key = '';
  2622. key = key.replace(/\s/g, ''); // 匹配任何空白字符,包括空格、制表符、换页符等等
  2623. const keys = key.split(','); // 同时设置多个快捷键,以','分割
  2624. let index = keys.lastIndexOf('');
  2625.  
  2626. // 快捷键可能包含',',需特殊处理
  2627. for (; index >= 0;) {
  2628. keys[index - 1] += ',';
  2629. keys.splice(index, 1);
  2630. index = keys.lastIndexOf('');
  2631. }
  2632.  
  2633. return keys
  2634. }
  2635.  
  2636. // 比较修饰键的数组
  2637. function compareArray (a1, a2) {
  2638. const arr1 = a1.length >= a2.length ? a1 : a2;
  2639. const arr2 = a1.length >= a2.length ? a2 : a1;
  2640. let isIndex = true;
  2641.  
  2642. for (let i = 0; i < arr1.length; i++) {
  2643. if (arr2.indexOf(arr1[i]) === -1) isIndex = false;
  2644. }
  2645. return isIndex
  2646. }
  2647.  
  2648. // Special Keys
  2649. const _keyMap = {
  2650. backspace: 8,
  2651. tab: 9,
  2652. clear: 12,
  2653. enter: 13,
  2654. return: 13,
  2655. esc: 27,
  2656. escape: 27,
  2657. space: 32,
  2658. left: 37,
  2659. up: 38,
  2660. right: 39,
  2661. down: 40,
  2662. del: 46,
  2663. delete: 46,
  2664. ins: 45,
  2665. insert: 45,
  2666. home: 36,
  2667. end: 35,
  2668. pageup: 33,
  2669. pagedown: 34,
  2670. capslock: 20,
  2671. num_0: 96,
  2672. num_1: 97,
  2673. num_2: 98,
  2674. num_3: 99,
  2675. num_4: 100,
  2676. num_5: 101,
  2677. num_6: 102,
  2678. num_7: 103,
  2679. num_8: 104,
  2680. num_9: 105,
  2681. num_multiply: 106,
  2682. num_add: 107,
  2683. num_enter: 108,
  2684. num_subtract: 109,
  2685. num_decimal: 110,
  2686. num_divide: 111,
  2687. '⇪': 20,
  2688. ',': 188,
  2689. '.': 190,
  2690. '/': 191,
  2691. '`': 192,
  2692. '-': isff ? 173 : 189,
  2693. '=': isff ? 61 : 187,
  2694. ';': isff ? 59 : 186,
  2695. '\'': 222,
  2696. '[': 219,
  2697. ']': 221,
  2698. '\\': 220
  2699. };
  2700.  
  2701. // Modifier Keys
  2702. const _modifier = {
  2703. // shiftKey
  2704. '⇧': 16,
  2705. shift: 16,
  2706. // altKey
  2707. '⌥': 18,
  2708. alt: 18,
  2709. option: 18,
  2710. // ctrlKey
  2711. '⌃': 17,
  2712. ctrl: 17,
  2713. control: 17,
  2714. // metaKey
  2715. '⌘': 91,
  2716. cmd: 91,
  2717. command: 91
  2718. };
  2719. const modifierMap = {
  2720. 16: 'shiftKey',
  2721. 18: 'altKey',
  2722. 17: 'ctrlKey',
  2723. 91: 'metaKey',
  2724.  
  2725. shiftKey: 16,
  2726. ctrlKey: 17,
  2727. altKey: 18,
  2728. metaKey: 91
  2729. };
  2730. const _mods = {
  2731. 16: false,
  2732. 18: false,
  2733. 17: false,
  2734. 91: false
  2735. };
  2736. const _handlers = {};
  2737.  
  2738. // F1~F12 special key
  2739. for (let k = 1; k < 20; k++) {
  2740. _keyMap[`f${k}`] = 111 + k;
  2741. }
  2742.  
  2743. // https://github.com/jaywcjlove/hotkeys
  2744.  
  2745. let _downKeys = []; // 记录摁下的绑定键
  2746. let winListendFocus = false; // window是否已经监听了focus事件
  2747. let _scope = 'all'; // 默认热键范围
  2748. const elementHasBindEvent = []; // 已绑定事件的节点记录
  2749.  
  2750. // 返回键码
  2751. const code = (x) => _keyMap[x.toLowerCase()] ||
  2752. _modifier[x.toLowerCase()] ||
  2753. x.toUpperCase().charCodeAt(0);
  2754.  
  2755. // 设置获取当前范围(默认为'所有')
  2756. function setScope (scope) {
  2757. _scope = scope || 'all';
  2758. }
  2759. // 获取当前范围
  2760. function getScope () {
  2761. return _scope || 'all'
  2762. }
  2763. // 获取摁下绑定键的键值
  2764. function getPressedKeyCodes () {
  2765. return _downKeys.slice(0)
  2766. }
  2767.  
  2768. // 表单控件控件判断 返回 Boolean
  2769. // hotkey is effective only when filter return true
  2770. function filter (event) {
  2771. const target = event.target || event.srcElement;
  2772. const { tagName } = target;
  2773. let flag = true;
  2774. // ignore: isContentEditable === 'true', <input> and <textarea> when readOnly state is false, <select>
  2775. if (
  2776. target.isContentEditable ||
  2777. ((tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') && !target.readOnly)
  2778. ) {
  2779. flag = false;
  2780. }
  2781. return flag
  2782. }
  2783.  
  2784. // 判断摁下的键是否为某个键,返回true或者false
  2785. function isPressed (keyCode) {
  2786. if (typeof keyCode === 'string') {
  2787. keyCode = code(keyCode); // 转换成键码
  2788. }
  2789. return _downKeys.indexOf(keyCode) !== -1
  2790. }
  2791.  
  2792. // 循环删除handlers中的所有 scope(范围)
  2793. function deleteScope (scope, newScope) {
  2794. let handlers;
  2795. let i;
  2796.  
  2797. // 没有指定scope,获取scope
  2798. if (!scope) scope = getScope();
  2799.  
  2800. for (const key in _handlers) {
  2801. if (Object.prototype.hasOwnProperty.call(_handlers, key)) {
  2802. handlers = _handlers[key];
  2803. for (i = 0; i < handlers.length;) {
  2804. if (handlers[i].scope === scope) handlers.splice(i, 1);
  2805. else i++;
  2806. }
  2807. }
  2808. }
  2809.  
  2810. // 如果scope被删除,将scope重置为all
  2811. if (getScope() === scope) setScope(newScope || 'all');
  2812. }
  2813.  
  2814. // 清除修饰键
  2815. function clearModifier (event) {
  2816. let key = event.keyCode || event.which || event.charCode;
  2817. const i = _downKeys.indexOf(key);
  2818.  
  2819. // 从列表中清除按压过的键
  2820. if (i >= 0) {
  2821. _downKeys.splice(i, 1);
  2822. }
  2823. // 特殊处理 cmmand 键,在 cmmand 组合快捷键 keyup 只执行一次的问题
  2824. if (event.key && event.key.toLowerCase() === 'meta') {
  2825. _downKeys.splice(0, _downKeys.length);
  2826. }
  2827.  
  2828. // 修饰键 shiftKey altKey ctrlKey (command||metaKey) 清除
  2829. if (key === 93 || key === 224) key = 91;
  2830. if (key in _mods) {
  2831. _mods[key] = false;
  2832.  
  2833. // 将修饰键重置为false
  2834. for (const k in _modifier) if (_modifier[k] === key) hotkeys[k] = false;
  2835. }
  2836. }
  2837.  
  2838. function unbind (keysInfo, ...args) {
  2839. // unbind(), unbind all keys
  2840. if (!keysInfo) {
  2841. Object.keys(_handlers).forEach((key) => delete _handlers[key]);
  2842. } else if (Array.isArray(keysInfo)) {
  2843. // support like : unbind([{key: 'ctrl+a', scope: 's1'}, {key: 'ctrl-a', scope: 's2', splitKey: '-'}])
  2844. keysInfo.forEach((info) => {
  2845. if (info.key) eachUnbind(info);
  2846. });
  2847. } else if (typeof keysInfo === 'object') {
  2848. // support like unbind({key: 'ctrl+a, ctrl+b', scope:'abc'})
  2849. if (keysInfo.key) eachUnbind(keysInfo);
  2850. } else if (typeof keysInfo === 'string') {
  2851. // support old method
  2852. // eslint-disable-line
  2853. let [scope, method] = args;
  2854. if (typeof scope === 'function') {
  2855. method = scope;
  2856. scope = '';
  2857. }
  2858. eachUnbind({
  2859. key: keysInfo,
  2860. scope,
  2861. method,
  2862. splitKey: '+'
  2863. });
  2864. }
  2865. }
  2866.  
  2867. // 解除绑定某个范围的快捷键
  2868. const eachUnbind = ({
  2869. key, scope, method, splitKey = '+'
  2870. }) => {
  2871. const multipleKeys = getKeys(key);
  2872. multipleKeys.forEach((originKey) => {
  2873. const unbindKeys = originKey.split(splitKey);
  2874. const len = unbindKeys.length;
  2875. const lastKey = unbindKeys[len - 1];
  2876. const keyCode = lastKey === '*' ? '*' : code(lastKey);
  2877. if (!_handlers[keyCode]) return
  2878. // 判断是否传入范围,没有就获取范围
  2879. if (!scope) scope = getScope();
  2880. const mods = len > 1 ? getMods(_modifier, unbindKeys) : [];
  2881. _handlers[keyCode] = _handlers[keyCode].filter((record) => {
  2882. // 通过函数判断,是否解除绑定,函数相等直接返回
  2883. const isMatchingMethod = method ? record.method === method : true;
  2884. return !(
  2885. isMatchingMethod &&
  2886. record.scope === scope &&
  2887. compareArray(record.mods, mods)
  2888. )
  2889. });
  2890. });
  2891. };
  2892.  
  2893. // 对监听对应快捷键的回调函数进行处理
  2894. function eventHandler (event, handler, scope, element) {
  2895. if (handler.element !== element) {
  2896. return
  2897. }
  2898. let modifiersMatch;
  2899.  
  2900. // 看它是否在当前范围
  2901. if (handler.scope === scope || handler.scope === 'all') {
  2902. // 检查是否匹配修饰符(如果有返回true)
  2903. modifiersMatch = handler.mods.length > 0;
  2904.  
  2905. for (const y in _mods) {
  2906. if (Object.prototype.hasOwnProperty.call(_mods, y)) {
  2907. if (
  2908. (!_mods[y] && handler.mods.indexOf(+y) > -1) ||
  2909. (_mods[y] && handler.mods.indexOf(+y) === -1)
  2910. ) {
  2911. modifiersMatch = false;
  2912. }
  2913. }
  2914. }
  2915.  
  2916. // 调用处理程序,如果是修饰键不做处理
  2917. if (
  2918. (handler.mods.length === 0 &&
  2919. !_mods[16] &&
  2920. !_mods[18] &&
  2921. !_mods[17] &&
  2922. !_mods[91]) ||
  2923. modifiersMatch ||
  2924. handler.shortcut === '*'
  2925. ) {
  2926. if (handler.method(event, handler) === false) {
  2927. if (event.preventDefault) event.preventDefault();
  2928. else event.returnValue = false;
  2929. if (event.stopPropagation) event.stopPropagation();
  2930. if (event.cancelBubble) event.cancelBubble = true;
  2931. }
  2932. }
  2933. }
  2934. }
  2935.  
  2936. // 处理keydown事件
  2937. function dispatch (event, element) {
  2938. const asterisk = _handlers['*'];
  2939. let key = event.keyCode || event.which || event.charCode;
  2940.  
  2941. // 表单控件过滤 默认表单控件不触发快捷键
  2942. if (!hotkeys.filter.call(this, event)) return
  2943.  
  2944. // Gecko(Firefox)的command键值224,在Webkit(Chrome)中保持一致
  2945. // Webkit左右 command 键值不一样
  2946. if (key === 93 || key === 224) key = 91;
  2947.  
  2948. /**
  2949. * Collect bound keys
  2950. * If an Input Method Editor is processing key input and the event is keydown, return 229.
  2951. * https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229
  2952. * http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
  2953. */
  2954. if (_downKeys.indexOf(key) === -1 && key !== 229) _downKeys.push(key);
  2955. /**
  2956. * Jest test cases are required.
  2957. * ===============================
  2958. */
  2959. ['ctrlKey', 'altKey', 'shiftKey', 'metaKey'].forEach((keyName) => {
  2960. const keyNum = modifierMap[keyName];
  2961. if (event[keyName] && _downKeys.indexOf(keyNum) === -1) {
  2962. _downKeys.push(keyNum);
  2963. } else if (!event[keyName] && _downKeys.indexOf(keyNum) > -1) {
  2964. _downKeys.splice(_downKeys.indexOf(keyNum), 1);
  2965. } else if (keyName === 'metaKey' && event[keyName] && _downKeys.length === 3) {
  2966. /**
  2967. * Fix if Command is pressed:
  2968. * ===============================
  2969. */
  2970. if (!(event.ctrlKey || event.shiftKey || event.altKey)) {
  2971. _downKeys = _downKeys.slice(_downKeys.indexOf(keyNum));
  2972. }
  2973. }
  2974. });
  2975. /**
  2976. * -------------------------------
  2977. */
  2978.  
  2979. if (key in _mods) {
  2980. _mods[key] = true;
  2981.  
  2982. // 将特殊字符的key注册到 hotkeys 上
  2983. for (const k in _modifier) {
  2984. if (_modifier[k] === key) hotkeys[k] = true;
  2985. }
  2986.  
  2987. if (!asterisk) return
  2988. }
  2989.  
  2990. // 将 modifierMap 里面的修饰键绑定到 event 中
  2991. for (const e in _mods) {
  2992. if (Object.prototype.hasOwnProperty.call(_mods, e)) {
  2993. _mods[e] = event[modifierMap[e]];
  2994. }
  2995. }
  2996. /**
  2997. * https://github.com/jaywcjlove/hotkeys/pull/129
  2998. * This solves the issue in Firefox on Windows where hotkeys corresponding to special characters would not trigger.
  2999. * An example of this is ctrl+alt+m on a Swedish keyboard which is used to type μ.
  3000. * Browser support: https://caniuse.com/#feat=keyboardevent-getmodifierstate
  3001. */
  3002. if (event.getModifierState && (!(event.altKey && !event.ctrlKey) && event.getModifierState('AltGraph'))) {
  3003. if (_downKeys.indexOf(17) === -1) {
  3004. _downKeys.push(17);
  3005. }
  3006.  
  3007. if (_downKeys.indexOf(18) === -1) {
  3008. _downKeys.push(18);
  3009. }
  3010.  
  3011. _mods[17] = true;
  3012. _mods[18] = true;
  3013. }
  3014.  
  3015. // 获取范围 默认为 `all`
  3016. const scope = getScope();
  3017. // 对任何快捷键都需要做的处理
  3018. if (asterisk) {
  3019. for (let i = 0; i < asterisk.length; i++) {
  3020. if (
  3021. asterisk[i].scope === scope &&
  3022. ((event.type === 'keydown' && asterisk[i].keydown) ||
  3023. (event.type === 'keyup' && asterisk[i].keyup))
  3024. ) {
  3025. eventHandler(event, asterisk[i], scope, element);
  3026. }
  3027. }
  3028. }
  3029. // key 不在 _handlers 中返回
  3030. if (!(key in _handlers)) return
  3031.  
  3032. for (let i = 0; i < _handlers[key].length; i++) {
  3033. if (
  3034. (event.type === 'keydown' && _handlers[key][i].keydown) ||
  3035. (event.type === 'keyup' && _handlers[key][i].keyup)
  3036. ) {
  3037. if (_handlers[key][i].key) {
  3038. const record = _handlers[key][i];
  3039. const { splitKey } = record;
  3040. const keyShortcut = record.key.split(splitKey);
  3041. const _downKeysCurrent = []; // 记录当前按键键值
  3042. for (let a = 0; a < keyShortcut.length; a++) {
  3043. _downKeysCurrent.push(code(keyShortcut[a]));
  3044. }
  3045. if (_downKeysCurrent.sort().join('') === _downKeys.sort().join('')) {
  3046. // 找到处理内容
  3047. eventHandler(event, record, scope, element);
  3048. }
  3049. }
  3050. }
  3051. }
  3052. }
  3053.  
  3054. // 判断 element 是否已经绑定事件
  3055. function isElementBind (element) {
  3056. return elementHasBindEvent.indexOf(element) > -1
  3057. }
  3058.  
  3059. function hotkeys (key, option, method) {
  3060. _downKeys = [];
  3061. const keys = getKeys(key); // 需要处理的快捷键列表
  3062. let mods = [];
  3063. let scope = 'all'; // scope默认为all,所有范围都有效
  3064. let element = document; // 快捷键事件绑定节点
  3065. let i = 0;
  3066. let keyup = false;
  3067. let keydown = true;
  3068. let splitKey = '+';
  3069.  
  3070. // 对为设定范围的判断
  3071. if (method === undefined && typeof option === 'function') {
  3072. method = option;
  3073. }
  3074.  
  3075. if (Object.prototype.toString.call(option) === '[object Object]') {
  3076. if (option.scope) scope = option.scope; // eslint-disable-line
  3077. if (option.element) element = option.element; // eslint-disable-line
  3078. if (option.keyup) keyup = option.keyup; // eslint-disable-line
  3079. if (option.keydown !== undefined) keydown = option.keydown; // eslint-disable-line
  3080. if (typeof option.splitKey === 'string') splitKey = option.splitKey; // eslint-disable-line
  3081. }
  3082.  
  3083. if (typeof option === 'string') scope = option;
  3084.  
  3085. // 对于每个快捷键进行处理
  3086. for (; i < keys.length; i++) {
  3087. key = keys[i].split(splitKey); // 按键列表
  3088. mods = [];
  3089.  
  3090. // 如果是组合快捷键取得组合快捷键
  3091. if (key.length > 1) mods = getMods(_modifier, key);
  3092.  
  3093. // 将非修饰键转化为键码
  3094. key = key[key.length - 1];
  3095. key = key === '*' ? '*' : code(key); // *表示匹配所有快捷键
  3096.  
  3097. // 判断key是否在_handlers中,不在就赋一个空数组
  3098. if (!(key in _handlers)) _handlers[key] = [];
  3099. _handlers[key].push({
  3100. keyup,
  3101. keydown,
  3102. scope,
  3103. mods,
  3104. shortcut: keys[i],
  3105. method,
  3106. key: keys[i],
  3107. splitKey,
  3108. element
  3109. });
  3110. }
  3111. // 在全局document上设置快捷键
  3112. if (typeof element !== 'undefined' && !isElementBind(element) && window) {
  3113. elementHasBindEvent.push(element);
  3114. addEvent(element, 'keydown', (e) => {
  3115. dispatch(e, element);
  3116. });
  3117. if (!winListendFocus) {
  3118. winListendFocus = true;
  3119. addEvent(window, 'focus', () => {
  3120. _downKeys = [];
  3121. });
  3122. }
  3123. addEvent(element, 'keyup', (e) => {
  3124. dispatch(e, element);
  3125. clearModifier(e);
  3126. });
  3127. }
  3128. }
  3129.  
  3130. function trigger (shortcut, scope = 'all') {
  3131. Object.keys(_handlers).forEach((key) => {
  3132. const data = _handlers[key].find((item) => item.scope === scope && item.shortcut === shortcut);
  3133. if (data && data.method) {
  3134. data.method();
  3135. }
  3136. });
  3137. }
  3138.  
  3139. const _api = {
  3140. setScope,
  3141. getScope,
  3142. deleteScope,
  3143. getPressedKeyCodes,
  3144. isPressed,
  3145. filter,
  3146. trigger,
  3147. unbind,
  3148. keyMap: _keyMap,
  3149. modifier: _modifier,
  3150. modifierMap
  3151. };
  3152. for (const a in _api) {
  3153. if (Object.prototype.hasOwnProperty.call(_api, a)) {
  3154. hotkeys[a] = _api[a];
  3155. }
  3156. }
  3157.  
  3158. if (typeof window !== 'undefined') {
  3159. const _hotkeys = window.hotkeys;
  3160. hotkeys.noConflict = (deep) => {
  3161. if (deep && window.hotkeys === hotkeys) {
  3162. window.hotkeys = _hotkeys;
  3163. }
  3164. return hotkeys
  3165. };
  3166. window.hotkeys = hotkeys;
  3167. }
  3168.  
  3169. /*!
  3170. * @name hotKeyRegister.js
  3171. * @description vue-debug-helper的快捷键配置
  3172. * @version 0.0.1
  3173. * @author xxxily
  3174. * @date 2022/04/26 14:37
  3175. * @github https://github.com/xxxily
  3176. */
  3177.  
  3178. function hotKeyRegister () {
  3179. const hotKeyMap = {
  3180. 'shift+alt+i': functionCall.toggleInspect,
  3181. 'shift+alt+a,shift+alt+ctrl+a': functionCall.componentsSummaryStatisticsSort,
  3182. 'shift+alt+l': functionCall.componentsStatistics,
  3183. 'shift+alt+d': functionCall.destroyStatisticsSort,
  3184. 'shift+alt+c': functionCall.clearAll,
  3185. 'shift+alt+e': function (event, handler) {
  3186. if (helper.config.dd.enabled) {
  3187. functionCall.undd();
  3188. } else {
  3189. functionCall.dd();
  3190. }
  3191. }
  3192. };
  3193.  
  3194. Object.keys(hotKeyMap).forEach(key => {
  3195. hotkeys(key, hotKeyMap[key]);
  3196. });
  3197. }
  3198.  
  3199. /*!
  3200. * @name vueDetector.js
  3201. * @description 检测页面是否存在Vue对象
  3202. * @version 0.0.1
  3203. * @author xxxily
  3204. * @date 2022/04/27 11:43
  3205. * @github https://github.com/xxxily
  3206. */
  3207.  
  3208. function mutationDetector (callback, shadowRoot) {
  3209. const win = window;
  3210. const MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
  3211. const docRoot = shadowRoot || win.document.documentElement;
  3212. const maxDetectTries = 1500;
  3213. const timeout = 1000 * 10;
  3214. const startTime = Date.now();
  3215. let detectCount = 0;
  3216. let detectStatus = false;
  3217.  
  3218. if (!MutationObserver) {
  3219. debug.warn('MutationObserver is not supported in this browser');
  3220. return false
  3221. }
  3222.  
  3223. let mObserver = null;
  3224. const mObserverCallback = (mutationsList, observer) => {
  3225. if (detectStatus) {
  3226. return
  3227. }
  3228.  
  3229. /* 超时或检测次数过多,取消监听 */
  3230. if (Date.now() - startTime > timeout || detectCount > maxDetectTries) {
  3231. debug.warn('mutationDetector timeout or detectCount > maxDetectTries, stop detect');
  3232. if (mObserver && mObserver.disconnect) {
  3233. mObserver.disconnect();
  3234. mObserver = null;
  3235. }
  3236. }
  3237.  
  3238. for (let i = 0; i < mutationsList.length; i++) {
  3239. detectCount++;
  3240. const mutation = mutationsList[i];
  3241. if (mutation.target && mutation.target.__vue__) {
  3242. let Vue = Object.getPrototypeOf(mutation.target.__vue__).constructor;
  3243. while (Vue.super) {
  3244. Vue = Vue.super;
  3245. }
  3246.  
  3247. /* 检测成功后销毁观察对象 */
  3248. if (mObserver && mObserver.disconnect) {
  3249. mObserver.disconnect();
  3250. mObserver = null;
  3251. }
  3252.  
  3253. detectStatus = true;
  3254. callback && callback(Vue);
  3255. break
  3256. }
  3257. }
  3258. };
  3259.  
  3260. mObserver = new MutationObserver(mObserverCallback);
  3261. mObserver.observe(docRoot, {
  3262. attributes: true,
  3263. childList: true,
  3264. subtree: true
  3265. });
  3266. }
  3267.  
  3268. /**
  3269. * 检测页面是否存在Vue对象,方法参考:https://github.com/vuejs/devtools/blob/main/packages/shell-chrome/src/detector.js
  3270. * @param {window} win windwod对象
  3271. * @param {function} callback 检测到Vue对象后的回调函数
  3272. */
  3273. function vueDetect (win, callback) {
  3274. let delay = 1000;
  3275. let detectRemainingTries = 10;
  3276. let detectSuc = false;
  3277.  
  3278. // Method 1: MutationObserver detector
  3279. mutationDetector((Vue) => {
  3280. if (!detectSuc) {
  3281. debug.info(`------------- Vue mutation detected (${Vue.version}) -------------`);
  3282. detectSuc = true;
  3283. callback(Vue);
  3284. }
  3285. });
  3286.  
  3287. function runDetect () {
  3288. if (detectSuc) {
  3289. return false
  3290. }
  3291.  
  3292. // Method 2: Check Vue 3
  3293. const vueDetected = !!(win.__VUE__);
  3294. if (vueDetected) {
  3295. debug.info(`------------- Vue global detected (${win.__VUE__.version}) -------------`);
  3296. detectSuc = true;
  3297. callback(win.__VUE__);
  3298. return
  3299. }
  3300.  
  3301. // Method 3: Scan all elements inside document
  3302. const all = document.querySelectorAll('*');
  3303. let el;
  3304. for (let i = 0; i < all.length; i++) {
  3305. if (all[i].__vue__) {
  3306. el = all[i];
  3307. break
  3308. }
  3309. }
  3310. if (el) {
  3311. let Vue = Object.getPrototypeOf(el.__vue__).constructor;
  3312. while (Vue.super) {
  3313. Vue = Vue.super;
  3314. }
  3315. debug.info(`------------- Vue dom detected (${Vue.version}) -------------`);
  3316. detectSuc = true;
  3317. callback(Vue);
  3318. return
  3319. }
  3320.  
  3321. if (detectRemainingTries > 0) {
  3322. detectRemainingTries--;
  3323.  
  3324. if (detectRemainingTries >= 7) {
  3325. setTimeout(() => {
  3326. runDetect();
  3327. }, 40);
  3328. } else {
  3329. setTimeout(() => {
  3330. runDetect();
  3331. }, delay);
  3332. delay *= 5;
  3333. }
  3334. }
  3335. }
  3336.  
  3337. setTimeout(() => {
  3338. runDetect();
  3339. }, 40);
  3340. }
  3341.  
  3342. /*!
  3343. * @name vueConfig.js
  3344. * @description 对Vue的配置进行修改
  3345. * @version 0.0.1
  3346. * @author xxxily
  3347. * @date 2022/05/10 15:15
  3348. * @github https://github.com/xxxily
  3349. */
  3350.  
  3351. function vueConfigInit (Vue, config) {
  3352. if (Vue.config) {
  3353. /* 自动开启Vue的调试模式 */
  3354. if (config.devtools) {
  3355. Vue.config.debug = true;
  3356. Vue.config.devtools = true;
  3357. Vue.config.performance = true;
  3358.  
  3359. setTimeout(() => {
  3360. const devtools = getVueDevtools();
  3361. if (devtools) {
  3362. if (!devtools.enabled) {
  3363. devtools.emit('init', Vue);
  3364. debug.info('vue devtools init emit.');
  3365. }
  3366. } else {
  3367. // debug.info(
  3368. // 'Download the Vue Devtools extension for a better development experience:\n' +
  3369. // 'https://github.com/vuejs/vue-devtools'
  3370. // )
  3371. debug.info('vue devtools check failed.');
  3372. }
  3373. }, 200);
  3374. } else {
  3375. Vue.config.debug = false;
  3376. Vue.config.devtools = false;
  3377. Vue.config.performance = false;
  3378. }
  3379. } else {
  3380. debug.log('Vue.config is not defined');
  3381. }
  3382. }
  3383.  
  3384. /*!
  3385. * @name inspect.js
  3386. * @description vue组件审查模块
  3387. * @version 0.0.1
  3388. * @author xxxily
  3389. * @date 2022/05/10 18:25
  3390. * @github https://github.com/xxxily
  3391. */
  3392.  
  3393. const inspect = {
  3394. findComponentsByElement (el) {
  3395. let result = null;
  3396. let deep = 0;
  3397. let parent = el;
  3398. while (parent) {
  3399. if (deep >= 50) {
  3400. break
  3401. }
  3402.  
  3403. if (parent.__vue__) {
  3404. result = parent;
  3405. break
  3406. }
  3407.  
  3408. deep++;
  3409. parent = parent.parentNode;
  3410. }
  3411.  
  3412. return result
  3413. },
  3414.  
  3415. setOverlay (el) {
  3416. let overlay = document.querySelector('#vue-debugger-overlay');
  3417. if (!overlay) {
  3418. overlay = document.createElement('div');
  3419. overlay.id = 'vue-debugger-overlay';
  3420. overlay.style.position = 'fixed';
  3421. overlay.style.backgroundColor = 'rgba(65, 184, 131, 0.35)';
  3422. overlay.style.zIndex = 2147483647;
  3423. overlay.style.pointerEvents = 'none';
  3424. document.body.appendChild(overlay);
  3425. }
  3426.  
  3427. const rect = el.getBoundingClientRect();
  3428.  
  3429. overlay.style.width = rect.width + 'px';
  3430. overlay.style.height = rect.height + 'px';
  3431. overlay.style.left = rect.x + 'px';
  3432. overlay.style.top = rect.y + 'px';
  3433. overlay.style.display = 'block';
  3434.  
  3435. console.log(el, rect, el.__vue__._componentTag);
  3436. },
  3437.  
  3438. init (Vue) {
  3439. document.body.addEventListener('mouseover', (event) => {
  3440. if (!helper.config.inspect.enabled) {
  3441. return
  3442. }
  3443.  
  3444. const componentEl = inspect.findComponentsByElement(event.target);
  3445.  
  3446. if (componentEl) {
  3447. inspect.setOverlay(componentEl);
  3448. }
  3449. });
  3450. }
  3451. };
  3452.  
  3453. /**
  3454. * 判断是否处于Iframe中
  3455. * @returns {boolean}
  3456. */
  3457. function isInIframe () {
  3458. return window !== window.top
  3459. }
  3460.  
  3461. /**
  3462. * 由于tampermonkey对window对象进行了封装,我们实际访问到的window并非页面真实的window
  3463. * 这就导致了如果我们需要将某些对象挂载到页面的window进行调试的时候就无法挂载了
  3464. * 所以必须使用特殊手段才能访问到页面真实的window对象,于是就有了下面这个函数
  3465. * @returns {Promise<void>}
  3466. */
  3467. async function getPageWindow () {
  3468. return new Promise(function (resolve, reject) {
  3469. if (window._pageWindow) {
  3470. return resolve(window._pageWindow)
  3471. }
  3472.  
  3473. const listenEventList = ['load', 'mousemove', 'scroll', 'get-page-window-event'];
  3474.  
  3475. function getWin (event) {
  3476. window._pageWindow = this;
  3477. // debug.log('getPageWindow succeed', event)
  3478. listenEventList.forEach(eventType => {
  3479. window.removeEventListener(eventType, getWin, true);
  3480. });
  3481. resolve(window._pageWindow);
  3482. }
  3483.  
  3484. listenEventList.forEach(eventType => {
  3485. window.addEventListener(eventType, getWin, true);
  3486. });
  3487.  
  3488. /* 自行派发事件以便用最短的时候获得pageWindow对象 */
  3489. window.dispatchEvent(new window.Event('get-page-window-event'));
  3490. })
  3491. }
  3492. // getPageWindow()
  3493.  
  3494. /**
  3495. * 通过同步的方式获取pageWindow
  3496. * 注意同步获取的方式需要将脚本写入head,部分网站由于安全策略会导致写入失败,而无法正常获取
  3497. * @returns {*}
  3498. */
  3499. function getPageWindowSync () {
  3500. if (document._win_) return document._win_
  3501.  
  3502. const head = document.head || document.querySelector('head');
  3503. const script = document.createElement('script');
  3504. script.appendChild(document.createTextNode('document._win_ = window'));
  3505. head.appendChild(script);
  3506.  
  3507. return document._win_
  3508. }
  3509.  
  3510. let registerStatus = 'init';
  3511. window._debugMode_ = true;
  3512.  
  3513. function init (win) {
  3514. if (isInIframe()) {
  3515. debug.log('running in iframe, skip init', window.location.href);
  3516. return false
  3517. }
  3518.  
  3519. if (registerStatus === 'initing') {
  3520. return false
  3521. }
  3522.  
  3523. registerStatus = 'initing';
  3524.  
  3525. vueDetect(win, function (Vue) {
  3526. /* 挂载到window上,方便通过控制台调用调试 */
  3527. helper.Vue = Vue;
  3528. win.vueDebugHelper = helper;
  3529.  
  3530. /* 注册阻断Vue组件的功能 */
  3531. vueHooks.blockComponents(Vue, helper.config);
  3532.  
  3533. /* 注册打印全局组件注册信息的功能 */
  3534. if (helper.config.hackVueComponent) {
  3535. vueHooks.hackVueComponent(Vue);
  3536. }
  3537.  
  3538. /* 注册接口拦截功能和接近数据缓存功能 */
  3539. ajaxHooks.init(win);
  3540.  
  3541. /* 注册性能观察的功能 */
  3542. performanceObserver.init();
  3543.  
  3544. /* 对Vue相关配置进行初始化 */
  3545. vueConfigInit(Vue, helper.config);
  3546.  
  3547. mixinRegister(Vue);
  3548. menuRegister(Vue);
  3549. hotKeyRegister();
  3550.  
  3551. inspect.init(Vue);
  3552.  
  3553. debug.log('vue debug helper register success');
  3554. registerStatus = 'success';
  3555. });
  3556.  
  3557. setTimeout(() => {
  3558. if (registerStatus !== 'success') {
  3559. menuRegister(null);
  3560. debug.warn('vue debug helper register failed, please check if vue is loaded .', win.location.href);
  3561. }
  3562. }, 1000 * 10);
  3563. }
  3564.  
  3565. let win$1 = null;
  3566. try {
  3567. win$1 = getPageWindowSync();
  3568. if (win$1) {
  3569. init(win$1);
  3570. debug.log('getPageWindowSync success');
  3571. }
  3572. } catch (e) {
  3573. debug.error('getPageWindowSync failed', e);
  3574. }
  3575. (async function () {
  3576. if (!win$1) {
  3577. win$1 = await getPageWindow();
  3578. init(win$1);
  3579. }
  3580. })();