// ==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