vue-debug-helper

Vue components debug helper

目前为 2022-05-13 提交的版本。查看 最新版本

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