【模組化程式設計】理解requireJS-實現一個簡單的模組載入器

葉小釵發表於2014-09-09

在前文中我們不止一次強調過模組化程式設計的重要性,以及其可以解決的問題:

① 解決單檔案變數命名衝突問題

② 解決前端多人協作問題

③ 解決檔案依賴問題

④ 按需載入(這個說法其實很假了)

⑤ ......

為了深入瞭解載入器,中間閱讀過一點requireJS的原始碼,但對於很多同學來說,對載入器的實現依舊不太清楚

事實上不通過程式碼實現,單單憑閱讀想理解一個庫或者框架只能達到一知半解的地步,所以今天便來實現一個簡單的載入器

載入器原理分析

分與合

事實上,一個程式執行需要完整的模組,以下程式碼為例:

 1   //求得績效係數
 2   var performanceCoefficient = function () {
 3     return 0.2;
 4   };
 5 
 6   //住房公積金計算方式
 7   var companyReserve = function (salary) {
 8     return salary * 0.2;
 9   };
10 
11   //個人所得稅
12   var incomeTax = function (salary) {
13     return salary * 0.2;
14   };
15 
16   //基本工資
17   var salary = 1000;
18 
19   //最終工資
20   var mySalary = salary + salary * performanceCoefficient();
21   mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary));
22   console.log(mySalary);

我一份完整的工資來說,公司會有績效獎勵,但是其演算法可能非常複雜,其中可能涉及到出勤率,完成度什麼的,這裡暫時不管

而有增便有減,所以我們會交住房公積金,也會扣除個人所得稅,最終才是我的工資

對於完整的程式來說上面的流程缺一不可,但是各個函式中卻有可能異常的複雜,跟錢有關係的東西都複雜,所以單單是公司績效便有可能超過1000行程式碼

於是我們這邊便會開始分:

 1 <script src="companyReserve.js" type="text/javascript"></script>
 2 <script src="incomeTax.js" type="text/javascript"></script>
 3 <script src="performanceCoefficient.js" type="text/javascript"></script>
 4 <script type="text/javascript">
 5 
 6   //基本工資
 7   var salary = 1000;
 8 
 9   //最終工資
10   var mySalary = salary + salary * performanceCoefficient();
11   mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary));
12   console.log(mySalary);
13 
14 </script>

上面的程式碼表明上是“分”開了,事實上也造成了“合”的問題,我要如何才能很好的把它們重新合到一起呢,畢竟其中的檔案可能還涉及到依賴,這裡便進入我們的require與define

require與define

事實上,上面的方案仍然是以檔案劃分,而不是以模組劃分的,若是檔名發生變化,頁面會涉及到改變,其實這裡應該有一個路徑的對映處理這個問題

var pathCfg = {
  'companyReserve': 'companyReserve',
  'incomeTax': 'incomeTax',
  'performanceCoefficient': 'performanceCoefficient'
};

於是我們一個模組便對應了一個路徑js檔案,剩下的便是將之對應模組的載入了,因為前端模組涉及到請求。所以這種寫法:

companyReserve = requile('companyReserve');

對於前端來說是不適用的,就算你在哪裡看到這樣做了,也一定是其中做了一些“手腳”,這裡我們便需要依據AMD規範了:

 1 require.config({
 2   'companyReserve': 'companyReserve',
 3   'incomeTax': 'incomeTax',
 4   'performanceCoefficient': 'performanceCoefficient'
 5 });
 6 
 7 require(['companyReserve', 'incomeTax', 'performanceCoefficient'], function (companyReserve, incomeTax, performanceCoefficient) {
 8   //基本工資
 9   var salary = 1000;
10 
11   //最終工資
12   var mySalary = salary + salary * performanceCoefficient();
13   mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary));
14   console.log(mySalary);
15 });

這裡便是一個標準的requireJS的寫法了,首先定義模組以及其路徑對映,其中定義依賴項

require(depArr, callback)

一個簡單完整的模組載入器基本就是這個樣子了,首先是一個依賴的陣列,其次是一個回撥,回撥要求依賴項全部載入才能執行,並且回撥的引數便是依賴項執行的結果,所以一般要求define模組具有一個返回值

