美文网首页前端那些事儿Web前端之路前端猎奇
JavaScript嗅探执行神器-sniffer.js,你值得拥

JavaScript嗅探执行神器-sniffer.js,你值得拥

作者: 我_是leon | 来源:发表于2017-02-14 00:29 被阅读569次

    一、热身——先看实战代码

    a.js 文件

    // 定义Wall及内部方法
    ;(function(window, FUNC, undefined){
        var name = 'wall';
    
        Wall.say = function(name){
            console.log('I\'m '+ name +' !');
        };
    
        Wall.message = {
            getName : function(){
                return name;
            },
            setName : function(firstName, secondName){
                name = firstName+'-'+secondName;
            }
        };
    })(window, window.Wall || (window.Wall = {}));
    

    index.jsp文件

    <script type='text/javascript'>
        <%
            // Java 代码直出 js
            out.print("Sniffer.run({'base':window,'name':'Wall.say','subscribe':true}, 'wall');\n");
        %>
    
        // Lab.js是一个文件加载工具
        // 依赖的a.js加载完毕后,则可执行缓存的js方法
        $LAB.script("a.js").wait(function(){
            // 触发已订阅的方法
            Sniffer.trigger({
                'base':window,
                'name':'Wall.say'
            });
        });
    </script>
    

    这样,不管a.js文件多大,Wall.say('wall')都可以等到文件真正加载完后,再执行。

    二、工具简介

    // 执行 Wall.message.setName('wang', 'wall');
    Sniffer.run({
        'base':Wall,
        'name':'message.setName',
        'subscribe':true
    }, 'wang', 'wall');
    

    看这个执行代码,你也许会感觉困惑-什么鬼!

    sniffer.js作用就是可以试探执行方法,如果不可执行,也不会抛错。

    比如例子Wall.message.setName('wang', 'wall');
     如果该方法所在文件还没有加载,也不会报错。
     处理的逻辑就是先缓存起来,等方法加载好后,再进行调用。

    再次调用的方法如下:

    // 触发已订阅的方法
    Sniffer.trigger({
        'base':Wall,
        'name':'message.setName'
    });
    

    在线demo:https://wall-wxk.github.io/blogDemo/2017/02/13/sniffer.html (需要在控制台看,建议用pc)

    说起这个工具的诞生,是因为公司业务的需要,自己写的一个工具。
     因为公司的后台语言是java,喜欢用jsp的out.print()方法,直接输出一些js方法给客户端执行。
     这就存在一个矛盾点,有时候js文件还没下载好,后台输出的语句已经开始调用方法,这就很尴尬。

    所以,这个工具的作用有两点

    1. 检测执行的js方法是否存在,存在则立即执行。
     2. 缓存暂时不存在的js方法,等真正可执行的时候,再从缓存队列里面拿出来,触发执行。

    三、嗅探核心基础——运算符in

    方法是通过使用运算符in去遍历命名空间中的方法,如果取得到值,则代表可执行。反之,则代表不可执行。

    运算符 in

    通过这个例子,就可以知道这个sniffer.js的嗅探原理了。

    四、抽象出嗅探方法

    /**
    * @function {private} 检测方法是否可用
    * @param {string} funcName -- 方法名***.***.***
    * @param {object} base -- 方法所依附的对象 
    **/
    function checkMethod(funcName, base){
        var methodList = funcName.split('.'), // 方法名list
            readyFunc = base, // 检测合格的函数部分
            result = {
                'success':true,
                'func':function(){}
            }, // 返回的检测结果
            methodName, // 单个方法名
            i;
            
        for(i = 0; i < methodList.length; i++){
            methodName = methodList[i];
            if(methodName in readyFunc){
                readyFunc = readyFunc[methodName];
            }else{
                result.success = false;
                return result;
            }
        }
        
        result.func = readyFunc;
        return result; 
    }
    

    Wall.message.setName('wang', 'wall');这样的方法,要判断是否可执行,需要执行以下步骤:
     1. 判断Wall是否存在window中。
     2. Wall存在,则继续判断message是否在Wall中。
     3. message存在,则继续判断setName是否在message
     4. 最后,都判断存在了,则代表可执行。如果中间的任意一个检测不通过,则方法不可执行。

    五、实现缓存

    缓存使用闭包实现的。以队列的性质,存储在list

    ;(function(FUN, undefined){
        'use strict'
    
        var list = []; // 存储订阅的需要调用的方法
    
        // 执行方法
        FUN.run = function(){
            // 很多代码...
            
            //将订阅的函数缓存起来
            list.push(...);
        };
        
    })(window.Sniffer || (window.Sniffer = {}));
    

    六、确定队列中单个项的内容

    1. 指定检测的基点 base
     由于运算符in工作时,需要几个基点给它检测。所以第一个要有的项就是base

    2. 检测的字符类型的方法名 name
     像Wall.message.setName('wang', 'wall');,如果已经指定基点{'base':Wall},则还需要message.setName。所以要存储message.setName,也即{'base':Wall, 'name':'message.setName'}

    3. 缓存方法的参数 args
     像Wall.message.setName('wang', 'wall');,有两个参数('wang', 'wall'),所以需要存储起来。也即{'base':Wall, 'name':'message.setName', 'args':['wang', 'wall']}

    为什么参数使用数组缓存起来,是因为方法的参数是变化的,所以后续的代码需要apply去做触发。同理,这里的参数就需要用数组进行缓存

    所以,缓存队列的单个项内容如下:

    {
        'base':Wall,
        'name':'message.setName',
        'args':['wang', 'wall']
    }
    

    七、实现run方法

    ;(function(FUN, undefined){
        'use strict'
    
        var list = []; // 存储订阅的需要调用的方法
    
        /**
        * @function 函数转换接口,用于判断函数是否存在命名空间中,有则调用,无则不调用
        * @version {create} 2015-11-30
        * @description
        *       用途:只设计用于延迟加载
        *       示例:Wall.mytext.init(45, false);
        *       调用:Sniffer.run({'base':window, 'name':'Wall.mytext.init'}, 45, false);
                    或 Sniffer.run({'base':Wall, 'name':'mytext.init'}, 45, false);
        *       如果不知道参数的个数,不能直接写,可以用apply的方式调用当前方法
        *       示例:  Sniffer.run.apply(window, [ {'name':'Wall.mytext.init'}, 45, false ]);
        **/
        FUN.run = function(){
            if(arguments.length < 1 || typeof arguments[0] != 'object'){
                throw new Error('Sniffer.run 参数错误');
                return;
            }
            
            var name = arguments[0].name, // 函数名 0位为Object类型,方便做扩展
                subscribe = arguments[0].subscribe || false, // 订阅当函数可执行时,调用该函数, true:订阅; false:不订阅
                prompt = arguments[0].prompt || false, // 是否显示提示语(当函数未能执行的时候)
                promptMsg = arguments[0].promptMsg || '功能还在加载中,请稍候', // 函数未能执行提示语
                base = arguments[0].base || window, // 基准对象,函数查找的起点
                
                args = Array.prototype.slice.call(arguments), // 参数列表
                funcArgs = args.slice(1), // 函数的参数列表
                callbackFunc = {}, // 临时存放需要回调的函数
                result; // 检测结果
    
            result = checkMethod(name, base);
            if(result.success){
                subscribe = false;
                try{
                    return result.func.apply(result.func, funcArgs); // apply调整函数的指针指向
                }catch(e){
                    (typeof console != 'undefined') && console.log && console.log('错误:name='+ e.name +'; message='+ e.message);
                }
            }else{
                if(prompt){
                    // 输出提示语到页面,代码略
                }
            }
            
            //将订阅的函数缓存起来
            if(subscribe){
                callbackFunc.name = name;
                callbackFunc.base = base;
                callbackFunc.args = funcArgs;
                list.push(callbackFunc);
            }
        };
        
        // 嗅探方法
        function checkMethod(funcName, base){
            // 代码...
        }
    })(window.Sniffer || (window.Sniffer = {}));
    

    run方法的作用是:检测方法是否可执行,可执行,则执行。不可执行,则根据传入的参数,决定要不要缓存。

    这个run方法的重点,是妙用arguments,实现0-n个参数自由传入

    第一个形参arguments[0],固定是用来传入配置项的。存储要检测的基点base,方法字符串argument[0].name以及缓存标志arguments[0].subscribe

    第二个形参到第n个形参,则由方法调用者传入需要使用的参数。

    利用泛型方法,将arguments转换为真正的数组。(args = Array.prototype.slice.call(arguments)
     然后,切割出方法调用需要用到的参数。(funcArgs = args.slice(1)

    run方法的arguments处理完毕后,就可以调用checkMethod方法进行嗅探。

    根据嗅探的结果,分两种情况

    嗅探结果为可执行,则调用apply执行
    return result.func.apply(result.func, funcArgs);

    这里的重点是必须制定作用域为result.func,也即例子的Wall.message.setName
     这样,如果方法中使用了this,指向也不会发生改变。

    使用return,是因为一些方法执行后是有返回值的,所以这里需要加上return,将返回值传递出去。

    嗅探结果为不可执行,则根据传入的配置值subscribe,决定是否缓存到队列list中。
     需要缓存,则拼接好队列单个项,push进list。

    八、实现trigger方法

    ;(function(FUN, undefined){
        'use strict'
    
        var list = []; // 存储订阅的需要调用的方法
    
        // 执行方法
        FUN.run = function(){
            // 代码...
        };
        
        /**
        * @function 触发函数接口,调用已提前订阅的函数
        * @param {object} option -- 需要调用的相关参数
        * @description
        *       用途:只设计用于延迟加载
        *       另外,调用trigger方法的前提是,订阅方法所在js已经加载并解析完毕
        *       不管触发成功与否,都会清除list中对应的项
        **/
        FUN.trigger = function(option){
            if(typeof option !== 'object'){
                throw new Error('Sniffer.trigger 参数错误');
                return;
            }
            
            var funcName = option.name || '', // 函数名
                base = option.base || window, // 基准对象,函数查找的起点
                newList = [], // 用于更新list
                result, // 检测结果
                func, // 存储执行方法的指针
                i, // 遍历list
                param; // 临时存储list[i]
            
            if(funcName.length < 1){
                return;
            }
            
            // 遍历list,执行对应的函数,并将其从缓存池list中删除
            for(i = 0; i < list.length; i++){
                param = list[i];
                if(param.name == funcName){
                    result = checkMethod(funcName, base);
                    if( result.success ){
                        try{
                            result.func.apply(result.func, param.args);
                        }catch(e){
                            (typeof console != 'undefined') && console.log && console.log('错误:name='+ e.name +'; message='+ e.message);
                        }
                    }
                }else{
                    newList.push(param);
                }
            }
            
            list = newList;
        };
        
        // 嗅探方法
        function checkMethod(funcName, base){
            // 代码...
        }
    })(window.Sniffer || (window.Sniffer = {}));
    

    如果前面的run方法看懂了,trigger方法也就不难理解了。

    1. 首先要告知trigger方法,需要从队列list中拿出哪个方法执行。
     2. 在执行方法之前,需要再次嗅探这个方法是否已经存在。存在了,才可以执行。否则,则可以认为方法已经不存在,可以从缓存中移除。


    九、实用性和可靠度

    实用性这方面是毋容置疑的,不管是什么代码栈,Sniffer.js都值得你拥有!

    可靠度方面,Sniffer.js使用在高流量的公司产品上,至今没有出现反馈任何兼容、或者性能问题。这方面也可以打包票!

    最后,附上源码地址:https://github.com/wall-wxk/sniffer/blob/master/sniffer.js


    喜欢我文章的朋友,扫描以下二维码,关注我的个人技术博客,我的技术文章会第一时间在博客上更新

    点击链接wall的个人博客

    wall的个人博客

    相关文章

      网友评论

        本文标题:JavaScript嗅探执行神器-sniffer.js,你值得拥

        本文链接:https://www.haomeiwen.com/subject/bxwcbttx.html