// ==UserScript== // @name vue-debug-helper // @name:en vue-debug-helper // @name:zh Vue调试分析助手 // @name:zh-TW Vue調試分析助手 // @name:ja Vueデバッグ分析アシスタント // @namespace https://github.com/xxxily/vue-debug-helper // @homepage https://github.com/xxxily/vue-debug-helper // @version 0.0.7 // @description Vue components debug helper // @description:en Vue components debug helper // @description:zh Vue组件探测、统计、分析辅助脚本 // @description:zh-TW Vue組件探測、統計、分析輔助腳本 // @description:ja Vueコンポーネントの検出、統計、分析補助スクリプト // @author ankvps // @icon https://cdn.jsdelivr.net/gh/xxxily/vue-debug-helper@main/logo.png // @match http://*/* // @match https://*/* // @grant unsafeWindow // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getTab // @grant GM_saveTab // @grant GM_getTabs // @grant GM_openInTab // @grant GM_download // @grant GM_xmlhttpRequest // @run-at document-start // @connect 127.0.0.1 // @license GPL // @downloadURL none // ==/UserScript== (function (w) { if (w) { w._vueDebugHelper_ = 'https://github.com/xxxily/vue-debug-helper'; } })(); class AssertionError extends Error {} AssertionError.prototype.name = 'AssertionError'; /** * Minimal assert function * @param {any} t Value to check if falsy * @param {string=} m Optional assertion error message * @throws {AssertionError} */ function assert (t, m) { if (!t) { var err = new AssertionError(m); if (Error.captureStackTrace) Error.captureStackTrace(err, assert); throw err } } /* eslint-env browser */ let ls; if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') { // A simple localStorage interface so that lsp works in SSR contexts. Not for persistant storage in node. const _nodeStorage = {}; ls = { getItem (name) { return _nodeStorage[name] || null }, setItem (name, value) { if (arguments.length < 2) throw new Error('Failed to execute \'setItem\' on \'Storage\': 2 arguments required, but only 1 present.') _nodeStorage[name] = (value).toString(); }, removeItem (name) { delete _nodeStorage[name]; } }; } else { ls = window.localStorage; } var localStorageProxy = (name, opts = {}) => { assert(name, 'namepace required'); const { defaults = {}, lspReset = false, storageEventListener = true } = opts; const state = new EventTarget(); try { const restoredState = JSON.parse(ls.getItem(name)) || {}; if (restoredState.lspReset !== lspReset) { ls.removeItem(name); for (const [k, v] of Object.entries({ ...defaults })) { state[k] = v; } } else { for (const [k, v] of Object.entries({ ...defaults, ...restoredState })) { state[k] = v; } } } catch (e) { console.error(e); ls.removeItem(name); } state.lspReset = lspReset; if (storageEventListener && typeof window !== 'undefined' && typeof window.addEventListener !== 'undefined') { state.addEventListener('storage', (ev) => { // Replace state with whats stored on localStorage... it is newer. for (const k of Object.keys(state)) { delete state[k]; } const restoredState = JSON.parse(ls.getItem(name)) || {}; for (const [k, v] of Object.entries({ ...defaults, ...restoredState })) { state[k] = v; } opts.lspReset = restoredState.lspReset; state.dispatchEvent(new Event('update')); }); } function boundHandler (rootRef) { return { get (obj, prop) { if (typeof obj[prop] === 'object' && obj[prop] !== null) { return new Proxy(obj[prop], boundHandler(rootRef)) } else if (typeof obj[prop] === 'function' && obj === rootRef && prop !== 'constructor') { // this returns bound EventTarget functions return obj[prop].bind(obj) } else { return obj[prop] } }, set (obj, prop, value) { obj[prop] = value; try { ls.setItem(name, JSON.stringify(rootRef)); rootRef.dispatchEvent(new Event('update')); return true } catch (e) { console.error(e); return false } } } } return new Proxy(state, boundHandler(state)) }; /** * 对特定数据结构的对象进行排序 * @param {object} obj 一个对象,其结构应该类似于:{key1: [], key2: []} * @param {boolean} reverse -可选 是否反转、降序排列,默认为false * @param {object} opts -可选 指定数组的配置项,默认为{key: 'key', value: 'value'} * @param {object} opts.key -可选 指定对象键名的别名,默认为'key' * @param {object} opts.value -可选 指定对象值的别名,默认为'value' * @returns {array} 返回一个数组,其结构应该类似于:[{key: key1, value: []}, {key: key2, value: []}] */ const objSort = (obj, reverse, opts = { key: 'key', value: 'value' }) => { const arr = []; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key) && Array.isArray(obj[key])) { const tmpObj = {}; tmpObj[opts.key] = key; tmpObj[opts.value] = obj[key]; arr.push(tmpObj); } } arr.sort((a, b) => { return a[opts.value].length - b[opts.value].length }); reverse && arr.reverse(); return arr }; /** * 根据指定长度创建空白数据 * @param {number} size -可选 指str的重复次数,默认为1024次,如果str为单个单字节字符,则意味着默认产生1Mb的空白数据 * @param {string|number|any} str - 可选 指定数据的字符串,默认为'd' */ function createEmptyData (count = 1024, str = 'd') { const arr = []; arr.length = count + 1; return arr.join(str) } /** * 将字符串分隔的过滤器转换为数组形式的过滤器 * @param {string|array} filter - 必选 字符串或数组,字符串支持使用 , |符号对多个项进行分隔 * @returns {array} */ function toArrFilters (filter) { filter = filter || []; /* 如果是字符串,则支持通过, | 两个符号来指定多个组件名称的过滤器 */ if (typeof filter === 'string') { /* 移除前后的, |分隔符,防止出现空字符的过滤规则 */ filter.replace(/^(,|\|)/, '').replace(/(,|\|)$/, ''); if (/\|/.test(filter)) { filter = filter.split('|'); } else { filter = filter.split(','); } } filter = filter.map(item => item.trim()); return filter } window.vueDebugHelper = { /* 存储全部未被销毁的组件对象 */ components: {}, /* 存储全部创建过的组件的概要信息,即使销毁了概要信息依然存在 */ componentsSummary: {}, /* 基于componentsSummary的组件情况统计 */ componentsSummaryStatistics: {}, /* 已销毁的组件概要信息列表 */ destroyList: [], /* 基于destroyList的组件情况统计 */ destroyStatistics: {}, config: { /* 是否在控制台打印组件生命周期的相关信息 */ lifecycle: { show: false, filters: ['created'], componentFilters: [] }, /* 查找组件的过滤器配置 */ findComponentsFilters: [], /* 阻止组件创建的过滤器 */ blockFilters: [], devtools: true, /* 给组件注入空白数据的配置信息 */ dd: { enabled: false, filters: [], count: 1024 } } }; const helper = window.vueDebugHelper; /* 配置信息跟localStorage联动 */ const state = localStorageProxy('vueDebugHelperConfig', { defaults: helper.config, lspReset: false, storageEventListener: false }); helper.config = state; const methods = { objSort, createEmptyData, /* 清除全部helper的全部记录数据,以便重新统计 */ clearAll () { helper.components = {}; helper.componentsSummary = {}; helper.componentsSummaryStatistics = {}; helper.destroyList = []; helper.destroyStatistics = {}; }, /** * 对当前的helper.components进行统计与排序 * 如果一直没运行过清理函数,则表示统计页面创建至今依然存活的组件对象 * 运行过清理函数,则表示统计清理后新创建且至今依然存活的组件对象 */ componentsStatistics (reverse = true) { const tmpObj = {}; Object.keys(helper.components).forEach(key => { const component = helper.components[key]; tmpObj[component._componentName] ? tmpObj[component._componentName].push(component) : (tmpObj[component._componentName] = [component]); }); return objSort(tmpObj, reverse, { key: 'componentName', value: 'componentInstance' }) }, /** * 对componentsSummaryStatistics进行排序输出,以便可以直观查看组件的创建情况 */ componentsSummaryStatisticsSort (reverse = true) { return objSort(helper.componentsSummaryStatistics, reverse, { key: 'componentName', value: 'componentsSummary' }) }, /** * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况 */ destroyStatisticsSort (reverse = true) { return objSort(helper.destroyStatistics, reverse, { key: 'componentName', value: 'destroyList' }) }, /** * 对destroyList进行排序输出,以便可以直观查看组件的销毁情况 */ getDestroyByDuration (duration = 1000) { const destroyList = helper.destroyList; const destroyListLength = destroyList.length; const destroyListDuration = destroyList.map(item => item.duration).sort(); const maxDuration = Math.max(...destroyListDuration); const minDuration = Math.min(...destroyListDuration); const avgDuration = destroyListDuration.reduce((a, b) => a + b, 0) / destroyListLength; const durationRange = maxDuration - minDuration; const durationRangePercent = (duration - minDuration) / durationRange; return { destroyList, destroyListLength, destroyListDuration, maxDuration, minDuration, avgDuration, durationRange, durationRangePercent } }, /** * 获取组件的调用链信息 */ getComponentChain (component, moreDetail = false) { const result = []; let current = component; let deep = 0; while (current && deep < 50) { deep++; /** * 由于脚本注入的运行时间会比应用创建时间晚,所以会导致部分先创建的组件缺少相关信息 * 这里尝试对部分信息进行修复,以便更好的查看组件的创建情况 */ if (!current._componentTag) { const tag = current.$vnode?.tag || current.$options?._componentTag || current._uid; current._componentTag = tag; current._componentName = isNaN(Number(tag)) ? tag.replace(/^vue-component-\d+-/, '') : 'anonymous-component'; } if (moreDetail) { result.push({ tag: current._componentTag, name: current._componentName, componentsSummary: helper.componentsSummary[current._uid] || null }); } else { result.push(current._componentName); } current = current.$parent; } if (moreDetail) { return result } else { return result.join(' -> ') } }, printLifeCycleInfo (lifecycleFilters, componentFilters) { lifecycleFilters = toArrFilters(lifecycleFilters); componentFilters = toArrFilters(componentFilters); helper.config.lifecycle = { show: true, filters: lifecycleFilters, componentFilters: componentFilters }; }, notPrintLifeCycleInfo () { helper.config.lifecycle = { show: false, filters: ['created'], componentFilters: [] }; }, /** * 查找组件 * @param {string|array} filters 组件名称或组件uid的过滤器,可以是字符串或者数组,如果是字符串多个过滤选可用,或|分隔 * 如果过滤项是数字,则跟组件的id进行精确匹配,如果是字符串,则跟组件的tag信息进行模糊匹配 * @returns {object} {components: [], componentNames: []} */ findComponents (filters) { filters = toArrFilters(filters); /* 对filters进行预处理,如果为纯数字则表示通过id查找组件 */ filters = filters.map(filter => { if (/^\d+$/.test(filter)) { return Number(filter) } else { return filter } }); helper.config.findComponentsFilters = filters; const result = { components: [], destroyedComponents: [] }; const components = helper.components; const keys = Object.keys(components); for (let i = 0; i < keys.length; i++) { const component = components[keys[i]]; for (let j = 0; j < filters.length; j++) { const filter = filters[j]; if (typeof filter === 'number' && component._uid === filter) { result.components.push(component); break } else if (typeof filter === 'string') { const { _componentTag, _componentName } = component; if (String(_componentTag).includes(filter) || String(_componentName).includes(filter)) { result.components.push(component); break } } } } helper.destroyList.forEach(item => { for (let j = 0; j < filters.length; j++) { const filter = filters[j]; if (typeof filter === 'number' && item.uid === filter) { result.destroyedComponents.push(item); break } else if (typeof filter === 'string') { if (String(item.tag).includes(filter) || String(item.name).includes(filter)) { result.destroyedComponents.push(item); break } } } }); return result }, findNotContainElementComponents () { const result = []; const keys = Object.keys(helper.components); keys.forEach(key => { const component = helper.components[key]; const elStr = Object.prototype.toString.call(component.$el); if (!/(HTML|Comment)/.test(elStr)) { result.push(component); } }); return result }, /** * 阻止组件的创建 * @param {string|array} filters 组件名称过滤器,可以是字符串或者数组,如果是字符串多个过滤选可用,或|分隔 */ blockComponents (filters) { filters = toArrFilters(filters); helper.config.blockFilters = filters; }, /** * 给指定组件注入大量空数据,以便观察组件的内存泄露情况 * @param {Array|string} filter -必选 指定组件的名称,如果为空则表示注入所有组件 * @param {number} count -可选 指定注入空数据的大小,单位Kb,默认为1024Kb,即1Mb * @returns */ dd (filter, count = 1024) { filter = toArrFilters(filter); helper.config.dd = { enabled: true, filters: filter, count }; }, /* 禁止给组件注入空数据 */ undd () { helper.config.dd = { enabled: false, filters: [], count: 1024 }; /* 删除之前注入的数据 */ Object.keys(helper.components).forEach(key => { const component = helper.components[key]; component.$data && delete component.$data.__dd__; }); }, toggleDevtools () { helper.config.devtools = !helper.config.devtools; } }; helper.methods = methods; class Debug { constructor (msg, printTime = false) { const t = this; msg = msg || 'debug message:'; t.log = t.createDebugMethod('log', null, msg); t.error = t.createDebugMethod('error', null, msg); t.info = t.createDebugMethod('info', null, msg); t.warn = t.createDebugMethod('warn', null, msg); } create (msg) { return new Debug(msg) } createDebugMethod (name, color, tipsMsg) { name = name || 'info'; const bgColorMap = { info: '#2274A5', log: '#95B46A', error: '#D33F49' }; const printTime = this.printTime; return function () { if (!window._debugMode_) { return false } const msg = tipsMsg || 'debug message:'; const arg = Array.from(arguments); arg.unshift(`color: white; background-color: ${color || bgColorMap[name] || '#95B46A'}`); if (printTime) { const curTime = new Date(); const H = curTime.getHours(); const M = curTime.getMinutes(); const S = curTime.getSeconds(); arg.unshift(`%c [${H}:${M}:${S}] ${msg} `); } else { arg.unshift(`%c ${msg} `); } window.console[name].apply(window.console, arg); } } isDebugMode () { return Boolean(window._debugMode_) } } var Debug$1 = new Debug(); var debug = Debug$1.create('vueDebugHelper:'); /** * 打印生命周期信息 * @param {Vue} vm vue组件实例 * @param {string} lifeCycle vue生命周期名称 * @returns */ function printLifeCycle (vm, lifeCycle) { const lifeCycleConf = helper.config.lifecycle || { show: false, filters: ['created'], componentFilters: [] }; if (!vm || !lifeCycle || !lifeCycleConf.show) { return false } const { _componentTag, _componentName, _componentChain, _createdHumanTime, _uid } = vm; const info = `[${lifeCycle}] tag: ${_componentTag}, uid: ${_uid}, createdTime: ${_createdHumanTime}, chain: ${_componentChain}`; const matchComponentFilters = lifeCycleConf.componentFilters.length === 0 || lifeCycleConf.componentFilters.includes(_componentName); if (lifeCycleConf.filters.includes(lifeCycle) && matchComponentFilters) { debug.log(info); } } function mixinRegister (Vue) { if (!Vue || !Vue.mixin) { debug.error('未检查到VUE对象,请检查是否引入了VUE,且将VUE对象挂载到全局变量window.Vue上'); return false } /* 自动开启Vue的调试模式 */ if (Vue.config) { if (helper.config.devtools) { Vue.config.debug = true; Vue.config.devtools = true; Vue.config.performance = true; } else { Vue.config.debug = false; Vue.config.devtools = false; Vue.config.performance = false; } } else { debug.log('Vue.config is not defined'); } Vue.mixin({ beforeCreate: function () { // const tag = this.$options?._componentTag || this.$vnode?.tag || this._uid const tag = this.$vnode?.tag || this.$options?._componentTag || this._uid; const chain = helper.methods.getComponentChain(this); this._componentTag = tag; this._componentChain = chain; this._componentName = isNaN(Number(tag)) ? tag.replace(/^vue-component-\d+-/, '') : 'anonymous-component'; this._createdTime = Date.now(); /* 增加人类方便查看的时间信息 */ const timeObj = new Date(this._createdTime); this._createdHumanTime = `${timeObj.getHours()}:${timeObj.getMinutes()}:${timeObj.getSeconds()}`; /* 判断是否为函数式组件,函数式组件无状态 (没有响应式数据),也没有实例,也没生命周期概念 */ if (this._componentName === 'anonymous-component' && !this.$parent && !this.$vnode) { this._componentName = 'functional-component'; } helper.components[this._uid] = this; /** * 收集所有创建过的组件信息,此处只存储组件的基础信息,没销毁的组件会包含组件实例 * 严禁对组件内其它对象进行引用,否则会导致组件实列无法被正常回收 */ const componentSummary = { uid: this._uid, name: this._componentName, tag: this._componentTag, createdTime: this._createdTime, createdHumanTime: this._createdHumanTime, // 0 表示还没被销毁 destroyTime: 0, // 0 表示还没被销毁,duration可持续当当前查看时间 duration: 0, component: this, chain }; helper.componentsSummary[this._uid] = componentSummary; /* 添加到componentsSummaryStatistics里,生成统计信息 */ Array.isArray(helper.componentsSummaryStatistics[this._componentName]) ? helper.componentsSummaryStatistics[this._componentName].push(componentSummary) : (helper.componentsSummaryStatistics[this._componentName] = [componentSummary]); printLifeCycle(this, 'beforeCreate'); /* 使用$destroy阻断组件的创建 */ if (helper.config.blockFilters && helper.config.blockFilters.length) { if (helper.config.blockFilters.includes(this._componentName)) { debug.log(`[block component]: name: ${this._componentName}, tag: ${this._componentTag}, uid: ${this._uid}`); this.$destroy(); return false } } }, created: function () { /* 增加空白数据,方便观察内存泄露情况 */ if (helper.config.dd.enabled) { let needDd = false; if (helper.config.dd.filters.length === 0) { needDd = true; } else { for (let index = 0; index < helper.config.dd.filters.length; index++) { const filter = helper.config.dd.filters[index]; if (filter === this._componentName || String(this._componentName).endsWith(filter)) { needDd = true; break } } } if (needDd) { const count = helper.config.dd.count * 1024; const componentInfo = `tag: ${this._componentTag}, uid: ${this._uid}, createdTime: ${this._createdHumanTime}`; /* 此处必须使用JSON.stringify对产生的字符串进行消费,否则没法将内存占用上去 */ this.$data.__dd__ = JSON.stringify(componentInfo + ' ' + helper.methods.createEmptyData(count, this._uid)); console.log(`[dd success] ${componentInfo} chain: ${this._componentChain}`); } } printLifeCycle(this, 'created'); }, beforeMount: function () { printLifeCycle(this, 'beforeMount'); }, mounted: function () { printLifeCycle(this, 'mounted'); }, beforeUpdate: function () { printLifeCycle(this, 'beforeUpdate'); }, activated: function () { printLifeCycle(this, 'activated'); }, deactivated: function () { printLifeCycle(this, 'deactivated'); }, updated: function () { printLifeCycle(this, 'updated'); }, beforeDestroy: function () { printLifeCycle(this, 'beforeDestroy'); }, destroyed: function () { printLifeCycle(this, 'destroyed'); if (this._componentTag) { const uid = this._uid; const name = this._componentName; const destroyTime = Date.now(); /* helper里的componentSummary有可能通过调用clear函数而被清除掉,所以需进行判断再更新赋值 */ const componentSummary = helper.componentsSummary[this._uid]; if (componentSummary) { /* 补充/更新组件信息 */ componentSummary.destroyTime = destroyTime; componentSummary.duration = destroyTime - this._createdTime; helper.destroyList.push(componentSummary); /* 统计被销毁的组件信息 */ Array.isArray(helper.destroyStatistics[name]) ? helper.destroyStatistics[name].push(componentSummary) : (helper.destroyStatistics[name] = [componentSummary]); /* 删除已销毁的组件实例 */ delete componentSummary.component; } // 解除引用关系 delete this._componentTag; delete this._componentChain; delete this._componentName; delete this._createdTime; delete this._createdHumanTime; delete this.$data.__dd__; delete helper.components[uid]; } else { console.error('存在未被正常标记的组件,请检查组件采集逻辑是否需完善', this); } } }); } /*! * @name menuCommand.js * @version 0.0.1 * @author Blaze * @date 2019/9/21 14:22 */ const monkeyMenu = { on (title, fn, accessKey) { return window.GM_registerMenuCommand && window.GM_registerMenuCommand(title, fn, accessKey) }, off (id) { return window.GM_unregisterMenuCommand && window.GM_unregisterMenuCommand(id) }, /* 切换类型的菜单功能 */ switch (title, fn, defVal) { const t = this; t.on(title, fn); } }; /** * 简单的i18n库 */ class I18n { constructor (config) { this._languages = {}; this._locale = this.getClientLang(); this._defaultLanguage = ''; this.init(config); } init (config) { if (!config) return false const t = this; t._locale = config.locale || t._locale; /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */ t._languages = config.languages || t._languages; t._defaultLanguage = config.defaultLanguage || t._defaultLanguage; } use () {} t (path) { const t = this; let result = t.getValByPath(t._languages[t._locale] || {}, path); /* 版本回退 */ if (!result && t._locale !== t._defaultLanguage) { result = t.getValByPath(t._languages[t._defaultLanguage] || {}, path); } return result || '' } /* 当前语言值 */ language () { return this._locale } languages () { return this._languages } changeLanguage (locale) { if (this._languages[locale]) { this._languages = locale; return locale } else { return false } } /** * 根据文本路径获取对象里面的值 * @param obj {Object} -必选 要操作的对象 * @param path {String} -必选 路径信息 * @returns {*} */ getValByPath (obj, path) { path = path || ''; const pathArr = path.split('.'); let result = obj; /* 递归提取结果值 */ for (let i = 0; i < pathArr.length; i++) { if (!result) break result = result[pathArr[i]]; } return result } /* 获取客户端当前的语言环境 */ getClientLang () { return navigator.languages ? navigator.languages[0] : navigator.language } } var zhCN = { about: '关于', issues: '反馈', setting: '设置', hotkeys: '快捷键', donate: '赞赏', debugHelper: { viewVueDebugHelperObject: 'vueDebugHelper对象', componentsStatistics: '当前存活组件统计', destroyStatisticsSort: '已销毁组件统计', componentsSummaryStatisticsSort: '全部组件混合统计', getDestroyByDuration: '组件存活时间信息', clearAll: '清空统计信息', printLifeCycleInfo: '打印组件生命周期信息', notPrintLifeCycleInfo: '取消组件生命周期信息打印', printLifeCycleInfoPrompt: { lifecycleFilters: '输入要打印的生命周期名称,多个可用,或|分隔,不输入则默认打印created', componentFilters: '输入要打印的组件名称,多个可用,或|分隔,不输入则默认打印所有组件' }, findComponents: '查找组件', findComponentsPrompt: { filters: '输入要查找的组件名称,或uid,多个可用,或|分隔' }, findNotContainElementComponents: '查找不包含DOM对象的组件', blockComponents: '阻断组件的创建', blockComponentsPrompt: { filters: '输入要阻断的组件名称,多个可用,或|分隔,输入为空则取消阻断' }, dd: '数据注入(dd)', undd: '取消数据注入(undd)', ddPrompt: { filter: '组件过滤器(如果为空,则对所有组件注入)', count: '指定注入数据的重复次数(默认1024)' }, devtools: { enabled: '自动开启vue-devtools', disable: '禁止开启vue-devtools' } } }; var enUS = { about: 'about', issues: 'feedback', setting: 'settings', hotkeys: 'Shortcut keys', donate: 'donate', debugHelper: { viewVueDebugHelperObject: 'vueDebugHelper object', componentsStatistics: 'Current surviving component statistics', destroyStatisticsSort: 'Destroyed component statistics', componentsSummaryStatisticsSort: 'All components mixed statistics', getDestroyByDuration: 'Component survival time information', clearAll: 'Clear statistics', dd: 'Data injection (dd)', undd: 'Cancel data injection (undd)', ddPrompt: { filter: 'Component filter (if empty, inject all components)', count: 'Specify the number of repetitions of injected data (default 1024)' } } }; var zhTW = { about: '關於', issues: '反饋', setting: '設置', hotkeys: '快捷鍵', donate: '讚賞', debugHelper: { viewVueDebugHelperObject: 'vueDebugHelper對象', componentsStatistics: '當前存活組件統計', destroyStatisticsSort: '已銷毀組件統計', componentsSummaryStatisticsSort: '全部組件混合統計', getDestroyByDuration: '組件存活時間信息', clearAll: '清空統計信息', dd: '數據注入(dd)', undd: '取消數據注入(undd)', ddPrompt: { filter: '組件過濾器(如果為空,則對所有組件注入)', count: '指定注入數據的重複次數(默認1024)' } } }; const messages = { 'zh-CN': zhCN, zh: zhCN, 'zh-HK': zhTW, 'zh-TW': zhTW, 'en-US': enUS, en: enUS, }; /*! * @name i18n.js * @description vue-debug-helper的国际化配置 * @version 0.0.1 * @author xxxily * @date 2022/04/26 14:56 * @github https://github.com/xxxily */ const i18n = new I18n({ defaultLanguage: 'en', /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */ // locale: 'zh-TW', languages: messages }); /*! * @name functionCall.js * @description 统一的提供外部功能调用管理模块 * @version 0.0.1 * @author xxxily * @date 2022/04/27 17:42 * @github https://github.com/xxxily */ const functionCall = { viewVueDebugHelperObject () { debug.log(i18n.t('debugHelper.viewVueDebugHelperObject'), helper); }, componentsStatistics () { const result = helper.methods.componentsStatistics(); let total = 0; /* 提供友好的可视化展示方式 */ console.table && console.table(result.map(item => { total += item.componentInstance.length; return { componentName: item.componentName, count: item.componentInstance.length } })); debug.log(`${i18n.t('debugHelper.componentsStatistics')} (total:${total})`, result); }, destroyStatisticsSort () { const result = helper.methods.destroyStatisticsSort(); /* 提供友好的可视化展示方式 */ console.table && console.table(result.map(item => { const durationList = item.destroyList.map(item => item.duration); const maxDuration = Math.max(...durationList); const minDuration = Math.min(...durationList); const durationRange = maxDuration - minDuration; return { componentName: item.componentName, count: item.destroyList.length, avgDuration: durationList.reduce((pre, cur) => pre + cur, 0) / durationList.length, maxDuration, minDuration, durationRange, durationRangePercent: (1000 - minDuration) / durationRange } })); debug.log(i18n.t('debugHelper.destroyStatisticsSort'), result); }, componentsSummaryStatisticsSort () { const result = helper.methods.componentsSummaryStatisticsSort(); let total = 0; /* 提供友好的可视化展示方式 */ console.table && console.table(result.map(item => { total += item.componentsSummary.length; return { componentName: item.componentName, count: item.componentsSummary.length } })); debug.log(`${i18n.t('debugHelper.componentsSummaryStatisticsSort')} (total:${total})`, result); }, getDestroyByDuration () { const destroyInfo = helper.methods.getDestroyByDuration(); console.table && console.table(destroyInfo.destroyList); debug.log(i18n.t('debugHelper.getDestroyByDuration'), destroyInfo); }, clearAll () { helper.methods.clearAll(); debug.log(i18n.t('debugHelper.clearAll')); }, printLifeCycleInfo () { const lifecycleFilters = window.prompt(i18n.t('debugHelper.printLifeCycleInfoPrompt.lifecycleFilters'), helper.config.lifecycle.filters.join(',')); const componentFilters = window.prompt(i18n.t('debugHelper.printLifeCycleInfoPrompt.componentFilters'), helper.config.lifecycle.componentFilters.join(',')); if (lifecycleFilters !== null && componentFilters !== null) { debug.log(i18n.t('debugHelper.printLifeCycleInfo')); helper.methods.printLifeCycleInfo(lifecycleFilters, componentFilters); } }, notPrintLifeCycleInfo () { debug.log(i18n.t('debugHelper.notPrintLifeCycleInfo')); helper.methods.notPrintLifeCycleInfo(); }, findComponents () { const filters = window.prompt(i18n.t('debugHelper.findComponentsPrompt.filters'), helper.config.findComponentsFilters.join(',')); if (filters !== null) { debug.log(i18n.t('debugHelper.findComponents'), helper.methods.findComponents(filters)); } }, findNotContainElementComponents () { debug.log(i18n.t('debugHelper.findNotContainElementComponents'), helper.methods.findNotContainElementComponents()); }, blockComponents () { const filters = window.prompt(i18n.t('debugHelper.blockComponentsPrompt.filters'), helper.config.blockFilters.join(',')); if (filters !== null) { helper.methods.blockComponents(filters); debug.log(i18n.t('debugHelper.blockComponents'), filters); } }, dd () { const filter = window.prompt(i18n.t('debugHelper.ddPrompt.filter'), helper.config.dd.filters.join(',')); const count = window.prompt(i18n.t('debugHelper.ddPrompt.count'), helper.config.dd.count); if (filter !== null && count !== null) { debug.log(i18n.t('debugHelper.dd')); helper.methods.dd(filter, Number(count)); } }, undd () { debug.log(i18n.t('debugHelper.undd')); helper.methods.undd(); } }; /*! * @name menu.js * @description vue-debug-helper的菜单配置 * @version 0.0.1 * @author xxxily * @date 2022/04/25 22:28 * @github https://github.com/xxxily */ function menuRegister (Vue) { if (!Vue) { monkeyMenu.on('not detected ' + i18n.t('issues'), () => { window.GM_openInTab('https://github.com/xxxily/vue-debug-helper/issues', { active: true, insert: true, setParent: true }); }); return false } /* 批量注册菜单 */ Object.keys(functionCall).forEach(key => { const text = i18n.t(`debugHelper.${key}`); if (text && functionCall[key] instanceof Function) { monkeyMenu.on(text, functionCall[key]); } }); /* 是否开启vue-devtools的菜单 */ const devtoolsText = helper.config.devtools ? i18n.t('debugHelper.devtools.disable') : i18n.t('debugHelper.devtools.enabled'); monkeyMenu.on(devtoolsText, helper.methods.toggleDevtools); // monkeyMenu.on('i18n.t('setting')', () => { // window.alert('功能开发中,敬请期待...') // }) monkeyMenu.on(i18n.t('issues'), () => { window.GM_openInTab('https://github.com/xxxily/vue-debug-helper/issues', { active: true, insert: true, setParent: true }); }); // monkeyMenu.on(i18n.t('donate'), () => { // window.GM_openInTab('https://cdn.jsdelivr.net/gh/xxxily/vue-debug-helper@main/donate.png', { // active: true, // insert: true, // setParent: true // }) // }) } const isff = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase().indexOf('firefox') > 0 : false; // 绑定事件 function addEvent (object, event, method) { if (object.addEventListener) { object.addEventListener(event, method, false); } else if (object.attachEvent) { object.attachEvent(`on${event}`, () => { method(window.event); }); } } // 修饰键转换成对应的键码 function getMods (modifier, key) { const mods = key.slice(0, key.length - 1); for (let i = 0; i < mods.length; i++) mods[i] = modifier[mods[i].toLowerCase()]; return mods } // 处理传的key字符串转换成数组 function getKeys (key) { if (typeof key !== 'string') key = ''; key = key.replace(/\s/g, ''); // 匹配任何空白字符,包括空格、制表符、换页符等等 const keys = key.split(','); // 同时设置多个快捷键,以','分割 let index = keys.lastIndexOf(''); // 快捷键可能包含',',需特殊处理 for (; index >= 0;) { keys[index - 1] += ','; keys.splice(index, 1); index = keys.lastIndexOf(''); } return keys } // 比较修饰键的数组 function compareArray (a1, a2) { const arr1 = a1.length >= a2.length ? a1 : a2; const arr2 = a1.length >= a2.length ? a2 : a1; let isIndex = true; for (let i = 0; i < arr1.length; i++) { if (arr2.indexOf(arr1[i]) === -1) isIndex = false; } return isIndex } // Special Keys const _keyMap = { backspace: 8, tab: 9, clear: 12, enter: 13, return: 13, esc: 27, escape: 27, space: 32, left: 37, up: 38, right: 39, down: 40, del: 46, delete: 46, ins: 45, insert: 45, home: 36, end: 35, pageup: 33, pagedown: 34, capslock: 20, num_0: 96, num_1: 97, num_2: 98, num_3: 99, num_4: 100, num_5: 101, num_6: 102, num_7: 103, num_8: 104, num_9: 105, num_multiply: 106, num_add: 107, num_enter: 108, num_subtract: 109, num_decimal: 110, num_divide: 111, '⇪': 20, ',': 188, '.': 190, '/': 191, '`': 192, '-': isff ? 173 : 189, '=': isff ? 61 : 187, ';': isff ? 59 : 186, '\'': 222, '[': 219, ']': 221, '\\': 220 }; // Modifier Keys const _modifier = { // shiftKey '⇧': 16, shift: 16, // altKey '⌥': 18, alt: 18, option: 18, // ctrlKey '⌃': 17, ctrl: 17, control: 17, // metaKey '⌘': 91, cmd: 91, command: 91 }; const modifierMap = { 16: 'shiftKey', 18: 'altKey', 17: 'ctrlKey', 91: 'metaKey', shiftKey: 16, ctrlKey: 17, altKey: 18, metaKey: 91 }; const _mods = { 16: false, 18: false, 17: false, 91: false }; const _handlers = {}; // F1~F12 special key for (let k = 1; k < 20; k++) { _keyMap[`f${k}`] = 111 + k; } // https://github.com/jaywcjlove/hotkeys let _downKeys = []; // 记录摁下的绑定键 let winListendFocus = false; // window是否已经监听了focus事件 let _scope = 'all'; // 默认热键范围 const elementHasBindEvent = []; // 已绑定事件的节点记录 // 返回键码 const code = (x) => _keyMap[x.toLowerCase()] || _modifier[x.toLowerCase()] || x.toUpperCase().charCodeAt(0); // 设置获取当前范围(默认为'所有') function setScope (scope) { _scope = scope || 'all'; } // 获取当前范围 function getScope () { return _scope || 'all' } // 获取摁下绑定键的键值 function getPressedKeyCodes () { return _downKeys.slice(0) } // 表单控件控件判断 返回 Boolean // hotkey is effective only when filter return true function filter (event) { const target = event.target || event.srcElement; const { tagName } = target; let flag = true; // ignore: isContentEditable === 'true', and