方案有了,那麼如何實現呢?

實現方案

說到模組載入,人們第一反應都是ajax,因為無論何時,能拿到模組檔案的內容,都是模組化的基本,但是採用ajax的方式是不行的,因為ajax有跨域的問題

而模組化方案又不可避免的要處理跨域的問題,所以使用動態建立script標籤載入js檔案便成為了首選,但是,不使用ajax的方案,對於實現難度來說還是有要求

PS:我們實際工作中還會有載入html模板檔案的場景,這個稍候再說

通常我們是這樣做的,require作為程式入口,排程javascript資源,而載入到各個define模組後,各個模組便悄無聲息的建立script標籤載入

載入結束後便往require模組佇列報告自己載入結束了,當require中多有依賴模組皆載入結束時,便執行其回撥

原理大致如此,剩下的只是具體實現,而後在論證這個理論是否靠譜即可

載入器閹割實現

核心模組

根據以上理論,我們由整體來說,首先以入口三個基本函式來說

var require = function () {
};
require.config = function () {
};
require.define = function () {
};

這三個模組比不可少:

① config用以配置模組與路徑的對映,或者還有其他用處

② require為程式入口

③ define設計各個模組,響應require的排程

然後我們這裡會有一個建立script標籤的方法,並且會監聽其onLoad事件

④ loadScript

其次我們載入script標籤後,應該有一個全域性的模組物件,用於儲存已經載入好的模組,於是這裡提出了兩個需求:

⑤ require.moduleObj 模組儲存物件

⑥ Module,模組的建構函式

有了以上核心模組,我們形成了如下程式碼:

 1 (function () {
 2 
 3   var Module = function () {
 4     this.status = 'loading'; //只具有loading與loaded兩個狀態
 5     this.depCount = 0; //模組依賴項
 6     this.value = null; //define函式回撥執行的返回
 7   };
 8 
 9 
10   var loadScript = function (url, callback) {
11 
12   };
13 
14   var config = function () {
15 
16   };
17 
18   var require = function (deps, callback) {
19 
20   };
21 
22   require.config = function (cfg) {
23 
24   };
25 
26   var define = function (deps, callback) {
27 
28   };
29 
30 })();

於是接下來便是具體實現,然後在實現過程中補足不具備的介面與細節,往往在最後的實現與最初的設計沒有半毛錢關係......

程式碼實現

這塊最初實現時,本來想直接參考requireJS的實現,但是我們老大笑眯眯的拿出了一個他寫的載入器,我一看不得不承認有點妖

