js模塊化歷程
無模塊時代
這個時候并沒有前端工程師,服務端工程師只需在頁面上隨便寫寫js就能搞定需求。那個時候的前端代碼大概像這樣:
if (xx) { //....... } else { //xxxxxxxxxxx } for (var i = 0; i < 10; i++) { //........ } element.onclick = function() { //....... }
模塊萌芽時代
b.js依賴a.js,標簽的書寫順序必須是
<script type="text/javascript" src="a.js"></script><script type="text/javascript" src="b.js"></script>
1. 用自執行函數來包裝代碼
modA = function() { var a, b; //變量a、b外部不可見 return { add: function(c) { a + b + c; }, format: function() { //...... } } }()
為了避免全局變量造成的沖突,人們想到或許可以用多級命名空間來進行管理,于是,代碼就變成了這個風格:
app.util.modA = xxx; app.tools.modA = xxx; app.tools.modA.format = xxx;
Yahoo的YUI早期就是這么做的,調用的時候不得不這么寫:
app.tools.modA.format();
3. jQuery風格的匿名自執行函數
(function(window){ //代碼 window.jQuery = window.$ = jQuery;//通過給window添加屬性而暴漏到全局 })(window);
模塊化面臨什么問題
源自nodejs的規范CommonJs
1. 模塊的標識應遵循的規則(書寫規范)2. 定義全局函數require,通過傳入模塊標識來引入其他模塊,執行的結果即為別的模塊暴漏出來的API3. 如果被require函數引入的模塊中也包含依賴,那么依次加載這些依賴4. 如果引入模塊失敗,那么require函數應該報一個異常5. 模塊通過變量exports來向往暴漏API,exports只能是一個對象,暴漏的API須作為此對象的屬性。
遵循commonjs規范的代碼看起來是這樣的:(來自官方的例子)
//math.js exports.add = function() { var sum = 0, i = 0, args = arguments, l = args.length; while (i < l) { sum += args[i++]; } return sum; }; //increment.js var add = require('math') .add; exports.increment = function(val) { return add(val, 1); }; //program.js var inc = require('increment').increment;var a = 1; inc(a); // 2
服務端向前端進軍
1. 全局有一個module變量,用來定義模塊2. 通過module.declare方法來定義一個模塊3. module.declare方法只接收一個參數,那就是模塊的factory,次factory可以是函數也可以是對象,如果是對象,那么模塊輸出就是此對象。4. 模塊的factory函數傳入三個參數:require,exports,module,用來引入其他依賴和導出本模塊API5. 如果factory函數最后明確寫有return數據(js函數中不寫return默認返回undefined),那么return的內容即為模塊的輸出。
使用該規范的例子看起來像這樣:
//可以使用exprots來對外暴漏APImodule.declare(function(require, exports, module) { exports.foo = "bar"; }); //也可以直接return來對外暴漏數據 module.declare(function(require) { return { foo: "bar" }; });
AMD/RequireJs的崛起與妥協
1. 用全局函數define來定義模塊,用法為:define(id?, dependencies?, factory);2. id為模塊標識,遵從CommonJS Module Identifiers規范3. dependencies為依賴的模塊數組,在factory中需傳入形參與之一一對應4. 如果dependencies的值中有"require"、"exports"或"module",則與commonjs中的實現保持一致5. 如果dependencies省略不寫,則默認為["require", "exports", "module"],factory中也會默認傳入require,exports,module6. 如果factory為函數,模塊對外暴漏API的方法有三種:return任意類型的數據、exports.xxx=xxx、module.exports=xxx7. 如果factory為對象,則該對象即為模塊的返回值
基于以上幾點基本規范,我們便可以用這樣的方式來進行模塊化組織代碼了:
//a.js define(function(){ console.log('a.js執行'); return { hello: function(){ console.log('hello, a.js'); } } }); //b.js define(function(){ console.log('b.js執行'); return { hello: function(){ console.log('hello, b.js'); } } }); //main.js require(['a', 'b'], function(a, b){ console.log('main.js執行'); a.hello(); $('#b').click(function(){ b.hello(); }); })
b.js執行
main.js執行
hello, a.js
另一點被吐槽的是,在定義模塊的時候,要把所有依賴模塊都羅列一遍,而且還要在factory中作為形參傳進去,要寫兩遍很大一串模塊名稱,像這樣:
define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){ ..... })
好的一點是,AMD保留了commonjs中的require、exprots、module這三個功能(上面提到的第4條)。你也可以不把依賴羅列在dependencies數組中。而是在代碼中用require來引入,如下:
define(function(){ console.log('main2.js執行'); require(['a'], function(a){ a.hello(); }); $('#b').click(function(){ require(['b'], function(b){ b.hello(); }); }); });
a.js執行
hello, a.js
在AMD的陣營中,也有一部分人提出這樣的觀點,代碼里寫一堆回調實在是太惡心了,他們更喜歡這樣來使用模塊:
var a = require('a'); a.hello(); $('#b').click(function(){ var b = require('b'); b.hello(); });
作了此兼容后,使用requirejs就可以這么寫代碼了:
//d.js define(function(require, exports, module){ console.log('d.js執行'); return { helloA: function(){ var a = require('a'); a.hello(); }, run: function(){ $('#b').click(function(){ var b = require('b'); b.hello(); }); } } });
我們把上面的代碼命名為d.js,在別的地方使用它:
require(['d'], function(d){ });
b.js執行
d.js執行
兼容并包的CMD/seajs
既然requirejs有上述種種不甚優雅的地方,所以必然會有新東西來完善它,這就是后起之秀seajs,seajs的作者是國內大牛淘寶前端布道者玉伯。seajs全面擁抱Modules/Wrappings規范,不用requirejs那樣回調的方式來編寫模塊。而它也不是完全按照Modules/Wrappings規范,seajs并沒有使用declare來定義模塊,而是使用和requirejs一樣的define,或許作者本人更喜歡這個名字吧。(然而這或多或少又會給人們造成理解上的混淆),用seajs定義模塊的寫法如下:
//a.js define(function(require, exports, module){ console.log('a.js執行'); return { hello: function(){ console.log('hello, a.js'); } } }); //b.js define(function(require, exports, module){ console.log('b.js執行'); return { hello: function(){ console.log('hello, b.js'); } } }); //main.js define(function(require, exports, module){ console.log('main.js執行'); var a = require('a'); a.hello(); $('#b').click(function(){ var b = require('b'); b.hello(); }); });
a.js執行
hello, a.js
hello, b.js
如果你一定要挑出一點不爽的話,那就是b.js的預先下載了。你可能不太想一開始就下載好所有的資源,希望像requirejs那樣,等點擊按鈕的時候再開始下載b.js。本著兼容并包的思想,seajs也實現了這一功能,提供require.async API,在點擊按鈕的時候,只需這樣寫:
var b = require.async('b'); b.hello();
面向未來的ES6模塊標準
定義一個模塊不需要專門的工作,因為一個模塊的作用就是對外提供API,所以只需用exoprt導出就可以了:
//方式一, a.js export var a = 1; export var obj = {name: 'abc', age: 20}; export function run(){....} //方式二, b.js var a = 1;var obj = {name: 'abc', age: 20};function run(){....} export {a, obj, run}
使用模塊的時候用import關鍵字,如:
import {run as go} from 'a'run()
如果想要使用模塊中的全部API,也可以不必把每個都列一遍,使用module關鍵字可以全部引入,用法:
module foo from 'a'console.log(foo.obj); a.run();