vue-debug-helper

Vue components debug helper

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

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