模塊加載器

最近在作新項目的時候本身利用一點業餘時間寫了一個簡單的js模塊加載器。後來由於用了webpack就沒有考慮把它放到項目裏面去,也沒有繼續更新它了。模塊加載器開源的有不少,通常來講seaJS和reqiureJS都能知足基本需求。本篇博文主要分享一下滷煮寫這個加載器的一些想法和思路,做爲學習的記錄。javascript

js模塊化加載已經不是一個新鮮概念了,不少人都一再強調,大型項目要使用模塊化開發,由於一旦隨着項目的增大,管理和組織代碼的難度會愈來愈難,使得咱們對代碼的管理變得重要起來。固然,在後端模塊化已經至關成熟,而做爲前端的模塊化概念,是好久以後才提出來的。模塊化好處是使得代碼結構更加清晰,高的內聚,功能獨立,複用等等。在服務端,隨着nodejs 的興起,js模塊化被愈來愈多地引發人們的注意。可是對於後端和前端來講,最大的區別就是同步和異步加載的問題,由於服務器上獲取模塊是不須要花費不少的,模塊加載進來的時間就操做系統文件的時間,這個過程能夠當作是同步的。而在瀏覽器的前端卻須要發送請求到服務器來獲取文件,這致使了一個異步延遲的問題,針對這個問題,以AMD規範的異步模塊加載器requireJS應運而生。前端

加載原理

以上簡單介紹了一下前端模塊化的歷程,下面主要介紹一下模塊加載主要原理:java

1. createElement('script')和appendChild(script) 動態建立腳本,添加到head元素中。node

