一个轻量、可拓展、针对手机网页的前端开发者调试面板。
这是github的readme介绍,对于调试移动端以及上线后出现的一些问题确实是一个很好的工具!! 为腾讯开源点个赞。
使用方法请参考:腾讯开源vConsle移动端调试控制台工具 (请原谅我把它叫的那么长哈哈哈)
总的来说,vConsole的源码还是很清晰的,采用了es6的语法,以面向对象的形式即类的显示来组织代码,按照控制台的ui来分割,这一点值得借鉴和学习。
简单的画了一个草图,首先要明确是 VConsole为核心类,负责插件的生成,事件绑定,运行等,而vConsolePlugin 类似一个抽象类,具体实现渲染绑定等由子类去具体实现。所以VConsole的扩展是通过添加VConsolePlugin的实现来的。(如果有歧义请指正批评)
vConsole采用的是一个叫做 Mito.js 的 一个简单的模板引擎(估计是腾讯自己内部写的一个模板引擎),这个文件在lib文件夹下,代码很简短就一个rener方法,然后配合模板html使用
//使用指定数据将模板文本编译成 element 对象或者 HTML 字符串。
//(required) tpl: 模板字符串。
//(required) data: 一组 key-value 形式的数据源。
//(optional) toString: 布尔值,用于设定返回值为 element 对象还是 HTML 字符串,默认为 `false`。
//返回值 Element 对象或者 HTML 字符串
function render(tpl, data, toString)
模板语法涉及到了常用的if else ,for和switch 常用的流程语句,对付一些简单的需求这个已经足够了,详细语法使用参考官方文档的helper_function.md
核心模块主要是core.js的这个js文件,也就是VConsole类的定义 。
首先来看下这个类的Construction方法,我把注释写在代码里 ,这样看起来 应该很清晰了
constructor(opt) {
//判断是否已经存在VConsole实例了,通过判断document文档结构是否存在了VCONSOLE_ID即(#__vconsole) 这个id。
if (!!$.one(VCONSOLE_ID)) {
console.debug('vConsole is already exists.');
return;
}
//将this用that保存起来,这里的作用不言而喻,就是为了防止接下来的操作(方法)调用会改变this的指向
let that = this;
//版本号
this.version = pkg.version;
//基础的dom 可以理解为root 即id为VCONSOLE_ID的文档元素
this.$dom = null;
//再次判断是否已经初始化的标识,和一开始的方法一样的作用
this.isInited = false;
//配置选项 以下是对加载哪几个默认插件的配置
this.option = {
defaultPlugins: ['system', 'network', 'element', 'storage']
};
//当前活动的tab页框
this.activedTab = '';
//tab页框数组
this.tabList = [];
//插件列表
this.pluginList = {};
//vConsole的悬浮按钮的配置(位置)
this.switchPos = {
x: 10, // right
y: 10, // bottom
startX: 0,
startY: 0,
endX: 0,
endY: 0
};
// 暴露一些公用的方法出去,比如判断是否对象,数组等 详见在../lib/tool.js的文件
this.tool = tool;
//暴露$的方法,query.js文件夹
this.$ = $;
// 合并用户配置和默认配置。for in 用法了解下
if (tool.isObject(opt)) {
for (let key in opt) {
this.option[key] = opt[key];
}
}
//初始化添加插件 插件的初始化 添加到tabList
this._addBuiltInPlugins();
// Vconsole的UI加载,
let _onload = function() {
if (that.isInited) {
return;
}
//渲染VConsole的主面板,swicth按钮
that._render();
//面板上的交互 模拟 触摸滑动
that._mockTap();
//绑定事件 拖动swicth按钮,点击面板的tab等
that._bindEvent();
//面板渲染完成后的插件自动加载渲染
that._autoRun();
};
//当document渲染加载完成 调用_onload方法
if (document !== undefined) {
if (document.readyState == 'complete') {
_onload();
} else {
$.bind(window, 'load', _onload);
}
} else {
// if document does not exist, wait for it
let _timer;
let _pollingDocument = function() {
if (!!document && document.readyState == 'complete') {
_timer && clearTimeout(_timer);
_onload();
} else {
_timer = setTimeout(_pollingDocument, 1);
}
};
_timer = setTimeout(_pollingDocument, 1);
}
}
总的流程用流程图表示
大概VConsole类的流程和做的一些事情都阐述完了,接下来分析一些重要的方法
addPlugin();
添加一个新的插件,这个为基本组件如何加到VConsole的公用方法,实现加入到VConsole体系,并且运行起来。
/**
* add a new plugin
* @public
* @param object VConsolePlugin object
* @return boolean
*/
addPlugin(plugin) {
// ignore this plugin if it has already been installed
if (this.pluginList[plugin.id] !== undefined) {
console.debug('Plugin ' + plugin.id + ' has already been added.');
return false;
}
this.pluginList[plugin.id] = plugin;
// init plugin only if vConsole is ready
if (this.isInited) {
this._initPlugin(plugin);
// if it's the first plugin, show it by default
if (this.tabList.length == 1) {
this.showTab(this.tabList[0]);
}
}
return true;
}
而在这个方法中最重要的方法就是下面这个方法_initPlugin();初始化插件,包括加入panel中的点击事件等一些操作,其中的insertAdjacentElement()方法相当于jq中的insertAfter和insertBefore等方法
/**
* init a plugin
* @private
*/
_initPlugin(plugin) {
let that = this;
plugin.vConsole = this;
// start init
plugin.trigger('init');
// render tab (if it is a tab plugin then it should has tab-related events)
plugin.trigger('renderTab', function(tabboxHTML) {
// add to tabList
that.tabList.push(plugin.id);
// render tabbar
let $tabbar = $.render(tplTabbar, { id: plugin.id, name: plugin.name });
$.one('.vc-tabbar', that.$dom).insertAdjacentElement('beforeend', $tabbar);
// render tabbox
let $tabbox = $.render(tplTabbox, { id: plugin.id });
if (!!tabboxHTML) {
if (tool.isString(tabboxHTML)) {
$tabbox.innerHTML += tabboxHTML;
} else if (tool.isFunction(tabboxHTML.appendTo)) {
tabboxHTML.appendTo($tabbox);
} else if (tool.isElement(tabboxHTML)) {
$tabbox.insertAdjacentElement('beforeend', tabboxHTML);
}
}
$.one('.vc-content', that.$dom).insertAdjacentElement('beforeend', $tabbox);
});
// render top bar
plugin.trigger('addTopBar', function(btnList) {
if (!btnList) {
return;
}
let $topbar = $.one('.vc-topbar', that.$dom);
for (let i = 0; i < btnList.length; i++) {
let item = btnList[i];
let $item = $.render(tplTopBarItem, {
name: item.name || 'Undefined',
className: item.className || '',
pluginID: plugin.id
});
if (item.data) {
for (let k in item.data) {
$item.dataset[k] = item.data[k];
}
}
if (tool.isFunction(item.onClick)) {
$.bind($item, 'click', function(e) {
let enable = item.onClick.call($item);
if (enable === false) {
// do nothing
} else {
$.removeClass($.all('.vc-topbar-' + plugin.id), 'vc-actived');
$.addClass($item, 'vc-actived');
}
});
}
$topbar.insertAdjacentElement('beforeend', $item);
}
});
// render tool bar
plugin.trigger('addTool', function(toolList) {
if (!toolList) {
return;
}
let $defaultBtn = $.one('.vc-tool-last', that.$dom);
for (let i = 0; i < toolList.length; i++) {
let item = toolList[i];
let $item = $.render(tplToolItem, {
name: item.name || 'Undefined',
pluginID: plugin.id
});
if (item.global == true) {
$.addClass($item, 'vc-global-tool');
}
if (tool.isFunction(item.onClick)) {
$.bind($item, 'click', function(e) {
item.onClick.call($item);
});
}
$defaultBtn.parentNode.insertBefore($item, $defaultBtn);
}
});
// end init
plugin.isReady = true;
plugin.trigger('ready');
}
这个函数是初始化插件,例如日志模块,网络模块都是以插件的形式,集成进来的,既然是插件,,那就要有插槽,自然,插件提供插口。这个函数就是一个插槽,当插件集成进来的时候,就通过触发插槽里的插口来实现调用插件的方法,这个有五个个触发的方法,插件中实现了就会触发
class VConsolePlugin {
constructor(id, name = 'newPlugin') {
//id 为log system 来标识插件
this.id = id;
this.name = name;
//组件的准备状态 在VConsole的初始化完插件 设置为true,即组件渲染完成
this.isReady = false;
//事件列表
this.eventList = {};
}
get id() {
return this._id;
}
set id(value) {
if (!value) {
throw 'Plugin ID cannot be empty';
}
this._id = value.toLowerCase();
}
get name() {
return this._name;
}
set name(value) {
if (!value) {
throw 'Plugin name cannot be empty';
}
this._name = value;
}
//获取VConsole实例
get vConsole() {
return this._vConsole || undefined;
}
set vConsole(value) {
if (!value) {
throw 'vConsole cannot be empty';
}
this._vConsole = value;
}
/**
* register an event
* @public
* @param string
* @param function
*/
on(eventName, callback) {
this.eventList[eventName] = callback;
return this;
}
/**
* trigger an event
* @public
* @param string
* @param mixed
*/
trigger(eventName, data) {
if (typeof this.eventList[eventName] === 'function') {
// registered by `.on()` method
this.eventList[eventName].call(this, data);
} else {
// registered by `.onXxx()` method
let method = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1);
if (typeof this[method] === 'function') {
this[method].call(this, data);
}
}
return this;
}
} // END class
其实也没做什么事,但是有一个很重要的trigger方法
/**
* trigger an event
* @public
* @param string
* @param mixed
*/
trigger(eventName, data) {
if (typeof this.eventList[eventName] === 'function') {
// registered by `.on()` method
this.eventList[eventName].call(this, data);
} else {
// registered by `.onXxx()` method
let method = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1);
if (typeof this[method] === 'function') {
this[method].call(this, data);
}
}
return this;
}
这样一来,只要每个插件的方法 以 on 开头 ,就可以通过基类触发方法的执行了,具体的逻辑是先去找 on注册的,如果没有 就去实例中找,还有一点这个方法是返回对象本身的。