最近在做新專案的時候自己利用一點業餘時間寫了一個簡單的js模組載入器。後來因為用了webpack就沒有考慮把它放到專案裡面去,也沒有繼續更新它了。模組載入器開源的有很多,一般來說seaJS和reqiureJS都能滿足基本需求。本篇博文主要分享一下滷煮寫這個載入器的一些想法和思路,作為學習的記錄。
js模組化載入已經不是一個新鮮概念了,很多人都一再強調,大型專案要使用模組化開發,因為一旦隨著專案的增大,管理和組織程式碼的難度會越來越難,使得我們對程式碼的管理變得重要起來。當然,在後端模組化已經相當成熟,而作為前端的模組化概念,是很久之後才提出來的。模組化好處是使得程式碼結構更加清晰,高的內聚,功能獨立,複用等等。在服務端,隨著nodejs 的興起,js模組化被越來越多地引起人們的注意。但是對於後端和前端來說,最大的區別就是同步和非同步載入的問題,因為伺服器上獲取模組是不需要花費很多的,模組載入進來的時間就作業系統檔案的時間,這個過程可以看成是同步的。而在瀏覽器的前端卻需要傳送請求到伺服器來獲取檔案,這導致了一個非同步延遲的問題,針對這個問題,以AMD規範的非同步模組載入器requireJS應運而生。
載入原理
以上簡單介紹了一下前端模組化的歷程,下面主要介紹一下模組載入主要原理:
1. createElement(‘script’)和appendChild(script) 動態建立指令碼,新增到head元素中。
2. fn.toString().match(/.require((“|’)[^)]*(“|’))/g) 將模組轉換為字串,然後通過正規表示式,匹配每個模組中的的依賴檔案。
3. 建立指令碼載入佇列。
4.遞迴載入,分析完依賴之後,我們需要按照依賴出現的位置,將它們載入到客戶端。
5.為每一個命名的模組建立快取,即 module[name] = callback;
6.currentScript : 對於匿名模組,通過currentScript 來獲取檔名,存入到快取中。
下面貼出對應主要的程式碼:
一、動態建立指令碼
建立指令碼較為簡單,主要是用createElement方法和appendChild。在建立指令碼函式中,我們需要為該指令碼繫結一個onload事件,這個事件是為了通知載入指令碼佇列執行的時間,告訴它什麼時候可以載入下一個js檔案了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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); } |
二、分析依賴建立
分析依賴是模組載入器中最重要的環節之一。每個模組可能會依賴不同的模組,我們需要理清楚這些模組之間的依賴關係,然後分別將它們載入進來。為了分析依賴關係,我們使用toString的方法,將模組轉化為一個string,然後去其中尋找依賴。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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 '' } } |
三、建立指令碼載入佇列
分析完依賴之後,我們可以得到一個指令碼名稱的棧,我們從其中獲取指令碼名稱,依次按照順序地載入它們。因為每個指令碼載入過程都是非同步的,所以,我們需要有一個非同步載入機制。在這裡,我們使用了設計模式中的職責鏈條模式來完成整個非同步載入過程。通過在onload事件通知佇列載入的完成情況。下面是職責鏈模式的實現程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
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函式,可以通知職責鏈中的下一個函式繼續執行,這樣解決了非同步載入問題。這裡將模式的實現程式碼放到模組載入器中是不太合適的,一般情況下我們可以將它獨立出來,放入公共模組當中,為其他的模組共同使用。但這裡純粹是一個單檔案的專案,所以就暫時將它放入此處。
四、遞迴載入
根據模組中的依賴出現的次序,依次載入各個模組。
1 2 3 4 5 6 7 8 9 10 11 12 |
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 = []; } } } } |
五、為模組建立快取物件
1 2 |
//在檔案載入完畢後將模組存入快取 return modules[string] = func(); |
六、currentScript
currentScript主要是用來解決獲取那些未命名的模組的js檔名,如 define(function(){})這樣的模組是匿名的,我們通過這個方法可以獲取正在執行的指令碼檔名,從而為其建立快取。
1 2 3 4 5 6 7 |
function _getCurrentScript() { //取得正在解析的script節點 if (doc.currentScript) { //firefox 4+ return doc.currentScript; } } |
七、定義module
最後我們需要做的事給出定義模組的方法,一般情況下定義方法主要分以下幾種:
1.define(‘a’, function(){})
2.define(function(){})
第一種是命名的模組,第二種是未命名的模組,我們需要對它們分別處理。用typeof方法分析引數,建立以string方法為基礎的載入模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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