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]

		console.log(funcName in base);
		
		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

閱讀原文:http://www.jianshu.com/p/7ca43eb55f4e


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM