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

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

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的个人博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,718评论 2 17
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,679评论 0 9
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,581评论 18 399
  • 第一章: JS简介 从当初简单的语言,变成了现在能够处理复杂计算和交互,拥有闭包、匿名函数, 甚至元编程等...
    LaBaby_阅读 1,640评论 0 6
  • 通篇都是孩提时代。对于一个出生在80年代末的人来说90年代就是他的童年。那时候的事情是说不完的,就回忆二三事吧。 ...
    欣如止水66阅读 218评论 0 0