Vue調試分析助手

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

目前為 2022-05-05 提交的版本,檢視 最新版本

  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.8
  10. // @description Vue components debug helper
  11. // @description:en Vue components debug helper
  12. // @description:zh Vue组件探测、统计、分析辅助脚本
  13. // @description:zh-TW Vue組件探測、統計、分析輔助腳本
  14. // @description:ja Vueコンポーネントの検出、統計、分析補助スクリプト
  15. // @author ankvps
  16. // @icon https://cdn.jsdelivr.net/gh/xxxily/vue-debug-helper@main/logo.png
  17. // @match http://*/*
  18. // @match https://*/*
  19. // @grant unsafeWindow
  20. // @grant GM_addStyle
  21. // @grant GM_setValue
  22. // @grant GM_getValue
  23. // @grant GM_deleteValue
  24. // @grant GM_listValues
  25. // @grant GM_addValueChangeListener
  26. // @grant GM_removeValueChangeListener
  27. // @grant GM_registerMenuCommand
  28. // @grant GM_unregisterMenuCommand
  29. // @grant GM_getTab
  30. // @grant GM_saveTab
  31. // @grant GM_getTabs
  32. // @grant GM_openInTab
  33. // @grant GM_download
  34. // @grant GM_xmlhttpRequest
  35. // @run-at document-start
  36. // @connect 127.0.0.1
  37. // @license GPL
  38. // ==/UserScript==
  39. (function (w) { if (w) { w._vueDebugHelper_ = 'https://github.com/xxxily/vue-debug-helper'; } })();
  40.  
  41. class AssertionError extends Error {}
  42. AssertionError.prototype.name = 'AssertionError';
  43.  
  44. /**
  45. * Minimal assert function
  46. * @param {any} t Value to check if falsy
  47. * @param {string=} m Optional assertion error message
  48. * @throws {AssertionError}
  49. */
  50. function assert (t, m) {
  51. if (!t) {
  52. var err = new AssertionError(m);
  53. if (Error.captureStackTrace) Error.captureStackTrace(err, assert);
  54. throw err
  55. }
  56. }
  57.  
  58. /* eslint-env browser */
  59.  
  60. let ls;
  61. if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
  62. // A simple localStorage interface so that lsp works in SSR contexts. Not for persistant storage in node.
  63. const _nodeStorage = {};
  64. ls = {
  65. getItem (name) {
  66. return _nodeStorage[name] || null
  67. },
  68. setItem (name, value) {
  69. if (arguments.length < 2) throw new Error('Failed to execute \'setItem\' on \'Storage\': 2 arguments required, but only 1 present.')
  70. _nodeStorage[name] = (value).toString();
  71. },
  72. removeItem (name) {
  73. delete _nodeStorage[name];
  74. }
  75. };
  76. } else {
  77. ls = window.localStorage;
  78. }
  79.  
  80. var localStorageProxy = (name, opts = {}) => {
  81. assert(name, 'namepace required');
  82. const {
  83. defaults = {},
  84. lspReset = false,
  85. storageEventListener = true
  86. } = opts;
  87.  
  88. const state = new EventTarget();
  89. try {
  90. const restoredState = JSON.parse(ls.getItem(name)) || {};
  91. if (restoredState.lspReset !== lspReset) {
  92. ls.removeItem(name);
  93. for (const [k, v] of Object.entries({
  94. ...defaults
  95. })) {
  96. state[k] = v;
  97. }
  98. } else {
  99. for (const [k, v] of Object.entries({
  100. ...defaults,
  101. ...restoredState
  102. })) {
  103. state[k] = v;
  104. }
  105. }
  106. } catch (e) {
  107. console.error(e);
  108. ls.removeItem(name);
  109. }
  110.  
  111. state.lspReset = lspReset;
  112.  
  113. if (storageEventListener && typeof window !== 'undefined' && typeof window.addEventListener !== 'undefined') {
  114. state.addEventListener('storage', (ev) => {
  115. // Replace state with whats stored on localStorage... it is newer.
  116. for (const k of Object.keys(state)) {
  117. delete state[k];
  118. }
  119. const restoredState = JSON.parse(ls.getItem(name)) || {};
  120. for (const [k, v] of Object.entries({
  121. ...defaults,
  122. ...restoredState
  123. })) {
  124. state[k] = v;
  125. }
  126. opts.lspReset = restoredState.lspReset;
  127. state.dispatchEvent(new Event('update'));
  128. });
  129. }
  130.  
  131. function boundHandler (rootRef) {
  132. return {
  133. get (obj, prop) {
  134. if (typeof obj[prop] === 'object' && obj[prop] !== null) {
  135. return new Proxy(obj[prop], boundHandler(rootRef))
  136. } else if (typeof obj[prop] === 'function' && obj === rootRef && prop !== 'constructor') {
  137. // this returns bound EventTarget functions
  138. return obj[prop].bind(obj)
  139. } else {
  140. return obj[prop]
  141. }
  142. },
  143. set (obj, prop, value) {
  144. obj[prop] = value;
  145. try {
  146. ls.setItem(name, JSON.stringify(rootRef));
  147. rootRef.dispatchEvent(new Event('update'));
  148. return true
  149. } catch (e) {
  150. console.error(e);
  151. return false
  152. }
  153. }
  154. }
  155. }
  156.  
  157. return new Proxy(state, boundHandler(state))
  158. };
  159.  
  160. /**
  161. * 对特定数据结构的对象进行排序
  162. * @param {object} obj 一个对象,其结构应该类似于:{key1: [], key2: []}
  163. * @param {boolean} reverse -可选 是否反转、降序排列,默认为false
  164. * @param {object} opts -可选 指定数组的配置项,默认为{key: 'key', value: 'value'}
  165. * @param {object} opts.key -可选 指定对象键名的别名,默认为'key'
  166. * @param {object} opts.value -可选 指定对象值的别名,默认为'value'
  167. * @returns {array} 返回一个数组,其结构应该类似于:[{key: key1, value: []}, {key: key2, value: []}]
  168. */
  169. const objSort = (obj, reverse, opts = { key: 'key', value: 'value' }) => {
  170. const arr = [];
  171. for (const key in obj) {
  172. if (Object.prototype.hasOwnProperty.call(obj, key) && Array.isArray(obj[key])) {
  173. const tmpObj = {};
  174. tmpObj[opts.key] = key;
  175. tmpObj[opts.value] = obj[key];
  176. arr.push(tmpObj);
  177. }
  178. }
  179.  
  180. arr.sort((a, b) => {
  181. return a[opts.value].length - b[opts.value].length
  182. });
  183.  
  184. reverse && arr.reverse();
  185. return arr
  186. };
  187.  
  188. /**
  189. * 根据指定长度创建空白数据
  190. * @param {number} size -可选 指str的重复次数,默认为1024次,如果str为单个单字节字符,则意味着默认产生1Mb的空白数据
  191. * @param {string|number|any} str - 可选 指定数据的字符串,默认为'd'
  192. */
  193. function createEmptyData (count = 1024, str = 'd') {
  194. const arr = [];
  195. arr.length = count + 1;
  196. return arr.join(str)
  197. }
  198.  
  199. /**
  200. * 将字符串分隔的过滤器转换为数组形式的过滤器
  201. * @param {string|array} filter - 必选 字符串或数组,字符串支持使用 , |符号对多个项进行分隔
  202. * @returns {array}
  203. */
  204. function toArrFilters (filter) {
  205. filter = filter || [];
  206.  
  207. /* 如果是字符串,则支持通过, | 两个符号来指定多个组件名称的过滤器 */
  208. if (typeof filter === 'string') {
  209. /* 移除前后的, |分隔符,防止出现空字符的过滤规则 */
  210. filter.replace(/^(,|\|)/, '').replace(/(,|\|)$/, '');
  211.  
  212. if (/\|/.test(filter)) {
  213. filter = filter.split('|');
  214. } else {
  215. filter = filter.split(',');
  216. }
  217. }
  218.  
  219. filter = filter.map(item => item.trim());
  220.  
  221. return filter
  222. }
  223.  
  224. class Debug {
  225. constructor (msg, printTime = false) {
  226. const t = this;
  227. msg = msg || 'debug message:';
  228. t.log = t.createDebugMethod('log', null, msg);
  229. t.error = t.createDebugMethod('error', null, msg);
  230. t.info = t.createDebugMethod('info', null, msg);
  231. t.warn = t.createDebugMethod('warn', null, msg);
  232. }
  233.  
  234. create (msg) {
  235. return new Debug(msg)
  236. }
  237.  
  238. createDebugMethod (name, color, tipsMsg) {
  239. name = name || 'info';
  240.  
  241. const bgColorMap = {
  242. info: '#2274A5',
  243. log: '#95B46A',
  244. error: '#D33F49'
  245. };
  246.  
  247. const printTime = this.printTime;
  248.  
  249. return function () {
  250. if (!window._debugMode_) {
  251. return false
  252. }
  253.  
  254. const msg = tipsMsg || 'debug message:';
  255.  
  256. const arg = Array.from(arguments);
  257. arg.unshift(`color: white; background-color: ${color || bgColorMap[name] || '#95B46A'}`);
  258.  
  259. if (printTime) {
  260. const curTime = new Date();
  261. const H = curTime.getHours();
  262. const M = curTime.getMinutes();
  263. const S = curTime.getSeconds();
  264. arg.unshift(`%c [${H}:${M}:${S}] ${msg} `);
  265. } else {
  266. arg.unshift(`%c ${msg} `);
  267. }
  268.  
  269. window.console[name].apply(window.console, arg);
  270. }
  271. }
  272.  
  273. isDebugMode () {
  274. return Boolean(window._debugMode_)
  275. }
  276. }
  277.  
  278. var Debug$1 = new Debug();
  279.  
  280. var debug = Debug$1.create('vueDebugHelper:');
  281.  
  282. /**
  283. * 简单的i18n库
  284. */
  285.  
  286. class I18n {
  287. constructor (config) {
  288. this._languages = {};
  289. this._locale = this.getClientLang();
  290. this._defaultLanguage = '';
  291. this.init(config);
  292. }
  293.  
  294. init (config) {
  295. if (!config) return false
  296.  
  297. const t = this;
  298. t._locale = config.locale || t._locale;
  299. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  300. t._languages = config.languages || t._languages;
  301. t._defaultLanguage = config.defaultLanguage || t._defaultLanguage;
  302. }
  303.  
  304. use () {}
  305.  
  306. t (path) {
  307. const t = this;
  308. let result = t.getValByPath(t._languages[t._locale] || {}, path);
  309.  
  310. /* 版本回退 */
  311. if (!result && t._locale !== t._defaultLanguage) {
  312. result = t.getValByPath(t._languages[t._defaultLanguage] || {}, path);
  313. }
  314.  
  315. return result || ''
  316. }
  317.  
  318. /* 当前语言值 */
  319. language () {
  320. return this._locale
  321. }
  322.  
  323. languages () {
  324. return this._languages
  325. }
  326.  
  327. changeLanguage (locale) {
  328. if (this._languages[locale]) {
  329. this._languages = locale;
  330. return locale
  331. } else {
  332. return false
  333. }
  334. }
  335.  
  336. /**
  337. * 根据文本路径获取对象里面的值
  338. * @param obj {Object} -必选 要操作的对象
  339. * @param path {String} -必选 路径信息
  340. * @returns {*}
  341. */
  342. getValByPath (obj, path) {
  343. path = path || '';
  344. const pathArr = path.split('.');
  345. let result = obj;
  346.  
  347. /* 递归提取结果值 */
  348. for (let i = 0; i < pathArr.length; i++) {
  349. if (!result) break
  350. result = result[pathArr[i]];
  351. }
  352.  
  353. return result
  354. }
  355.  
  356. /* 获取客户端当前的语言环境 */
  357. getClientLang () {
  358. return navigator.languages ? navigator.languages[0] : navigator.language
  359. }
  360. }
  361.  
  362. var zhCN = {
  363. about: '关于',
  364. issues: '反馈',
  365. setting: '设置',
  366. hotkeys: '快捷键',
  367. donate: '赞赏',
  368. debugHelper: {
  369. viewVueDebugHelperObject: 'vueDebugHelper对象',
  370. componentsStatistics: '当前存活组件统计',
  371. destroyStatisticsSort: '已销毁组件统计',
  372. componentsSummaryStatisticsSort: '全部组件混合统计',
  373. getDestroyByDuration: '组件存活时间信息',
  374. clearAll: '清空统计信息',
  375. printLifeCycleInfo: '打印组件生命周期信息',
  376. notPrintLifeCycleInfo: '取消组件生命周期信息打印',
  377. printLifeCycleInfoPrompt: {
  378. lifecycleFilters: '输入要打印的生命周期名称,多个可用,或|分隔,不输入则默认打印created',
  379. componentFilters: '输入要打印的组件名称,多个可用,或|分隔,不输入则默认打印所有组件'
  380. },
  381. findComponents: '查找组件',
  382. findComponentsPrompt: {
  383. filters: '输入要查找的组件名称,或uid,多个可用,或|分隔'
  384. },
  385. findNotContainElementComponents: '查找不包含DOM对象的组件',
  386. blockComponents: '阻断组件的创建',
  387. blockComponentsPrompt: {
  388. filters: '输入要阻断的组件名称,多个可用,或|分隔,输入为空则取消阻断'
  389. },
  390. dd: '数据注入(dd)',
  391. undd: '取消数据注入(undd)',
  392. ddPrompt: {
  393. filter: '组件过滤器(如果为空,则对所有组件注入)',
  394. count: '指定注入数据的重复次数(默认1024)'
  395. },
  396. toggleHackVueComponent: '改写/还原Vue.component',
  397. hackVueComponent: {
  398. hack: '改写Vue.component',
  399. unhack: '还原Vue.component'
  400. },
  401. devtools: {
  402. enabled: '自动开启vue-devtools',
  403. disable: '禁止开启vue-devtools'
  404. }
  405. }
  406. };
  407.  
  408. var enUS = {
  409. about: 'about',
  410. issues: 'feedback',
  411. setting: 'settings',
  412. hotkeys: 'Shortcut keys',
  413. donate: 'donate',
  414. debugHelper: {
  415. viewVueDebugHelperObject: 'vueDebugHelper object',
  416. componentsStatistics: 'Current surviving component statistics',
  417. destroyStatisticsSort: 'Destroyed component statistics',
  418. componentsSummaryStatisticsSort: 'All components mixed statistics',
  419. getDestroyByDuration: 'Component survival time information',
  420. clearAll: 'Clear statistics',
  421. dd: 'Data injection (dd)',
  422. undd: 'Cancel data injection (undd)',
  423. ddPrompt: {
  424. filter: 'Component filter (if empty, inject all components)',
  425. count: 'Specify the number of repetitions of injected data (default 1024)'
  426. }
  427. }
  428. };
  429.  
  430. var zhTW = {
  431. about: '關於',
  432. issues: '反饋',
  433. setting: '設置',
  434. hotkeys: '快捷鍵',
  435. donate: '讚賞',
  436. debugHelper: {
  437. viewVueDebugHelperObject: 'vueDebugHelper對象',
  438. componentsStatistics: '當前存活組件統計',
  439. destroyStatisticsSort: '已銷毀組件統計',
  440. componentsSummaryStatisticsSort: '全部組件混合統計',
  441. getDestroyByDuration: '組件存活時間信息',
  442. clearAll: '清空統計信息',
  443. dd: '數據注入(dd)',
  444. undd: '取消數據注入(undd)',
  445. ddPrompt: {
  446. filter: '組件過濾器(如果為空,則對所有組件注入)',
  447. count: '指定注入數據的重複次數(默認1024)'
  448. }
  449. }
  450. };
  451.  
  452. const messages = {
  453. 'zh-CN': zhCN,
  454. zh: zhCN,
  455. 'zh-HK': zhTW,
  456. 'zh-TW': zhTW,
  457. 'en-US': enUS,
  458. en: enUS,
  459. };
  460.  
  461. /*!
  462. * @name i18n.js
  463. * @description vue-debug-helper的国际化配置
  464. * @version 0.0.1
  465. * @author xxxily
  466. * @date 2022/04/26 14:56
  467. * @github https://github.com/xxxily
  468. */
  469.  
  470. const i18n = new I18n({
  471. defaultLanguage: 'en',
  472. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  473. // locale: 'zh-TW',
  474. languages: messages
  475. });
  476.  
  477. window.vueDebugHelper = {
  478. /* 存储全部未被销毁的组件对象 */
  479. components: {},
  480. /* 存储全部创建过的组件的概要信息,即使销毁了概要信息依然存在 */
  481. componentsSummary: {},
  482. /* 基于componentsSummary的组件情况统计 */
  483. componentsSummaryStatistics: {},
  484. /* 已销毁的组件概要信息列表 */
  485. destroyList: [],
  486. /* 基于destroyList的组件情况统计 */
  487. destroyStatistics: {},
  488.  
  489. config: {
  490. /* 是否在控制台打印组件生命周期的相关信息 */
  491. lifecycle: {
  492. show: false,
  493. filters: ['created'],
  494. componentFilters: []
  495. },
  496.  
  497. /* 查找组件的过滤器配置 */
  498. findComponentsFilters: [],
  499.  
  500. /* 阻止组件创建的过滤器 */
  501. blockFilters: [],
  502.  
  503. devtools: true,
  504.  
  505. /* 改写Vue.component */
  506. hackVueComponent: false,
  507.  
  508. /* 给组件注入空白数据的配置信息 */
  509. dd: {
  510. enabled: false,
  511. filters: [],
  512. count: 1024
  513. }
  514. }
  515. };
  516.  
  517. const helper = window.vueDebugHelper;
  518.  
  519. /* 配置信息跟localStorage联动 */
  520. const state = localStorageProxy('vueDebugHelperConfig', {
  521. defaults: helper.config,
  522. lspReset: false,
  523. storageEventListener: false
  524. });
  525. helper.config = state;
  526.  
  527. const methods = {
  528. objSort,
  529. createEmptyData,
  530. /* 清除全部helper的全部记录数据,以便重新统计 */
  531. clearAll () {
  532. helper.components = {};
  533. helper.componentsSummary = {};
  534. helper.componentsSummaryStatistics = {};
  535. helper.destroyList = [];
  536. helper.destroyStatistics = {};
  537. },
  538.  
  539. /**
  540. * 对当前的helper.components进行统计与排序
  541. * 如果一直没运行过清理函数,则表示统计页面创建至今依然存活的组件对象
  542. * 运行过清理函数,则表示统计清理后新创建且至今依然存活的组件对象
  543. */
  544. componentsStatistics (reverse = true) {
  545. const tmpObj = {};
  546.  
  547. Object.keys(helper.components).forEach(key => {
  548. const component = helper.components[key];
  549.  
  550. tmpObj[component._componentName]
  551. ? tmpObj[component._componentName].push(component)
  552. : (tmpObj[component._componentName] = [component]);
  553. });
  554.  
  555. return objSort(tmpObj, reverse, {
  556. key: 'componentName',
  557. value: 'componentInstance'
  558. })
  559. },
  560.  
  561. /**
  562. * 对componentsSummaryStatistics进行排序输出,以便可以直观查看组件的创建情况
  563. */
  564. componentsSummaryStatisticsSort (reverse = true) {
  565. return objSort(helper.componentsSummaryStatistics, reverse, {
  566. key: 'componentName',
  567. value: 'componentsSummary'
  568. })
  569. },
  570.  
  571. /**
  572. * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况
  573. */
  574. destroyStatisticsSort (reverse = true) {
  575. return objSort(helper.destroyStatistics, reverse, {
  576. key: 'componentName',
  577. value: 'destroyList'
  578. })
  579. },
  580.  
  581. /**
  582. * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况
  583. */
  584. getDestroyByDuration (duration = 1000) {
  585. const destroyList = helper.destroyList;
  586. const destroyListLength = destroyList.length;
  587. const destroyListDuration = destroyList.map(item => item.duration).sort();
  588. const maxDuration = Math.max(...destroyListDuration);
  589. const minDuration = Math.min(...destroyListDuration);
  590. const avgDuration = destroyListDuration.reduce((a, b) => a + b, 0) / destroyListLength;
  591. const durationRange = maxDuration - minDuration;
  592. const durationRangePercent = (duration - minDuration) / durationRange;
  593.  
  594. return {
  595. destroyList,
  596. destroyListLength,
  597. destroyListDuration,
  598. maxDuration,
  599. minDuration,
  600. avgDuration,
  601. durationRange,
  602. durationRangePercent
  603. }
  604. },
  605.  
  606. /**
  607. * 获取组件的调用链信息
  608. */
  609. getComponentChain (component, moreDetail = false) {
  610. const result = [];
  611. let current = component;
  612. let deep = 0;
  613.  
  614. while (current && deep < 50) {
  615. deep++;
  616.  
  617. /**
  618. * 由于脚本注入的运行时间会比应用创建时间晚,所以会导致部分先创建的组件缺少相关信息
  619. * 这里尝试对部分信息进行修复,以便更好的查看组件的创建情况
  620. */
  621. if (!current._componentTag) {
  622. const tag = current.$vnode?.tag || current.$options?._componentTag || current._uid;
  623. current._componentTag = tag;
  624. current._componentName = isNaN(Number(tag)) ? tag.replace(/^vue-component-\d+-/, '') : 'anonymous-component';
  625. }
  626.  
  627. if (moreDetail) {
  628. result.push({
  629. tag: current._componentTag,
  630. name: current._componentName,
  631. componentsSummary: helper.componentsSummary[current._uid] || null
  632. });
  633. } else {
  634. result.push(current._componentName);
  635. }
  636.  
  637. current = current.$parent;
  638. }
  639.  
  640. if (moreDetail) {
  641. return result
  642. } else {
  643. return result.join(' -> ')
  644. }
  645. },
  646.  
  647. printLifeCycleInfo (lifecycleFilters, componentFilters) {
  648. lifecycleFilters = toArrFilters(lifecycleFilters);
  649. componentFilters = toArrFilters(componentFilters);
  650.  
  651. helper.config.lifecycle = {
  652. show: true,
  653. filters: lifecycleFilters,
  654. componentFilters: componentFilters
  655. };
  656. },
  657. notPrintLifeCycleInfo () {
  658. helper.config.lifecycle = {
  659. show: false,
  660. filters: ['created'],
  661. componentFilters: []
  662. };
  663. },
  664.  
  665. /**
  666. * 查找组件
  667. * @param {string|array} filters 组件名称或组件uid的过滤器,可以是字符串或者数组,如果是字符串多个过滤选可用,或|分隔
  668. * 如果过滤项是数字,则跟组件的id进行精确匹配,如果是字符串,则跟组件的tag信息进行模糊匹配
  669. * @returns {object} {components: [], componentNames: []}
  670. */
  671. findComponents (filters) {
  672. filters = toArrFilters(filters);
  673.  
  674. /* 对filters进行预处理,如果为纯数字则表示通过id查找组件 */
  675. filters = filters.map(filter => {
  676. if (/^\d+$/.test(filter)) {
  677. return Number(filter)
  678. } else {
  679. return filter
  680. }
  681. });
  682.  
  683. helper.config.findComponentsFilters = filters;
  684.  
  685. const result = {
  686. components: [],
  687. globalComponents: [],
  688. destroyedComponents: []
  689. };
  690.  
  691. /* 在helper.components里进行组件查找 */
  692. const components = helper.components;
  693. const keys = Object.keys(components);
  694. for (let i = 0; i < keys.length; i++) {
  695. const component = components[keys[i]];
  696.  
  697. for (let j = 0; j < filters.length; j++) {
  698. const filter = filters[j];
  699.  
  700. if (typeof filter === 'number' && component._uid === filter) {
  701. result.components.push(component);
  702. break
  703. } else if (typeof filter === 'string') {
  704. const { _componentTag, _componentName } = component;
  705.  
  706. if (String(_componentTag).includes(filter) || String(_componentName).includes(filter)) {
  707. result.components.push(component);
  708. break
  709. }
  710. }
  711. }
  712. }
  713.  
  714. /* 进行全局组件查找 */
  715. const globalComponentsKeys = Object.keys(helper.Vue.options.components);
  716. for (let i = 0; i < globalComponentsKeys.length; i++) {
  717. const key = String(globalComponentsKeys[i]);
  718. const component = helper.Vue.options.components[globalComponentsKeys[i]];
  719.  
  720. for (let j = 0; j < filters.length; j++) {
  721. const filter = filters[j];
  722. if (key.includes(filter)) {
  723. const tmpObj = {};
  724. tmpObj[key] = component;
  725. result.globalComponents.push(tmpObj);
  726. break
  727. }
  728. }
  729. }
  730.  
  731. helper.destroyList.forEach(item => {
  732. for (let j = 0; j < filters.length; j++) {
  733. const filter = filters[j];
  734.  
  735. if (typeof filter === 'number' && item.uid === filter) {
  736. result.destroyedComponents.push(item);
  737. break
  738. } else if (typeof filter === 'string') {
  739. if (String(item.tag).includes(filter) || String(item.name).includes(filter)) {
  740. result.destroyedComponents.push(item);
  741. break
  742. }
  743. }
  744. }
  745. });
  746.  
  747. return result
  748. },
  749.  
  750. findNotContainElementComponents () {
  751. const result = [];
  752. const keys = Object.keys(helper.components);
  753. keys.forEach(key => {
  754. const component = helper.components[key];
  755. const elStr = Object.prototype.toString.call(component.$el);
  756. if (!/(HTML|Comment)/.test(elStr)) {
  757. result.push(component);
  758. }
  759. });
  760.  
  761. return result
  762. },
  763.  
  764. /**
  765. * 阻止组件的创建
  766. * @param {string|array} filters 组件名称过滤器,可以是字符串或者数组,如果是字符串多个过滤选可用,或|分隔
  767. */
  768. blockComponents (filters) {
  769. filters = toArrFilters(filters);
  770. helper.config.blockFilters = filters;
  771. },
  772.  
  773. /**
  774. * 给指定组件注入大量空数据,以便观察组件的内存泄露情况
  775. * @param {Array|string} filter -必选 指定组件的名称,如果为空则表示注入所有组件
  776. * @param {number} count -可选 指定注入空数据的大小,单位Kb,默认为1024Kb,即1Mb
  777. * @returns
  778. */
  779. dd (filter, count = 1024) {
  780. filter = toArrFilters(filter);
  781. helper.config.dd = {
  782. enabled: true,
  783. filters: filter,
  784. count
  785. };
  786. },
  787. /* 禁止给组件注入空数据 */
  788. undd () {
  789. helper.config.dd = {
  790. enabled: false,
  791. filters: [],
  792. count: 1024
  793. };
  794.  
  795. /* 删除之前注入的数据 */
  796. Object.keys(helper.components).forEach(key => {
  797. const component = helper.components[key];
  798. component.$data && delete component.$data.__dd__;
  799. });
  800. },
  801.  
  802. toggleDevtools () {
  803. helper.config.devtools = !helper.config.devtools;
  804. },
  805.  
  806. /* 对Vue.component进行hack,以便观察什么时候进行了哪些全局组件的注册操作 */
  807. hackVueComponent (callback) {
  808. if (!helper.Vue || !(helper.Vue.component instanceof Function) || helper._vueComponentOrgin_) {
  809. debug.log(i18n.t('debugHelper.hackVueComponent.hack') + ' (failed)');
  810. return false
  811. }
  812.  
  813. const vueComponentOrgin = helper.Vue.component;
  814.  
  815. helper.Vue.component = function (name, opts) {
  816. if (callback instanceof Function) {
  817. callback.apply(helper.Vue, arguments);
  818. } else {
  819. let repeatTips = '';
  820. if (helper.Vue.options.components[name]) {
  821. repeatTips = '[REPEAT]';
  822. }
  823. debug.log('[Vue.component]' + repeatTips, name, opts);
  824. }
  825.  
  826. return vueComponentOrgin.apply(helper.Vue, arguments)
  827. };
  828.  
  829. helper._vueComponentOrgin_ = vueComponentOrgin;
  830. debug.log(i18n.t('debugHelper.hackVueComponent.hack') + ' (success)');
  831. return true
  832. },
  833.  
  834. unHackVueComponent () {
  835. if (helper._vueComponentOrgin_ && helper.Vue) {
  836. helper.Vue.component = helper._vueComponentOrgin_;
  837. debug.log(i18n.t('debugHelper.hackVueComponent.unHack') + ' (success)');
  838. return true
  839. }
  840. }
  841. };
  842.  
  843. helper.methods = methods;
  844.  
  845. /**
  846. * 打印生命周期信息
  847. * @param {Vue} vm vue组件实例
  848. * @param {string} lifeCycle vue生命周期名称
  849. * @returns
  850. */
  851. function printLifeCycle (vm, lifeCycle) {
  852. const lifeCycleConf = helper.config.lifecycle || { show: false, filters: ['created'], componentFilters: [] };
  853.  
  854. if (!vm || !lifeCycle || !lifeCycleConf.show) {
  855. return false
  856. }
  857.  
  858. const { _componentTag, _componentName, _componentChain, _createdHumanTime, _uid } = vm;
  859. const info = `[${lifeCycle}] tag: ${_componentTag}, uid: ${_uid}, createdTime: ${_createdHumanTime}, chain: ${_componentChain}`;
  860. const matchComponentFilters = lifeCycleConf.componentFilters.length === 0 || lifeCycleConf.componentFilters.includes(_componentName);
  861.  
  862. if (lifeCycleConf.filters.includes(lifeCycle) && matchComponentFilters) {
  863. debug.log(info);
  864. }
  865. }
  866.  
  867. function mixinRegister (Vue) {
  868. if (!Vue || !Vue.mixin) {
  869. debug.error('未检查到VUE对象,请检查是否引入了VUE,且将VUE对象挂载到全局变量window.Vue上');
  870. return false
  871. }
  872.  
  873. /* 自动开启Vue的调试模式 */
  874. if (Vue.config) {
  875. if (helper.config.devtools) {
  876. Vue.config.debug = true;
  877. Vue.config.devtools = true;
  878. Vue.config.performance = true;
  879. } else {
  880. Vue.config.debug = false;
  881. Vue.config.devtools = false;
  882. Vue.config.performance = false;
  883. }
  884. } else {
  885. debug.log('Vue.config is not defined');
  886. }
  887.  
  888. if (helper.config.hackVueComponent) {
  889. helper.methods.hackVueComponent();
  890. }
  891.  
  892. Vue.mixin({
  893. beforeCreate: function () {
  894. // const tag = this.$options?._componentTag || this.$vnode?.tag || this._uid
  895. const tag = this.$vnode?.tag || this.$options?._componentTag || this._uid;
  896. const chain = helper.methods.getComponentChain(this);
  897. this._componentTag = tag;
  898. this._componentChain = chain;
  899. this._componentName = isNaN(Number(tag)) ? tag.replace(/^vue-component-\d+-/, '') : 'anonymous-component';
  900. this._createdTime = Date.now();
  901.  
  902. /* 增加人类方便查看的时间信息 */
  903. const timeObj = new Date(this._createdTime);
  904. this._createdHumanTime = `${timeObj.getHours()}:${timeObj.getMinutes()}:${timeObj.getSeconds()}`;
  905.  
  906. /* 判断是否为函数式组件,函数式组件无状态 (没有响应式数据),也没有实例,也没生命周期概念 */
  907. if (this._componentName === 'anonymous-component' && !this.$parent && !this.$vnode) {
  908. this._componentName = 'functional-component';
  909. }
  910.  
  911. helper.components[this._uid] = this;
  912.  
  913. /**
  914. * 收集所有创建过的组件信息,此处只存储组件的基础信息,没销毁的组件会包含组件实例
  915. * 严禁对组件内其它对象进行引用,否则会导致组件实列无法被正常回收
  916. */
  917. const componentSummary = {
  918. uid: this._uid,
  919. name: this._componentName,
  920. tag: this._componentTag,
  921. createdTime: this._createdTime,
  922. createdHumanTime: this._createdHumanTime,
  923. // 0 表示还没被销毁
  924. destroyTime: 0,
  925. // 0 表示还没被销毁,duration可持续当当前查看时间
  926. duration: 0,
  927. component: this,
  928. chain
  929. };
  930. helper.componentsSummary[this._uid] = componentSummary;
  931.  
  932. /* 添加到componentsSummaryStatistics里,生成统计信息 */
  933. Array.isArray(helper.componentsSummaryStatistics[this._componentName])
  934. ? helper.componentsSummaryStatistics[this._componentName].push(componentSummary)
  935. : (helper.componentsSummaryStatistics[this._componentName] = [componentSummary]);
  936.  
  937. printLifeCycle(this, 'beforeCreate');
  938.  
  939. /* 使用$destroy阻断组件的创建 */
  940. if (helper.config.blockFilters && helper.config.blockFilters.length) {
  941. if (helper.config.blockFilters.includes(this._componentName)) {
  942. debug.log(`[block component]: name: ${this._componentName}, tag: ${this._componentTag}, uid: ${this._uid}`);
  943. this.$destroy();
  944. return false
  945. }
  946. }
  947. },
  948. created: function () {
  949. /* 增加空白数据,方便观察内存泄露情况 */
  950. if (helper.config.dd.enabled) {
  951. let needDd = false;
  952.  
  953. if (helper.config.dd.filters.length === 0) {
  954. needDd = true;
  955. } else {
  956. for (let index = 0; index < helper.config.dd.filters.length; index++) {
  957. const filter = helper.config.dd.filters[index];
  958. if (filter === this._componentName || String(this._componentName).endsWith(filter)) {
  959. needDd = true;
  960. break
  961. }
  962. }
  963. }
  964.  
  965. if (needDd) {
  966. const count = helper.config.dd.count * 1024;
  967. const componentInfo = `tag: ${this._componentTag}, uid: ${this._uid}, createdTime: ${this._createdHumanTime}`;
  968.  
  969. /* 此处必须使用JSON.stringify对产生的字符串进行消费,否则没法将内存占用上去 */
  970. this.$data.__dd__ = JSON.stringify(componentInfo + ' ' + helper.methods.createEmptyData(count, this._uid));
  971.  
  972. console.log(`[dd success] ${componentInfo} chain: ${this._componentChain}`);
  973. }
  974. }
  975.  
  976. printLifeCycle(this, 'created');
  977. },
  978. beforeMount: function () {
  979. printLifeCycle(this, 'beforeMount');
  980. },
  981. mounted: function () {
  982. printLifeCycle(this, 'mounted');
  983. },
  984. beforeUpdate: function () {
  985. printLifeCycle(this, 'beforeUpdate');
  986. },
  987. activated: function () {
  988. printLifeCycle(this, 'activated');
  989. },
  990. deactivated: function () {
  991. printLifeCycle(this, 'deactivated');
  992. },
  993. updated: function () {
  994. printLifeCycle(this, 'updated');
  995. },
  996. beforeDestroy: function () {
  997. printLifeCycle(this, 'beforeDestroy');
  998. },
  999. destroyed: function () {
  1000. printLifeCycle(this, 'destroyed');
  1001.  
  1002. if (this._componentTag) {
  1003. const uid = this._uid;
  1004. const name = this._componentName;
  1005. const destroyTime = Date.now();
  1006.  
  1007. /* helper里的componentSummary有可能通过调用clear函数而被清除掉,所以需进行判断再更新赋值 */
  1008. const componentSummary = helper.componentsSummary[this._uid];
  1009. if (componentSummary) {
  1010. /* 补充/更新组件信息 */
  1011. componentSummary.destroyTime = destroyTime;
  1012. componentSummary.duration = destroyTime - this._createdTime;
  1013.  
  1014. helper.destroyList.push(componentSummary);
  1015.  
  1016. /* 统计被销毁的组件信息 */
  1017. Array.isArray(helper.destroyStatistics[name])
  1018. ? helper.destroyStatistics[name].push(componentSummary)
  1019. : (helper.destroyStatistics[name] = [componentSummary]);
  1020.  
  1021. /* 删除已销毁的组件实例 */
  1022. delete componentSummary.component;
  1023. }
  1024.  
  1025. // 解除引用关系
  1026. delete this._componentTag;
  1027. delete this._componentChain;
  1028. delete this._componentName;
  1029. delete this._createdTime;
  1030. delete this._createdHumanTime;
  1031. delete this.$data.__dd__;
  1032. delete helper.components[uid];
  1033. } else {
  1034. console.error('存在未被正常标记的组件,请检查组件采集逻辑是否需完善', this);
  1035. }
  1036. }
  1037. });
  1038. }
  1039.  
  1040. /*!
  1041. * @name menuCommand.js
  1042. * @version 0.0.1
  1043. * @author Blaze
  1044. * @date 2019/9/21 14:22
  1045. */
  1046.  
  1047. const monkeyMenu = {
  1048. on (title, fn, accessKey) {
  1049. return window.GM_registerMenuCommand && window.GM_registerMenuCommand(title, fn, accessKey)
  1050. },
  1051. off (id) {
  1052. return window.GM_unregisterMenuCommand && window.GM_unregisterMenuCommand(id)
  1053. },
  1054. /* 切换类型的菜单功能 */
  1055. switch (title, fn, defVal) {
  1056. const t = this;
  1057. t.on(title, fn);
  1058. }
  1059. };
  1060.  
  1061. /*!
  1062. * @name functionCall.js
  1063. * @description 统一的提供外部功能调用管理模块
  1064. * @version 0.0.1
  1065. * @author xxxily
  1066. * @date 2022/04/27 17:42
  1067. * @github https://github.com/xxxily
  1068. */
  1069.  
  1070. const functionCall = {
  1071. viewVueDebugHelperObject () {
  1072. debug.log(i18n.t('debugHelper.viewVueDebugHelperObject'), helper);
  1073. },
  1074. componentsStatistics () {
  1075. const result = helper.methods.componentsStatistics();
  1076. let total = 0;
  1077.  
  1078. /* 提供友好的可视化展示方式 */
  1079. console.table && console.table(result.map(item => {
  1080. total += item.componentInstance.length;
  1081. return {
  1082. componentName: item.componentName,
  1083. count: item.componentInstance.length
  1084. }
  1085. }));
  1086.  
  1087. debug.log(`${i18n.t('debugHelper.componentsStatistics')} (total:${total})`, result);
  1088. },
  1089. destroyStatisticsSort () {
  1090. const result = helper.methods.destroyStatisticsSort();
  1091. let total = 0;
  1092.  
  1093. /* 提供友好的可视化展示方式 */
  1094. console.table && console.table(result.map(item => {
  1095. const durationList = item.destroyList.map(item => item.duration);
  1096. const maxDuration = Math.max(...durationList);
  1097. const minDuration = Math.min(...durationList);
  1098. const durationRange = maxDuration - minDuration;
  1099. total += item.destroyList.length;
  1100.  
  1101. return {
  1102. componentName: item.componentName,
  1103. count: item.destroyList.length,
  1104. avgDuration: durationList.reduce((pre, cur) => pre + cur, 0) / durationList.length,
  1105. maxDuration,
  1106. minDuration,
  1107. durationRange,
  1108. durationRangePercent: (1000 - minDuration) / durationRange
  1109. }
  1110. }));
  1111.  
  1112. debug.log(`${i18n.t('debugHelper.destroyStatisticsSort')} (total:${total})`, result);
  1113. },
  1114. componentsSummaryStatisticsSort () {
  1115. const result = helper.methods.componentsSummaryStatisticsSort();
  1116. let total = 0;
  1117.  
  1118. /* 提供友好的可视化展示方式 */
  1119. console.table && console.table(result.map(item => {
  1120. total += item.componentsSummary.length;
  1121. return {
  1122. componentName: item.componentName,
  1123. count: item.componentsSummary.length
  1124. }
  1125. }));
  1126.  
  1127. debug.log(`${i18n.t('debugHelper.componentsSummaryStatisticsSort')} (total:${total})`, result);
  1128. },
  1129. getDestroyByDuration () {
  1130. const destroyInfo = helper.methods.getDestroyByDuration();
  1131. console.table && console.table(destroyInfo.destroyList);
  1132. debug.log(i18n.t('debugHelper.getDestroyByDuration'), destroyInfo);
  1133. },
  1134. clearAll () {
  1135. helper.methods.clearAll();
  1136. debug.log(i18n.t('debugHelper.clearAll'));
  1137. },
  1138.  
  1139. printLifeCycleInfo () {
  1140. const lifecycleFilters = window.prompt(i18n.t('debugHelper.printLifeCycleInfoPrompt.lifecycleFilters'), helper.config.lifecycle.filters.join(','));
  1141. const componentFilters = window.prompt(i18n.t('debugHelper.printLifeCycleInfoPrompt.componentFilters'), helper.config.lifecycle.componentFilters.join(','));
  1142.  
  1143. if (lifecycleFilters !== null && componentFilters !== null) {
  1144. debug.log(i18n.t('debugHelper.printLifeCycleInfo'));
  1145. helper.methods.printLifeCycleInfo(lifecycleFilters, componentFilters);
  1146. }
  1147. },
  1148.  
  1149. notPrintLifeCycleInfo () {
  1150. debug.log(i18n.t('debugHelper.notPrintLifeCycleInfo'));
  1151. helper.methods.notPrintLifeCycleInfo();
  1152. },
  1153.  
  1154. findComponents () {
  1155. const filters = window.prompt(i18n.t('debugHelper.findComponentsPrompt.filters'), helper.config.findComponentsFilters.join(','));
  1156. if (filters !== null) {
  1157. debug.log(i18n.t('debugHelper.findComponents'), helper.methods.findComponents(filters));
  1158. }
  1159. },
  1160.  
  1161. findNotContainElementComponents () {
  1162. debug.log(i18n.t('debugHelper.findNotContainElementComponents'), helper.methods.findNotContainElementComponents());
  1163. },
  1164.  
  1165. blockComponents () {
  1166. const filters = window.prompt(i18n.t('debugHelper.blockComponentsPrompt.filters'), helper.config.blockFilters.join(','));
  1167. if (filters !== null) {
  1168. helper.methods.blockComponents(filters);
  1169. debug.log(i18n.t('debugHelper.blockComponents'), filters);
  1170. }
  1171. },
  1172.  
  1173. dd () {
  1174. const filter = window.prompt(i18n.t('debugHelper.ddPrompt.filter'), helper.config.dd.filters.join(','));
  1175. const count = window.prompt(i18n.t('debugHelper.ddPrompt.count'), helper.config.dd.count);
  1176.  
  1177. if (filter !== null && count !== null) {
  1178. debug.log(i18n.t('debugHelper.dd'));
  1179. helper.methods.dd(filter, Number(count));
  1180. }
  1181. },
  1182.  
  1183. undd () {
  1184. debug.log(i18n.t('debugHelper.undd'));
  1185. helper.methods.undd();
  1186. },
  1187.  
  1188. toggleHackVueComponent () {
  1189. helper.config.hackVueComponent ? helper.methods.unHackVueComponent() : helper.methods.hackVueComponent();
  1190. helper.config.hackVueComponent = !helper.config.hackVueComponent;
  1191. }
  1192.  
  1193. };
  1194.  
  1195. /*!
  1196. * @name menu.js
  1197. * @description vue-debug-helper的菜单配置
  1198. * @version 0.0.1
  1199. * @author xxxily
  1200. * @date 2022/04/25 22:28
  1201. * @github https://github.com/xxxily
  1202. */
  1203.  
  1204. function menuRegister (Vue) {
  1205. if (!Vue) {
  1206. monkeyMenu.on('not detected ' + i18n.t('issues'), () => {
  1207. window.GM_openInTab('https://github.com/xxxily/vue-debug-helper/issues', {
  1208. active: true,
  1209. insert: true,
  1210. setParent: true
  1211. });
  1212. });
  1213. return false
  1214. }
  1215.  
  1216. /* 批量注册菜单 */
  1217. Object.keys(functionCall).forEach(key => {
  1218. const text = i18n.t(`debugHelper.${key}`);
  1219. if (text && functionCall[key] instanceof Function) {
  1220. monkeyMenu.on(text, functionCall[key]);
  1221. }
  1222. });
  1223.  
  1224. /* 是否开启vue-devtools的菜单 */
  1225. const devtoolsText = helper.config.devtools ? i18n.t('debugHelper.devtools.disable') : i18n.t('debugHelper.devtools.enabled');
  1226. monkeyMenu.on(devtoolsText, helper.methods.toggleDevtools);
  1227.  
  1228. // monkeyMenu.on('i18n.t('setting')', () => {
  1229. // window.alert('功能开发中,敬请期待...')
  1230. // })
  1231.  
  1232. monkeyMenu.on(i18n.t('issues'), () => {
  1233. window.GM_openInTab('https://github.com/xxxily/vue-debug-helper/issues', {
  1234. active: true,
  1235. insert: true,
  1236. setParent: true
  1237. });
  1238. });
  1239.  
  1240. // monkeyMenu.on(i18n.t('donate'), () => {
  1241. // window.GM_openInTab('https://cdn.jsdelivr.net/gh/xxxily/vue-debug-helper@main/donate.png', {
  1242. // active: true,
  1243. // insert: true,
  1244. // setParent: true
  1245. // })
  1246. // })
  1247. }
  1248.  
  1249. const isff = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase().indexOf('firefox') > 0 : false;
  1250.  
  1251. // 绑定事件
  1252. function addEvent (object, event, method) {
  1253. if (object.addEventListener) {
  1254. object.addEventListener(event, method, false);
  1255. } else if (object.attachEvent) {
  1256. object.attachEvent(`on${event}`, () => { method(window.event); });
  1257. }
  1258. }
  1259.  
  1260. // 修饰键转换成对应的键码
  1261. function getMods (modifier, key) {
  1262. const mods = key.slice(0, key.length - 1);
  1263. for (let i = 0; i < mods.length; i++) mods[i] = modifier[mods[i].toLowerCase()];
  1264. return mods
  1265. }
  1266.  
  1267. // 处理传的key字符串转换成数组
  1268. function getKeys (key) {
  1269. if (typeof key !== 'string') key = '';
  1270. key = key.replace(/\s/g, ''); // 匹配任何空白字符,包括空格、制表符、换页符等等
  1271. const keys = key.split(','); // 同时设置多个快捷键,以','分割
  1272. let index = keys.lastIndexOf('');
  1273.  
  1274. // 快捷键可能包含',',需特殊处理
  1275. for (; index >= 0;) {
  1276. keys[index - 1] += ',';
  1277. keys.splice(index, 1);
  1278. index = keys.lastIndexOf('');
  1279. }
  1280.  
  1281. return keys
  1282. }
  1283.  
  1284. // 比较修饰键的数组
  1285. function compareArray (a1, a2) {
  1286. const arr1 = a1.length >= a2.length ? a1 : a2;
  1287. const arr2 = a1.length >= a2.length ? a2 : a1;
  1288. let isIndex = true;
  1289.  
  1290. for (let i = 0; i < arr1.length; i++) {
  1291. if (arr2.indexOf(arr1[i]) === -1) isIndex = false;
  1292. }
  1293. return isIndex
  1294. }
  1295.  
  1296. // Special Keys
  1297. const _keyMap = {
  1298. backspace: 8,
  1299. tab: 9,
  1300. clear: 12,
  1301. enter: 13,
  1302. return: 13,
  1303. esc: 27,
  1304. escape: 27,
  1305. space: 32,
  1306. left: 37,
  1307. up: 38,
  1308. right: 39,
  1309. down: 40,
  1310. del: 46,
  1311. delete: 46,
  1312. ins: 45,
  1313. insert: 45,
  1314. home: 36,
  1315. end: 35,
  1316. pageup: 33,
  1317. pagedown: 34,
  1318. capslock: 20,
  1319. num_0: 96,
  1320. num_1: 97,
  1321. num_2: 98,
  1322. num_3: 99,
  1323. num_4: 100,
  1324. num_5: 101,
  1325. num_6: 102,
  1326. num_7: 103,
  1327. num_8: 104,
  1328. num_9: 105,
  1329. num_multiply: 106,
  1330. num_add: 107,
  1331. num_enter: 108,
  1332. num_subtract: 109,
  1333. num_decimal: 110,
  1334. num_divide: 111,
  1335. '⇪': 20,
  1336. ',': 188,
  1337. '.': 190,
  1338. '/': 191,
  1339. '`': 192,
  1340. '-': isff ? 173 : 189,
  1341. '=': isff ? 61 : 187,
  1342. ';': isff ? 59 : 186,
  1343. '\'': 222,
  1344. '[': 219,
  1345. ']': 221,
  1346. '\\': 220
  1347. };
  1348.  
  1349. // Modifier Keys
  1350. const _modifier = {
  1351. // shiftKey
  1352. '⇧': 16,
  1353. shift: 16,
  1354. // altKey
  1355. '⌥': 18,
  1356. alt: 18,
  1357. option: 18,
  1358. // ctrlKey
  1359. '⌃': 17,
  1360. ctrl: 17,
  1361. control: 17,
  1362. // metaKey
  1363. '⌘': 91,
  1364. cmd: 91,
  1365. command: 91
  1366. };
  1367. const modifierMap = {
  1368. 16: 'shiftKey',
  1369. 18: 'altKey',
  1370. 17: 'ctrlKey',
  1371. 91: 'metaKey',
  1372.  
  1373. shiftKey: 16,
  1374. ctrlKey: 17,
  1375. altKey: 18,
  1376. metaKey: 91
  1377. };
  1378. const _mods = {
  1379. 16: false,
  1380. 18: false,
  1381. 17: false,
  1382. 91: false
  1383. };
  1384. const _handlers = {};
  1385.  
  1386. // F1~F12 special key
  1387. for (let k = 1; k < 20; k++) {
  1388. _keyMap[`f${k}`] = 111 + k;
  1389. }
  1390.  
  1391. // https://github.com/jaywcjlove/hotkeys
  1392.  
  1393. let _downKeys = []; // 记录摁下的绑定键
  1394. let winListendFocus = false; // window是否已经监听了focus事件
  1395. let _scope = 'all'; // 默认热键范围
  1396. const elementHasBindEvent = []; // 已绑定事件的节点记录
  1397.  
  1398. // 返回键码
  1399. const code = (x) => _keyMap[x.toLowerCase()] ||
  1400. _modifier[x.toLowerCase()] ||
  1401. x.toUpperCase().charCodeAt(0);
  1402.  
  1403. // 设置获取当前范围(默认为'所有')
  1404. function setScope (scope) {
  1405. _scope = scope || 'all';
  1406. }
  1407. // 获取当前范围
  1408. function getScope () {
  1409. return _scope || 'all'
  1410. }
  1411. // 获取摁下绑定键的键值
  1412. function getPressedKeyCodes () {
  1413. return _downKeys.slice(0)
  1414. }
  1415.  
  1416. // 表单控件控件判断 返回 Boolean
  1417. // hotkey is effective only when filter return true
  1418. function filter (event) {
  1419. const target = event.target || event.srcElement;
  1420. const { tagName } = target;
  1421. let flag = true;
  1422. // ignore: isContentEditable === 'true', <input> and <textarea> when readOnly state is false, <select>
  1423. if (
  1424. target.isContentEditable ||
  1425. ((tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') && !target.readOnly)
  1426. ) {
  1427. flag = false;
  1428. }
  1429. return flag
  1430. }
  1431.  
  1432. // 判断摁下的键是否为某个键,返回true或者false
  1433. function isPressed (keyCode) {
  1434. if (typeof keyCode === 'string') {
  1435. keyCode = code(keyCode); // 转换成键码
  1436. }
  1437. return _downKeys.indexOf(keyCode) !== -1
  1438. }
  1439.  
  1440. // 循环删除handlers中的所有 scope(范围)
  1441. function deleteScope (scope, newScope) {
  1442. let handlers;
  1443. let i;
  1444.  
  1445. // 没有指定scope,获取scope
  1446. if (!scope) scope = getScope();
  1447.  
  1448. for (const key in _handlers) {
  1449. if (Object.prototype.hasOwnProperty.call(_handlers, key)) {
  1450. handlers = _handlers[key];
  1451. for (i = 0; i < handlers.length;) {
  1452. if (handlers[i].scope === scope) handlers.splice(i, 1);
  1453. else i++;
  1454. }
  1455. }
  1456. }
  1457.  
  1458. // 如果scope被删除,将scope重置为all
  1459. if (getScope() === scope) setScope(newScope || 'all');
  1460. }
  1461.  
  1462. // 清除修饰键
  1463. function clearModifier (event) {
  1464. let key = event.keyCode || event.which || event.charCode;
  1465. const i = _downKeys.indexOf(key);
  1466.  
  1467. // 从列表中清除按压过的键
  1468. if (i >= 0) {
  1469. _downKeys.splice(i, 1);
  1470. }
  1471. // 特殊处理 cmmand 键,在 cmmand 组合快捷键 keyup 只执行一次的问题
  1472. if (event.key && event.key.toLowerCase() === 'meta') {
  1473. _downKeys.splice(0, _downKeys.length);
  1474. }
  1475.  
  1476. // 修饰键 shiftKey altKey ctrlKey (command||metaKey) 清除
  1477. if (key === 93 || key === 224) key = 91;
  1478. if (key in _mods) {
  1479. _mods[key] = false;
  1480.  
  1481. // 将修饰键重置为false
  1482. for (const k in _modifier) if (_modifier[k] === key) hotkeys[k] = false;
  1483. }
  1484. }
  1485.  
  1486. function unbind (keysInfo, ...args) {
  1487. // unbind(), unbind all keys
  1488. if (!keysInfo) {
  1489. Object.keys(_handlers).forEach((key) => delete _handlers[key]);
  1490. } else if (Array.isArray(keysInfo)) {
  1491. // support like : unbind([{key: 'ctrl+a', scope: 's1'}, {key: 'ctrl-a', scope: 's2', splitKey: '-'}])
  1492. keysInfo.forEach((info) => {
  1493. if (info.key) eachUnbind(info);
  1494. });
  1495. } else if (typeof keysInfo === 'object') {
  1496. // support like unbind({key: 'ctrl+a, ctrl+b', scope:'abc'})
  1497. if (keysInfo.key) eachUnbind(keysInfo);
  1498. } else if (typeof keysInfo === 'string') {
  1499. // support old method
  1500. // eslint-disable-line
  1501. let [scope, method] = args;
  1502. if (typeof scope === 'function') {
  1503. method = scope;
  1504. scope = '';
  1505. }
  1506. eachUnbind({
  1507. key: keysInfo,
  1508. scope,
  1509. method,
  1510. splitKey: '+'
  1511. });
  1512. }
  1513. }
  1514.  
  1515. // 解除绑定某个范围的快捷键
  1516. const eachUnbind = ({
  1517. key, scope, method, splitKey = '+'
  1518. }) => {
  1519. const multipleKeys = getKeys(key);
  1520. multipleKeys.forEach((originKey) => {
  1521. const unbindKeys = originKey.split(splitKey);
  1522. const len = unbindKeys.length;
  1523. const lastKey = unbindKeys[len - 1];
  1524. const keyCode = lastKey === '*' ? '*' : code(lastKey);
  1525. if (!_handlers[keyCode]) return
  1526. // 判断是否传入范围,没有就获取范围
  1527. if (!scope) scope = getScope();
  1528. const mods = len > 1 ? getMods(_modifier, unbindKeys) : [];
  1529. _handlers[keyCode] = _handlers[keyCode].filter((record) => {
  1530. // 通过函数判断,是否解除绑定,函数相等直接返回
  1531. const isMatchingMethod = method ? record.method === method : true;
  1532. return !(
  1533. isMatchingMethod &&
  1534. record.scope === scope &&
  1535. compareArray(record.mods, mods)
  1536. )
  1537. });
  1538. });
  1539. };
  1540.  
  1541. // 对监听对应快捷键的回调函数进行处理
  1542. function eventHandler (event, handler, scope, element) {
  1543. if (handler.element !== element) {
  1544. return
  1545. }
  1546. let modifiersMatch;
  1547.  
  1548. // 看它是否在当前范围
  1549. if (handler.scope === scope || handler.scope === 'all') {
  1550. // 检查是否匹配修饰符(如果有返回true)
  1551. modifiersMatch = handler.mods.length > 0;
  1552.  
  1553. for (const y in _mods) {
  1554. if (Object.prototype.hasOwnProperty.call(_mods, y)) {
  1555. if (
  1556. (!_mods[y] && handler.mods.indexOf(+y) > -1) ||
  1557. (_mods[y] && handler.mods.indexOf(+y) === -1)
  1558. ) {
  1559. modifiersMatch = false;
  1560. }
  1561. }
  1562. }
  1563.  
  1564. // 调用处理程序,如果是修饰键不做处理
  1565. if (
  1566. (handler.mods.length === 0 &&
  1567. !_mods[16] &&
  1568. !_mods[18] &&
  1569. !_mods[17] &&
  1570. !_mods[91]) ||
  1571. modifiersMatch ||
  1572. handler.shortcut === '*'
  1573. ) {
  1574. if (handler.method(event, handler) === false) {
  1575. if (event.preventDefault) event.preventDefault();
  1576. else event.returnValue = false;
  1577. if (event.stopPropagation) event.stopPropagation();
  1578. if (event.cancelBubble) event.cancelBubble = true;
  1579. }
  1580. }
  1581. }
  1582. }
  1583.  
  1584. // 处理keydown事件
  1585. function dispatch (event, element) {
  1586. const asterisk = _handlers['*'];
  1587. let key = event.keyCode || event.which || event.charCode;
  1588.  
  1589. // 表单控件过滤 默认表单控件不触发快捷键
  1590. if (!hotkeys.filter.call(this, event)) return
  1591.  
  1592. // Gecko(Firefox)的command键值224,在Webkit(Chrome)中保持一致
  1593. // Webkit左右 command 键值不一样
  1594. if (key === 93 || key === 224) key = 91;
  1595.  
  1596. /**
  1597. * Collect bound keys
  1598. * If an Input Method Editor is processing key input and the event is keydown, return 229.
  1599. * https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229
  1600. * http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
  1601. */
  1602. if (_downKeys.indexOf(key) === -1 && key !== 229) _downKeys.push(key);
  1603. /**
  1604. * Jest test cases are required.
  1605. * ===============================
  1606. */
  1607. ['ctrlKey', 'altKey', 'shiftKey', 'metaKey'].forEach((keyName) => {
  1608. const keyNum = modifierMap[keyName];
  1609. if (event[keyName] && _downKeys.indexOf(keyNum) === -1) {
  1610. _downKeys.push(keyNum);
  1611. } else if (!event[keyName] && _downKeys.indexOf(keyNum) > -1) {
  1612. _downKeys.splice(_downKeys.indexOf(keyNum), 1);
  1613. } else if (keyName === 'metaKey' && event[keyName] && _downKeys.length === 3) {
  1614. /**
  1615. * Fix if Command is pressed:
  1616. * ===============================
  1617. */
  1618. if (!(event.ctrlKey || event.shiftKey || event.altKey)) {
  1619. _downKeys = _downKeys.slice(_downKeys.indexOf(keyNum));
  1620. }
  1621. }
  1622. });
  1623. /**
  1624. * -------------------------------
  1625. */
  1626.  
  1627. if (key in _mods) {
  1628. _mods[key] = true;
  1629.  
  1630. // 将特殊字符的key注册到 hotkeys 上
  1631. for (const k in _modifier) {
  1632. if (_modifier[k] === key) hotkeys[k] = true;
  1633. }
  1634.  
  1635. if (!asterisk) return
  1636. }
  1637.  
  1638. // 将 modifierMap 里面的修饰键绑定到 event 中
  1639. for (const e in _mods) {
  1640. if (Object.prototype.hasOwnProperty.call(_mods, e)) {
  1641. _mods[e] = event[modifierMap[e]];
  1642. }
  1643. }
  1644. /**
  1645. * https://github.com/jaywcjlove/hotkeys/pull/129
  1646. * This solves the issue in Firefox on Windows where hotkeys corresponding to special characters would not trigger.
  1647. * An example of this is ctrl+alt+m on a Swedish keyboard which is used to type μ.
  1648. * Browser support: https://caniuse.com/#feat=keyboardevent-getmodifierstate
  1649. */
  1650. if (event.getModifierState && (!(event.altKey && !event.ctrlKey) && event.getModifierState('AltGraph'))) {
  1651. if (_downKeys.indexOf(17) === -1) {
  1652. _downKeys.push(17);
  1653. }
  1654.  
  1655. if (_downKeys.indexOf(18) === -1) {
  1656. _downKeys.push(18);
  1657. }
  1658.  
  1659. _mods[17] = true;
  1660. _mods[18] = true;
  1661. }
  1662.  
  1663. // 获取范围 默认为 `all`
  1664. const scope = getScope();
  1665. // 对任何快捷键都需要做的处理
  1666. if (asterisk) {
  1667. for (let i = 0; i < asterisk.length; i++) {
  1668. if (
  1669. asterisk[i].scope === scope &&
  1670. ((event.type === 'keydown' && asterisk[i].keydown) ||
  1671. (event.type === 'keyup' && asterisk[i].keyup))
  1672. ) {
  1673. eventHandler(event, asterisk[i], scope, element);
  1674. }
  1675. }
  1676. }
  1677. // key 不在 _handlers 中返回
  1678. if (!(key in _handlers)) return
  1679.  
  1680. for (let i = 0; i < _handlers[key].length; i++) {
  1681. if (
  1682. (event.type === 'keydown' && _handlers[key][i].keydown) ||
  1683. (event.type === 'keyup' && _handlers[key][i].keyup)
  1684. ) {
  1685. if (_handlers[key][i].key) {
  1686. const record = _handlers[key][i];
  1687. const { splitKey } = record;
  1688. const keyShortcut = record.key.split(splitKey);
  1689. const _downKeysCurrent = []; // 记录当前按键键值
  1690. for (let a = 0; a < keyShortcut.length; a++) {
  1691. _downKeysCurrent.push(code(keyShortcut[a]));
  1692. }
  1693. if (_downKeysCurrent.sort().join('') === _downKeys.sort().join('')) {
  1694. // 找到处理内容
  1695. eventHandler(event, record, scope, element);
  1696. }
  1697. }
  1698. }
  1699. }
  1700. }
  1701.  
  1702. // 判断 element 是否已经绑定事件
  1703. function isElementBind (element) {
  1704. return elementHasBindEvent.indexOf(element) > -1
  1705. }
  1706.  
  1707. function hotkeys (key, option, method) {
  1708. _downKeys = [];
  1709. const keys = getKeys(key); // 需要处理的快捷键列表
  1710. let mods = [];
  1711. let scope = 'all'; // scope默认为all,所有范围都有效
  1712. let element = document; // 快捷键事件绑定节点
  1713. let i = 0;
  1714. let keyup = false;
  1715. let keydown = true;
  1716. let splitKey = '+';
  1717.  
  1718. // 对为设定范围的判断
  1719. if (method === undefined && typeof option === 'function') {
  1720. method = option;
  1721. }
  1722.  
  1723. if (Object.prototype.toString.call(option) === '[object Object]') {
  1724. if (option.scope) scope = option.scope; // eslint-disable-line
  1725. if (option.element) element = option.element; // eslint-disable-line
  1726. if (option.keyup) keyup = option.keyup; // eslint-disable-line
  1727. if (option.keydown !== undefined) keydown = option.keydown; // eslint-disable-line
  1728. if (typeof option.splitKey === 'string') splitKey = option.splitKey; // eslint-disable-line
  1729. }
  1730.  
  1731. if (typeof option === 'string') scope = option;
  1732.  
  1733. // 对于每个快捷键进行处理
  1734. for (; i < keys.length; i++) {
  1735. key = keys[i].split(splitKey); // 按键列表
  1736. mods = [];
  1737.  
  1738. // 如果是组合快捷键取得组合快捷键
  1739. if (key.length > 1) mods = getMods(_modifier, key);
  1740.  
  1741. // 将非修饰键转化为键码
  1742. key = key[key.length - 1];
  1743. key = key === '*' ? '*' : code(key); // *表示匹配所有快捷键
  1744.  
  1745. // 判断key是否在_handlers中,不在就赋一个空数组
  1746. if (!(key in _handlers)) _handlers[key] = [];
  1747. _handlers[key].push({
  1748. keyup,
  1749. keydown,
  1750. scope,
  1751. mods,
  1752. shortcut: keys[i],
  1753. method,
  1754. key: keys[i],
  1755. splitKey,
  1756. element
  1757. });
  1758. }
  1759. // 在全局document上设置快捷键
  1760. if (typeof element !== 'undefined' && !isElementBind(element) && window) {
  1761. elementHasBindEvent.push(element);
  1762. addEvent(element, 'keydown', (e) => {
  1763. dispatch(e, element);
  1764. });
  1765. if (!winListendFocus) {
  1766. winListendFocus = true;
  1767. addEvent(window, 'focus', () => {
  1768. _downKeys = [];
  1769. });
  1770. }
  1771. addEvent(element, 'keyup', (e) => {
  1772. dispatch(e, element);
  1773. clearModifier(e);
  1774. });
  1775. }
  1776. }
  1777.  
  1778. function trigger (shortcut, scope = 'all') {
  1779. Object.keys(_handlers).forEach((key) => {
  1780. const data = _handlers[key].find((item) => item.scope === scope && item.shortcut === shortcut);
  1781. if (data && data.method) {
  1782. data.method();
  1783. }
  1784. });
  1785. }
  1786.  
  1787. const _api = {
  1788. setScope,
  1789. getScope,
  1790. deleteScope,
  1791. getPressedKeyCodes,
  1792. isPressed,
  1793. filter,
  1794. trigger,
  1795. unbind,
  1796. keyMap: _keyMap,
  1797. modifier: _modifier,
  1798. modifierMap
  1799. };
  1800. for (const a in _api) {
  1801. if (Object.prototype.hasOwnProperty.call(_api, a)) {
  1802. hotkeys[a] = _api[a];
  1803. }
  1804. }
  1805.  
  1806. if (typeof window !== 'undefined') {
  1807. const _hotkeys = window.hotkeys;
  1808. hotkeys.noConflict = (deep) => {
  1809. if (deep && window.hotkeys === hotkeys) {
  1810. window.hotkeys = _hotkeys;
  1811. }
  1812. return hotkeys
  1813. };
  1814. window.hotkeys = hotkeys;
  1815. }
  1816.  
  1817. /*!
  1818. * @name hotKeyRegister.js
  1819. * @description vue-debug-helper的快捷键配置
  1820. * @version 0.0.1
  1821. * @author xxxily
  1822. * @date 2022/04/26 14:37
  1823. * @github https://github.com/xxxily
  1824. */
  1825.  
  1826. function hotKeyRegister () {
  1827. const hotKeyMap = {
  1828. 'shift+alt+a,shift+alt+ctrl+a': functionCall.componentsSummaryStatisticsSort,
  1829. 'shift+alt+l': functionCall.componentsStatistics,
  1830. 'shift+alt+d': functionCall.destroyStatisticsSort,
  1831. 'shift+alt+c': functionCall.clearAll,
  1832. 'shift+alt+e': function (event, handler) {
  1833. if (helper.config.dd.enabled) {
  1834. functionCall.undd();
  1835. } else {
  1836. functionCall.dd();
  1837. }
  1838. }
  1839. };
  1840.  
  1841. Object.keys(hotKeyMap).forEach(key => {
  1842. hotkeys(key, hotKeyMap[key]);
  1843. });
  1844. }
  1845.  
  1846. /*!
  1847. * @name vueDetector.js
  1848. * @description 检测页面是否存在Vue对象
  1849. * @version 0.0.1
  1850. * @author xxxily
  1851. * @date 2022/04/27 11:43
  1852. * @github https://github.com/xxxily
  1853. */
  1854.  
  1855. function mutationDetector (callback, shadowRoot) {
  1856. const win = window;
  1857. const MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
  1858. const docRoot = shadowRoot || win.document.documentElement;
  1859. const maxDetectTries = 1500;
  1860. const timeout = 1000 * 10;
  1861. const startTime = Date.now();
  1862. let detectCount = 0;
  1863. let detectStatus = false;
  1864.  
  1865. if (!MutationObserver) {
  1866. debug.warn('MutationObserver is not supported in this browser');
  1867. return false
  1868. }
  1869.  
  1870. let mObserver = null;
  1871. const mObserverCallback = (mutationsList, observer) => {
  1872. if (detectStatus) {
  1873. return
  1874. }
  1875.  
  1876. /* 超时或检测次数过多,取消监听 */
  1877. if (Date.now() - startTime > timeout || detectCount > maxDetectTries) {
  1878. debug.warn('mutationDetector timeout or detectCount > maxDetectTries, stop detect');
  1879. if (mObserver && mObserver.disconnect) {
  1880. mObserver.disconnect();
  1881. mObserver = null;
  1882. }
  1883. }
  1884.  
  1885. for (let i = 0; i < mutationsList.length; i++) {
  1886. detectCount++;
  1887. const mutation = mutationsList[i];
  1888. if (mutation.target && mutation.target.__vue__) {
  1889. let Vue = Object.getPrototypeOf(mutation.target.__vue__).constructor;
  1890. while (Vue.super) {
  1891. Vue = Vue.super;
  1892. }
  1893.  
  1894. /* 检测成功后销毁观察对象 */
  1895. if (mObserver && mObserver.disconnect) {
  1896. mObserver.disconnect();
  1897. mObserver = null;
  1898. }
  1899.  
  1900. detectStatus = true;
  1901. callback && callback(Vue);
  1902. break
  1903. }
  1904. }
  1905. };
  1906.  
  1907. mObserver = new MutationObserver(mObserverCallback);
  1908. mObserver.observe(docRoot, {
  1909. attributes: true,
  1910. childList: true,
  1911. subtree: true
  1912. });
  1913. }
  1914.  
  1915. /**
  1916. * 检测页面是否存在Vue对象,方法参考:https://github.com/vuejs/devtools/blob/main/packages/shell-chrome/src/detector.js
  1917. * @param {window} win windwod对象
  1918. * @param {function} callback 检测到Vue对象后的回调函数
  1919. */
  1920. function vueDetect (win, callback) {
  1921. let delay = 1000;
  1922. let detectRemainingTries = 10;
  1923. let detectSuc = false;
  1924.  
  1925. // Method 1: MutationObserver detector
  1926. mutationDetector((Vue) => {
  1927. if (!detectSuc) {
  1928. debug.info(`------------- Vue mutation detected (${Vue.version}) -------------`);
  1929. detectSuc = true;
  1930. callback(Vue);
  1931. }
  1932. });
  1933.  
  1934. function runDetect () {
  1935. if (detectSuc) {
  1936. return false
  1937. }
  1938.  
  1939. // Method 2: Check Vue 3
  1940. const vueDetected = !!(win.__VUE__);
  1941. if (vueDetected) {
  1942. debug.info(`------------- Vue global detected (${win.__VUE__.version}) -------------`);
  1943. detectSuc = true;
  1944. callback(win.__VUE__);
  1945. return
  1946. }
  1947.  
  1948. // Method 3: Scan all elements inside document
  1949. const all = document.querySelectorAll('*');
  1950. let el;
  1951. for (let i = 0; i < all.length; i++) {
  1952. if (all[i].__vue__) {
  1953. el = all[i];
  1954. break
  1955. }
  1956. }
  1957. if (el) {
  1958. let Vue = Object.getPrototypeOf(el.__vue__).constructor;
  1959. while (Vue.super) {
  1960. Vue = Vue.super;
  1961. }
  1962. debug.info(`------------- Vue dom detected (${Vue.version}) -------------`);
  1963. detectSuc = true;
  1964. callback(Vue);
  1965. return
  1966. }
  1967.  
  1968. if (detectRemainingTries > 0) {
  1969. detectRemainingTries--;
  1970.  
  1971. if (detectRemainingTries >= 7) {
  1972. setTimeout(() => {
  1973. runDetect();
  1974. }, 40);
  1975. } else {
  1976. setTimeout(() => {
  1977. runDetect();
  1978. }, delay);
  1979. delay *= 5;
  1980. }
  1981. }
  1982. }
  1983.  
  1984. setTimeout(() => {
  1985. runDetect();
  1986. }, 40);
  1987. }
  1988.  
  1989. /**
  1990. * 判断是否处于Iframe中
  1991. * @returns {boolean}
  1992. */
  1993. function isInIframe () {
  1994. return window !== window.top
  1995. }
  1996.  
  1997. /**
  1998. * 由于tampermonkey对window对象进行了封装,我们实际访问到的window并非页面真实的window
  1999. * 这就导致了如果我们需要将某些对象挂载到页面的window进行调试的时候就无法挂载了
  2000. * 所以必须使用特殊手段才能访问到页面真实的window对象,于是就有了下面这个函数
  2001. * @returns {Promise<void>}
  2002. */
  2003. async function getPageWindow () {
  2004. return new Promise(function (resolve, reject) {
  2005. if (window._pageWindow) {
  2006. return resolve(window._pageWindow)
  2007. }
  2008.  
  2009. const listenEventList = ['load', 'mousemove', 'scroll', 'get-page-window-event'];
  2010.  
  2011. function getWin (event) {
  2012. window._pageWindow = this;
  2013. // debug.log('getPageWindow succeed', event)
  2014. listenEventList.forEach(eventType => {
  2015. window.removeEventListener(eventType, getWin, true);
  2016. });
  2017. resolve(window._pageWindow);
  2018. }
  2019.  
  2020. listenEventList.forEach(eventType => {
  2021. window.addEventListener(eventType, getWin, true);
  2022. });
  2023.  
  2024. /* 自行派发事件以便用最短的时候获得pageWindow对象 */
  2025. window.dispatchEvent(new window.Event('get-page-window-event'));
  2026. })
  2027. }
  2028. // getPageWindow()
  2029.  
  2030. /**
  2031. * 通过同步的方式获取pageWindow
  2032. * 注意同步获取的方式需要将脚本写入head,部分网站由于安全策略会导致写入失败,而无法正常获取
  2033. * @returns {*}
  2034. */
  2035. function getPageWindowSync () {
  2036. if (document._win_) return document._win_
  2037.  
  2038. const head = document.head || document.querySelector('head');
  2039. const script = document.createElement('script');
  2040. script.appendChild(document.createTextNode('document._win_ = window'));
  2041. head.appendChild(script);
  2042.  
  2043. return document._win_
  2044. }
  2045.  
  2046. let registerStatus = 'init';
  2047. window._debugMode_ = true;
  2048.  
  2049. function init (win) {
  2050. if (isInIframe()) {
  2051. debug.log('running in iframe, skip init', window.location.href);
  2052. return false
  2053. }
  2054.  
  2055. if (registerStatus === 'initing') {
  2056. return false
  2057. }
  2058.  
  2059. registerStatus = 'initing';
  2060.  
  2061. vueDetect(win, function (Vue) {
  2062. /* 挂载到window上,方便通过控制台调用调试 */
  2063. helper.Vue = Vue;
  2064. win.vueDebugHelper = helper;
  2065.  
  2066. mixinRegister(Vue);
  2067. menuRegister(Vue);
  2068. hotKeyRegister();
  2069.  
  2070. debug.log('vue debug helper register success');
  2071. registerStatus = 'success';
  2072. });
  2073.  
  2074. setTimeout(() => {
  2075. if (registerStatus !== 'success') {
  2076. menuRegister(null);
  2077. debug.warn('vue debug helper register failed, please check if vue is loaded .', win.location.href);
  2078. }
  2079. }, 1000 * 10);
  2080. }
  2081.  
  2082. let win = null;
  2083. try {
  2084. win = getPageWindowSync();
  2085. if (win) {
  2086. init(win);
  2087. debug.log('getPageWindowSync success');
  2088. }
  2089. } catch (e) {
  2090. debug.error('getPageWindowSync failed', e);
  2091. }
  2092. (async function () {
  2093. if (!win) {
  2094. win = await getPageWindow();
  2095. init(win);
  2096. }
  2097. })();