【模塊化編程】理解requireJS-實現一個簡單的模塊加載器

在前文中咱們不止一次強調過模塊化編程的重要性,以及其能夠解決的問題:javascript

① 解決單文件變量命名衝突問題html

② 解決前端多人協做問題前端

③ 解決文件依賴問題java

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

⑤ ......算法

爲了深刻了解加載器,中間閱讀過一點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);

我一份完整的工資來講,公司會有績效獎勵,可是其算法可能很是複雜,其中可能涉及到出勤率,完成度什麼的,這裏暫時無論app

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

對於完整的程序來講上面的流程缺一不可,可是各個函數中卻有可能異常的複雜,跟錢有關係的東西都複雜,因此單單是公司績效便有可能超過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,最後順利進入模塊化編程的行列

相關文章
相關標籤/搜索