// ==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.2
// @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 Debug {
constructor (msg) {
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'
};
return function () {
if (!window._debugMode_) {
return false
}
const curTime = new Date();
const H = curTime.getHours();
const M = curTime.getMinutes();
const S = curTime.getSeconds();
const msg = tipsMsg || 'debug message:';
const arg = Array.from(arguments);
arg.unshift(`color: white; background-color: ${color || bgColorMap[name] || '#95B46A'}`);
arg.unshift(`%c [${H}:${M}:${S}] ${msg} `);
window.console[name].apply(window.console, arg);
}
}
isDebugMode () {
return Boolean(window._debugMode_)
}
}
var Debug$1 = new Debug();
var debug = Debug$1.create('vue-debug-helper message:');
/**
* 对特定数据结构的对象进行排序
* @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 -可选 指定数据长度,默认为1024
* @param {string} str - 可选 指定数据的字符串,默认为'd'
*/
function createEmptyData (size = 1024, str = 'd') {
const arr = [];
arr.length = size + 1;
return arr.join(str)
}
window.vueDebugHelper = {
/* 存储全部未被销毁的组件对象 */
components: {},
/* 存储全部创建过的组件的概要信息,即使销毁了概要信息依然存在 */
componentsSummary: {},
/* 基于componentsSummary的组件情况统计 */
componentsSummaryStatistics: {},
/* 已销毁的组件概要信息列表 */
destroyList: [],
/* 基于destroyList的组件情况统计 */
destroyStatistics: {},
/* 给组件注入空白数据的配置信息 */
ddConfig: {
enabled: false,
filters: [],
size: 1024
}
};
const helper = window.vueDebugHelper;
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 (moreDetail) {
result.push({
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(' -> ')
}
},
/**
* 给指定组件注入大量空数据,以便观察组件的内存泄露情况
* @param {Array|string} filter -必选 指定组件的名称,如果为空则表示注入所有组件
* @param {number} size -可选 指定注入空数据的大小,单位Kb,默认为1024Kb,即1Mb
* @returns
*/
dd (filter, size = 1024) {
filter = filter || [];
/* 如果是字符串,则支持通过, | 两个符号来指定多个组件名称的过滤器 */
if (typeof filter === 'string') {
/* 移除前后的, |分隔符,防止出现空字符的过滤规则 */
filter.replace(/^(,|\|)/, '').replace(/(,|\|)$/, '');
if (/\|/.test(filter)) {
filter = filter.split('|');
} else {
filter = filter.split(',');
}
}
helper.ddConfig = {
enabled: true,
filters: filter,
size
};
},
/* 禁止给组件注入空数据 */
undd () {
helper.ddConfig = {
enabled: false,
filters: [],
size: 1024
};
}
};
helper.methods = methods;
function mixinRegister (Vue) {
if (!Vue || !Vue.mixin) {
debug.error('未检查到VUE对象,请检查是否引入了VUE,且将VUE对象挂载到全局变量window.Vue上');
return false
}
Vue.mixin({
beforeCreate: function () {
const tag = this.$options?._componentTag || this.$vnode?.tag || 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();
/* 判断是否为函数式组件,函数式组件无状态 (没有响应式数据),也没有实例,也没生命周期概念 */
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,
// 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]);
},
created: function () {
/* 增加空白数据,方便观察内存泄露情况 */
if (helper.ddConfig.enabled) {
let needDd = false;
if (helper.ddConfig.filters.length === 0) {
needDd = true;
} else {
for (let index = 0; index < helper.ddConfig.filters.length; index++) {
const filter = helper.ddConfig.filters[index];
if (filter === this._componentName || String(this._componentName).endsWith(filter)) {
needDd = true;
break
}
}
}
if (needDd) {
const size = helper.ddConfig.size * 1024;
const componentInfo = `tag: ${this._componentTag}, uid: ${this._uid}, createdTime: ${this._createdTime}`;
/* 此处必须使用JSON.stringify对产生的字符串进行消费,否则没法将内存占用上去 */
this.$data.__dd__ = componentInfo + ' ' + JSON.stringify(helper.methods.createEmptyData(size, 'd'));
console.log(`[dd success] ${componentInfo} componentChain: ${this._componentChain}`);
}
}
},
destroyed: function () {
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 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: '赞赏',
};
var enUS = {
about: 'about',
issues: 'issues',
setting: 'setting',
hotkeys: 'hotkeys',
donate: 'donate'
};
var zhTW = {
about: '關於',
issues: '反饋',
setting: '設置',
hotkeys: '快捷鍵',
donate: '讚賞',
};
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 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 () {
monkeyMenu.on('查看vueDebugHelper对象', () => {
debug.log('vueDebugHelper对象', helper);
});
monkeyMenu.on('当前存活组件统计', () => {
debug.log('当前存活组件统计', helper.methods.componentsStatistics());
});
monkeyMenu.on('已销毁组件统计', () => {
debug.log('已销毁组件统计', helper.methods.destroyStatisticsSort());
});
monkeyMenu.on('全部组件混合统计', () => {
debug.log('全部组件混合统计', helper.methods.componentsSummaryStatisticsSort());
});
monkeyMenu.on('组件存活时间信息', () => {
debug.log('组件存活时间信息', helper.methods.getDestroyByDuration());
});
monkeyMenu.on('清空统计信息', () => {
helper.methods.clearAll();
debug.log('清空统计信息');
});
monkeyMenu.on('数据注入(dd)', () => {
const filter = window.prompt('组件过滤器(如果为空,则对所有组件注入)', '');
const size = window.prompt('指定注入数据的大小值(默认1Mb)', 1024);
debug.log('数据注入(dd)');
helper.methods.dd(filter, Number(size));
});
monkeyMenu.on('取消数据注入(undd)', () => {
debug.log('取消数据注入(undd)');
helper.methods.undd();
});
// 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/h5player@master/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