vue-debug-helper

Vue components debug helper

当前为 2022-05-11 提交的版本,查看 最新版本

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