於是這裡便借鑑了其實現,做了簡單改造:

  1 (function () {
  2 
  3   //儲存已經載入好的模組
  4   var moduleCache = {};
  5 
  6   var require = function (deps, callback) {
  7     var params = [];
  8     var depCount = 0;
  9     var i, len, isEmpty = false, modName;
 10 
 11     //獲取當前正在執行的js程式碼段,這個在onLoad事件之前執行
 12     modName = document.currentScript && document.currentScript.id || 'REQUIRE_MAIN';
 13 
 14     //簡單實現,這裡未做引數檢查,只考慮陣列的情況
 15     if (deps.length) {
 16       for (i = 0, len = deps.length; i < len; i++) {
 17         (function (i) {
 18           //依賴加一
 19           depCount++;
 20           //這塊回撥很關鍵
 21           loadMod(deps[i], function (param) {
 22             params[i] = param;
 23             depCount--;
 24             if (depCount == 0) {
 25               saveModule(modName, params, callback);
 26             }
 27           });
 28         })(i);
 29       }
 30     } else {
 31       isEmpty = true;
 32     }
 33 
 34     if (isEmpty) {
 35       setTimeout(function () {
 36         saveModule(modName, null, callback);
 37       }, 0);
 38     }
 39 
 40   };
 41 
 42   //考慮最簡單邏輯即可
 43   var _getPathUrl = function (modName) {
 44     var url = modName;
 45     //不嚴謹
 46     if (url.indexOf('.js') == -1) url = url + '.js';
 47     return url;
 48   };
 49 
 50   //模組載入
 51   var loadMod = function (modName, callback) {
 52     var url = _getPathUrl(modName), fs, mod;
 53 
 54     //如果該模組已經被載入
 55     if (moduleCache[modName]) {
 56       mod = moduleCache[modName];
 57       if (mod.status == 'loaded') {
 58         setTimeout(callback(this.params), 0);
 59       } else {
 60         //如果未到載入狀態直接往onLoad插入值,在依賴項載入好後會解除依賴
 61         mod.onload.push(callback);
 62       }
 63     } else {
 64 
 65       /*
 66       這裡重點說一下Module物件
 67       status代表模組狀態
 68       onLoad事實上對應requireJS的事件回撥,該模組被引用多少次變化執行多少次回撥,通知被依賴項解除依賴
 69       */
 70       mod = moduleCache[modName] = {
 71         modName: modName,
 72         status: 'loading',
 73         export: null,
 74         onload: [callback]
 75       };
 76 
 77       _script = document.createElement('script');
 78       _script.id = modName;
 79       _script.type = 'text/javascript';
 80       _script.charset = 'utf-8';
 81       _script.async = true;
 82       _script.src = url;
 83 
 84       //這段程式碼在這個場景中意義不大,註釋了
 85       //      _script.onload = function (e) {};
 86 
 87       fs = document.getElementsByTagName('script')[0];
 88       fs.parentNode.insertBefore(_script, fs);
 89 
 90     }
 91   };
 92 
 93   var saveModule = function (modName, params, callback) {
 94     var mod, fn;
 95 
 96     if (moduleCache.hasOwnProperty(modName)) {
 97       mod = moduleCache[modName];
 98       mod.status = 'loaded';
 99       //輸出項
100       mod.export = callback ? callback(params) : null;
101 
102       //解除父類依賴,這裡事實上使用事件監聽較好
103       while (fn = mod.onload.shift()) {
104         fn(mod.export);
105       }
106     } else {
107       callback && callback.apply(window, params);
108     }
109   };
110 
111   window.require = require;
112   window.define = require;
113 
114 })();
View Code

首先這段程式碼有一些問題:

沒有處理引數問題,字串之類皆未處理

未處理迴圈依賴問題

未處理CMD寫法

未處理html模板載入相關

未處理引數配置,baseUrl什麼都沒有搞

基於此想實現打包檔案也不可能

......

但就是這100行程式碼,便是載入器的核心,程式碼很短,對各位理解載入器很有幫助,裡面有兩點需要注意:

① requireJS是使用事件監聽處理本身依賴,這裡直接將之放到了onLoad陣列中了

② 這裡有一個很有意思的東西

document.currentScript

這個可以獲取當前執行的程式碼段

requireJS是在onLoad中處理各個模組的,這裡就用了一個不一樣的實現,每個js檔案載入後,都會執行require(define)方法

執行後便取到當前正在執行的檔案,並且取到檔名載入之,正因為如此,連script的onLoad事件都省了......

demo實現

 1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head>
 3   <title></title>
 4 </head>
 5 <body>
 6 </body>
 7 <script src="require.js" type="text/javascript"></script>
 8 <script type="text/javascript">
 9   require(['util', 'math', 'num'], function (util, math, num) {
10 
11     num = math.getRadom() + '_' + num;
12     num = util.formatNum(num);
13     console.log(num);
14   });
15 </script>
16 </html>
1 //util
2 define([], function () {
3   return {
4     formatNum: function (n) {
5       if (n < 10) return '0' + n;
6       return n;
7     }
8   };
9 });
1 //math
2 define(['num'], function (num) {
3   return {
4     getRadom: function () {
5       return parseInt(Math.random() * num);
6     }
7   };
8 });
1 //num
2 define([], function () {
3   return 10;
4 });

小結

今天我們實現了一個簡單的模組載入器,通過他希望可以幫助各位瞭解requireJS或者seaJS,最後順利進入模組化程式設計的行列

相關文章