最近在作新項目的時候本身利用一點業餘時間寫了一個簡單的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 來獲取文件名,存入到緩存中。正則表達式
下面貼出對應主要的代碼:後端
建立腳本較爲簡單,主要是用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); }
分析依賴是模塊加載器中最重要的環節之一。每一個模塊可能會依賴不一樣的模塊,咱們須要理清楚這些模塊之間的依賴關係,而後分別將它們加載進來。爲了分析依賴關係,咱們使用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 '' } }
分析完依賴以後,咱們能夠獲得一個腳本名稱的棧,咱們從其中獲取腳本名稱,依次按照順序地加載它們。由於每一個腳本加載過程都是異步的,因此,咱們須要有一個異步加載機制。在這裏,咱們使用了設計模式中的職責鏈條模式來完成整個異步加載過程。經過在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函數,能夠通知職責鏈中的下一個函數繼續執行,這樣解決了異步加載問題。這裏將模式的實現代碼放到模塊加載器中是不太合適的,通常狀況下咱們能夠將它獨立出來,放入公共模塊當中,爲其餘的模塊共同使用。但這裏純粹是一個單文件的項目,因此就暫時將它放入此處。
根據模塊中的依賴出現的次序,依次加載各個模塊。
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 = []; } } } }
//在文件加載完畢後將模塊存入緩存 return modules[string] = func();
currentScript主要是用來解決獲取那些未命名的模塊的js文件名,如 define(function(){})這樣的模塊是匿名的,咱們經過這個方法能夠獲取正在執行的腳本文件名,從而爲其創建緩存。
function _getCurrentScript() { //取得正在解析的script節點 if (doc.currentScript) { //firefox 4+ return doc.currentScript; } }
最後咱們須要作的事給出定義模塊的方法,通常狀況下定義方法主要分如下幾種:
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