2. fn.toString().match(/\.require\((\"|\')[^\)]*(\"|\')\)/g) 將模塊轉換爲字符串,而後經過正則表達式,匹配每一個模塊中的的依賴文件。webpack

3. 創建腳本加載隊列。git

4.遞歸加載,分析完依賴以後,咱們須要按照依賴出現的位置,將它們加載到客戶端。github

5.爲每個命名的模塊創建緩存,即 module[name] = callback; web

6.currentScript : 對於匿名模塊,經過currentScript 來獲取文件名,存入到緩存中。正則表達式

下面貼出對應主要的代碼:後端

1、動態建立腳本

建立腳本較爲簡單,主要是用createElement方法和appendChild。在建立腳本函數中,咱們須要爲該腳本綁定一個onload事件,這個事件是爲了通知加載腳本隊列執行的時間,告訴它何時能夠加載下一個js文件了。

function _createScript(url) {
	//建立script
	var script = doc.createElement('script');
	var me = this;
	//設置屬性爲異步加載
	script.async = true;
	script.src = url + '.js';
	//爲腳本添加加載完成事件
	if ('onload' in script) {
		script.onload = function(event) {
			return _scriptLoaded.call(me, script);
		};
	} else {
		script.onreadystatechange = function() {
			if (/loaded|complete/.test(node.readyState)) {
				me.next();
				_scriptLoaded(script);
			}
		};
	}
	//加入script
	head.appendChild(script);
}

2、分析依賴創建

分析依賴是模塊加載器中最重要的環節之一。每一個模塊可能會依賴不一樣的模塊,咱們須要理清楚這些模塊之間的依賴關係,而後分別將它們加載進來。爲了分析依賴關係,咱們使用toString的方法,將模塊轉化爲一個string,而後去其中尋找依賴。

function _analyseDepend(func) {
	//匹配依賴,全部在.reqiure()括號內的依賴都會被匹配出來。
	var firstReg = /\.require\((\"|\')[^\)]*(\"|\')\)/g,
		secondReg = /\((\"|\')[^\)]*(\"|\')\)/g,
		lastReplaceRge = /\((\"|\')|(\"|\')\)/g;
	//將模塊字符串化
	var string = func.toString();
	var allFiles = string.match(firstReg);
	var newArr = [];
	if (!allFiles) {
		return '';
	}
	//將依賴的文件名存入一個堆棧內
	allFiles.map(function(v) {
  //對文件名作處理 var m = v.match(secondReg)[0].replace(lastReplaceRge, ''); //只有在異步加載的狀況下須要 返回解析依賴 if(!modules[_analyseName(m)]) { newArr.push(m); } }); if(newArr.length > 0) { return newArr; }else{ return '' } }

3、創建腳本加載隊列

分析完依賴以後,咱們能夠獲得一個腳本名稱的棧,咱們從其中獲取腳本名稱,依次按照順序地加載它們。由於每一個腳本加載過程都是異步的,因此,咱們須要有一個異步加載機制。在這裏,咱們使用了設計模式中的職責鏈條模式來完成整個異步加載過程。經過在onload事件通知隊列加載的完成狀況。下面是職責鏈模式的實現代碼

function _Chain() {
	this.cache = [];
}
/**
 * add function to order stack
 * @param func (func)
 * @returns {_Chain}
 */
_Chain.prototype.after = function(fn) {
		this.cache.push(fn);
		this.cur = 0;
		return this;
	}
	/**
	 * To pass the authority to next function excute
	 * @param 
	 * @returns
	 */
_Chain.prototype.passRequest = function() {
		var result = 'continue';
		while (this.cur < this.cache.length && result === 'continue') {
			result = this.cache[this.cur++].apply(this, arguments);
			if (this.cur === this.cache.length) {
				this.clear();
			}
		}
	}
	/**
	 * an api to excute func in stack
	 * @param 
	 * @returns 
	 */
_Chain.prototype.next = function() {
		this.excute();
	}
	/**
	 * let use to excute those function
	 * @param 
	 * @returns
	 */
_Chain.prototype.excute = function() {
	this.passRequest.apply(this, arguments)
}

/**
 * to clear stack all function
 * @param 
 * @returns
 */
_Chain.prototype.clear = function() {
	this.cache = [];
	this.cur = 0;
}

var excuteChain = new _Chain();

每一個腳本加載完畢後調用next函數,能夠通知職責鏈中的下一個函數繼續執行,這樣解決了異步加載問題。這裏將模式的實現代碼放到模塊加載器中是不太合適的,通常狀況下咱們能夠將它獨立出來,放入公共模塊當中,爲其餘的模塊共同使用。但這裏純粹是一個單文件的項目,因此就暫時將它放入此處。

4、遞歸加載

根據模塊中的依賴出現的次序,依次加載各個模塊。

function _excuteRequire(depends) {
	if (depends.length === 0) {
		var u = excuteStack.length;
		while (u--) {
			var params = excuteStack[u]();
			if (u === 0) {
				Events.trigger('excute', params);
				excuteStack = [];
			}
		}
	}
}

5、爲模塊創建緩存對象

 

//在文件加載完畢後將模塊存入緩存
return modules[string] = func();

6、currentScript

currentScript主要是用來解決獲取那些未命名的模塊的js文件名,如 define(function(){})這樣的模塊是匿名的,咱們經過這個方法能夠獲取正在執行的腳本文件名,從而爲其創建緩存。

function _getCurrentScript() {
		//取得正在解析的script節點
		if (doc.currentScript) {
			//firefox 4+
			return doc.currentScript;
		}
	}

7、定義module

最後咱們須要作的事給出定義模塊的方法,通常狀況下定義方法主要分如下幾種:

1.define('a', function(){})

2.define(function(){})

第一種是命名的模塊,第二種是未命名的模塊,咱們須要對它們分別處理。用typeof方法分析參數,創建以string方法爲基礎的加載模式:

function define() {
	var arg = Array.prototype.slice.call(arguments);
	var paramType = Object.prototype.toString.call(arg[0]).split(' ')[1].replace(/\]/, '');
	defineParamObj[paramType].apply(null, arg);
	// Chain.excute();
}

function _String(string, func) {
	string = _analyseName(string);
	//分析依賴
	var depends = _analyseDepend(func) || [];
	// 將加載好的模塊存入緩存
	excuteStack.push(function() {
		return modules[string] = func();
	});
	//執行加載依賴函數
	_excuteRequire(depends);
	for (var i = 0, l = depends.length; i < l; i++) {
		(function(i) {
			excuteChain.after(function() {
				var c = require(depends[i]);
				if(c) {
					this.next();
				};
			});
		})(i);
	}
}

function _Function(func) {
	var name = _analyseName(_getCurrentScript().src);
	_String(name, func);
}

結束

以上就是一個實現模塊加載器的主要原理,滷煮寫完發現也只有四百行的代碼,實現了最基本的模塊加載功能。固然,其中還有不少細節沒有實現,比起大而全的requireJs來講,只是一個小兒科而已。可是明白了主要這幾項後,對於咱們來講就足夠理解一個模塊加載器的實現方式了。代碼存入github上: https://github.com/constantince/require

相關文章
相關標籤/搜索