关于vConsole 源码的理解分享(vConsole一个移动端调试控制台工具)(1)

一个轻量、可拓展、针对手机网页的前端开发者调试面板。

特性

  • 查看 console 日志
  • 查看网络请求
  • 查看页面 element 结构
  • 查看 Cookies 和 localStorage
  • 手动执行 JS 命令行
  • 自定义插件

这是github的readme介绍,对于调试移动端以及上线后出现的一些问题确实是一个很好的工具!! 为腾讯开源点个赞。
使用方法请参考:腾讯开源vConsle移动端调试控制台工具 (请原谅我把它叫的那么长哈哈哈)

源码分析

总的来说,vConsole的源码还是很清晰的,采用了es6的语法,以面向对象的形式即类的显示来组织代码,按照控制台的ui来分割,这一点值得借鉴和学习。

源码代码结构

VConsole的类结构图

简单的画了一个草图,首先要明确是 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);
        }
    }

总的流程用流程图表示

Created with Raphaël 2.1.2 开始 初始化一些参数:tablist,Pluginlist,版本号等 加载初始化配置的插件 是否加载document完成 VConsole加载UI,绑定事件,运行插件等 结束 等待document加载完 yes no

大概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');
    }

这个函数是初始化插件,例如日志模块,网络模块都是以插件的形式,集成进来的,既然是插件,,那就要有插槽,自然,插件提供插口。这个函数就是一个插槽,当插件集成进来的时候,就通过触发插槽里的插口来实现调用插件的方法,这个有五个个触发的方法,插件中实现了就会触发

  • init 初始化插件
  • renderTab 渲染tab 基础的如log system这个
  • addTopBar 渲染 bar栏 tab下的子栏目
  • addTool 添加工具方法 如clear hide
  • ready 插件准备完毕,开始输出 log 等
VConsolePlugin 这个抽象类做了什么
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注册的,如果没有 就去实例中找,还有一点这个方法是返回对象本身的。

发布了38 篇原创文章 · 获赞 14 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/sinat_23156865/article/details/80754829