iT邦幫忙

DAY 7
4

Javascript面面觀系列 第 7

Javascript面面觀:核心篇《模式-lazy load》

lazy load就是只載入精簡的核心,其他的程式到需要時才動態載入。這個主題其實上http://ajaxpatterns.org網站,找一下On-Demand Javascript就可以看到詳細的說明以及許多的解決方法。所以我的就做做參考吧...

要動態載入外部的Javascript檔案,一般有兩種作法,一種使用DOM一種使用ajax。兩種方法各有優缺點,所以要使用哪個方法就看需要囉。
使用ajax + eval
使用ajax的方式,就是用ajax把Javascript檔案內容以字串的方式取得,然後透過eval()函數來執行。

以下是一個簡單的例子(loader.js,為了行文方便,我都是取loader.js中部份程式,但是本文中的方法可以組成一個完整的程式來用):

(function(){
	var window = this;
	var document = window.document;
	var _xmlhttp = function() {
		var ajax = function() {
			return new XMLHttpRequest();
		}();
		return function(){return ajax;};
	}();
	$ = {};
	$.read = function (path) {
		try {
			var loader = _xmlhttp();
			loader.open("GET", path, false);
			loader.send(null);
			if (loader.readyState == 4) {
				if (loader.status == 200) {
					return loader.responseText;
				}
			} else {
				throw "ERROR: " + loader.status;
			}
		} catch (e) {
			alert(e);
		}
	};
	$.require = function (path, callbacks) {
		try {
			if (window.execScript) {//for IE
				window.execScript(this.read(path));
			} else {
				window.eval.call(window, this.read(path));
			}
		} catch(e) {
			alert(e);
		}
		if (callbacks) {
			callbacks.call(window);
		}
	};
})();

(為了偷懶,我沒有用非同步的方式跑ajax,如果要用非同步的方式跑,那需要用onreadystatechange事件來處理)

loader.js會加入一個函數叫做$.require,用來動態載入Javascript。第一個參數Javascript檔案的路徑,第二個則是要同時執行的程式(必須是函數)。使用eval有一個特殊的限制,就是他的execution context是由執行時的execution context決定的,但是載入的Javascript應該都會在Global執行,所以做了特別處理。另外,IE的eval()函數無法這樣改,所以只好用他的execScript函數來做。

接下來是網頁:

<script src="js/loader.js"></script>


<div id="target"></div>
<script>
	$.require(
		'iron016.js',
		function(a, b){
			return function(){
				append(a, b);
			};
		}('target', 'it changed.')
	);
</script>
<input type="button" value="test" onclick="append('target','it should be.')">

最後是動態載入的程式碼(iron016.js):

function append(id, content){
	document.getElementById(id).innerHTML = content;
}

使用ajax來動態載入Javascript有一個特殊限制,就是ajax需要遵守same origin規則,所以程式只能來自同一個domain。如果想要動態載入在不同domain上的javascript,那就只好使用下一節介紹的DOM載入方式。

使用DOM
使用dom的方式,簡單地說就是在DOM中間加入一個script element node,src設為你要加入的script路徑。使用DOM的方式動態加入Javascript會碰到一個問題,就是把這個script element node附加到DOM tree的操作完成時,瀏覽器還需要去伺服器把script下載完畢才能用,這樣會有時間差,如果接下來要直接利用到動態加入的程式碼就可能發生錯誤。

解決方式是把要立刻執行的程式傳給負責動態載入的程式,然後把這個要執行的程式改到script element node的onload或onreadystatechange事件處理函數中執行。IE的onload事件要在文件載入時才會觸發,所以必須使用onreadystatechange事件來處理。以下是一個簡單的例子:

首先是用來做動態載入的程式碼(loader.js):

(function(){
	var window = this;
	var document = window.document;
	var head = document.getElementsByTagName('head')[0];
	if (!head) {
		head = document.createElement('head');
		document.ownerDocument.insertBefore('body', head);
	}
	$ = {};
	$.include = function(path, callbacks) {
		var node = document.createElement('script');
		node.type = "text/javascript";
		node.src = path;
		head.appendChild(node);
		if (node.attachEvent) {//it's IE
			var f = function() {
				if (node.readyState == 'loaded') {
					if (callbacks) {
						callbacks.call(window);
						node.detachEvent('onreadystatechange',f);
					}
					head.removeChild(node);
					delete node;
					delete callbacks;
				}
			};
			node.attachEvent('onreadystatechange', f);
		} else {
			var f = function() {
				if (callbacks) {
					callbacks.call(window);
					node.removeEventListener('load',f,false);
				}
				head.removeChild(node);
				delete node;
				delete callbacks
			};
			node.addEventListener('load',f,false);
		}
	};
})();

網頁中要把載入的方法改成$.include,動態載入的程式碼(iron016.js)則與上例相同。

loader.js會加入一個$.include函數來動態載入其他script,第一個參數是script path,第二個參數是要接著執行的程式,在上例中必須是一個函數。用DOM的方式做載入,就不會有相同domain的限制,使用起來也很方便。

script element node在載入完成後,javascript程式就會加到javascript的環境中,所以把script element node刪除也不會影響程式的執行。

重複載入
如果重複載入之前已載入的Javascript,其實是在浪費時間,要避免重複載入,可以用一個簡單的方法來做管理,就是在載入時做一個簡單的確認。所以我在loader.js裡面加上一段程式:

Array.prototype.find = function(v) {
	var i = 0;
	for (;i < this.length; i++) {
		if (this[i] ==v) {
			return true;
		}
	}
	return false;
};
var loaded = [];

然後在動態載入的函數開頭,加入判斷:

$.include = function(path, callbacks) {
	if (!loaded.find(path)) {
		loaded.push(path);
		....
	} else {
		if (callbacks) {
			callbacks.call(window);
		}
	}
};

另外一個函數也做相同的處理:

$.require = function(path, callbacks) {
	if (!loaded.find(path)) {
		loaded.push(path);
		....
	} else {
		if (callbacks) {
			callbacks.call(window);
		}
	}
};

已載入的程式,就會把路徑加入loaded變數,這樣就可以用這個變數來判斷是否有動態載入過這個javascript。

管理dependency
如果動態載入的東西更龐大,那就可能會碰到dependency的問題,光是使用之前的方式還會有所不足,這個時候就必須要用dependency管理的方式來解決。

常見的方法是在套件中宣告每個要載入程式的先備條件,也就是需要先載入哪些程式。例如載入c.js之前,必須先載入a.js跟b.js,那可以這樣子定義:

var deps = {
  'c.js': ['a.js', 'b.js']
};

取出要先預先載入的程式後,就可以先透過前面說的loaded變數過濾,然後再這些函數動態載入,最後處理真正要載入的程式。


上一篇
Javascript面面觀:核心篇《Web worker-多執行緒模型》
下一篇
Javascript面面觀:核心篇《模式-observer》
系列文
Javascript面面觀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
fillano
iT邦超人 1 級 ‧ 2009-10-19 11:33:22

補充一下,因為關鍵字new ActiveXObject似乎會被擋掉,所以我把產生ajax物件的範例程式刪掉一大塊使用MSXML的部份。

我要留言

立即登入留言