1、簡介
隨着前端業務復雜度的增加,模塊化成為一個大的趨勢。而在ES6還未被瀏覽器所支持的情況下,commonjs作為ES6中標准模塊加載方案,在客服端中的支持情況並不好,現在在客服端中有2中模塊化的解決方案,CMD和AMD,他們的代表分別為seajs和requirejs。這篇文章主要介紹我對commonjs、AMD以及CMD的理解。
2、commonJS
commonjs的目標是制定一個js模塊化的標准,它的目標制定一個可以同時在客服端和服務端運行的模塊。這些模塊擁有自己獨立的作用域,也可以向頂層曝露出自己的api也就是module.exports。在ES6中common被制定為標准。但是在ES6還未被瀏覽器完美支持的情況下,commonjs規范之能在服務端發揮它的作用。比如在nodejs和webpack等中。而我們服務端的異步加載模塊主要有2種加載方案,CMD和AMD,這兩種規范的典型是seajs和rjs(requirejs)。這兩種方案雖然都是加載模塊的解決方案,但是還是有一些的差別。
我們先來了解下commonjs規范。
此規范指出了如何編寫可以在同類模塊系統中所共用的模塊,這類模塊系統可以同時在客戶端和服務端,以安全的或者不安全的方式已經被實現了或者通過語法擴展可以被未來的系統所支持。這些模塊需要提供頂級作用域的私有性,並提供從其他模塊導入單例對象到自身並且可以導出自身API的能力。含蓄的說,這個規范定義了如果一個模塊系統要支持共用模塊,那么它需要提供的最少的功能特性。
模塊上下文
- 在一個模塊中,存在一個自由的變量"require",它是一個函數。
- 這個"require"函數接收一個模塊標識符。
- "require"返回外部模塊所輸出的API。
- 如果出現依賴閉環(dependency cycle),那么外部模塊在被它的傳遞依賴(transitive dependencies)所require的時候可能並沒有執行完成;在這種情況下,"require"返回的對象必須至少包含此外部模塊在調用require函數(會進入當前模塊執行環境)之前就已經准備完畢的輸出。
- 如果請求的模塊不能返回,那么"require"必須拋出一個錯誤。
- 在一個模塊中,會存在一個名為"exports"的自由變量,它是一個對象,模塊可以在執行的時候把自身的API加入到其中。
- 模塊必須使用"exports"對象來做為輸出的唯一表示。
如果不理解請移步CommonJS Modules/1.0 規范 & AMD 規范中文版 這里面會有具體的例子。
從模塊的上下文可以可以看出,這只是制定了一種模塊的方式,在模塊內部用require函數去獲取我們所需要的API,用exports拋出當前模塊的API。這其實也就是我們node的模塊方式,沒錯,node就是commonjs的實踐者,commonjs只是一種規范,而nodejs就是實踐這個規范。同樣的,ES6中,也執行了commonjs規范,但是介於我們瀏覽器還不能完美的支持ES6,所以我們也不能用這種很cool的方式。(注解:當然babel是一個完美的工具,把ES6轉成目前支持的es5。但是最終在瀏覽器中執行的代碼還是es5。所以我們直接編寫的ES6代碼,只能通過webpack等打包工具,把模塊之間的依賴通過打包工具來實現。)
既然無法在瀏覽器中使用ES6,那么我們如和在瀏覽器中去執行模塊依賴加載的呢?這里有2中方案去實現瀏覽器下模塊和依賴加載,CMD和AMD,這兩種方案的典型是seajs和requirejs。下面我們先分析AMD
3、AMD規范
AMD是為了彌補commonjs規范在瀏覽器中目前無法支持ES6的一種解決方案。異步模塊定義規范(AMD)制定了定義模塊的規則,這樣模塊和模塊的依賴可以被異步加載。這和瀏覽器的異步加載模塊的環境剛好適應(瀏覽器同步加載模塊會導致性能、可用性、調試和跨域訪問等問題)。
這個規范只定義define函數。
define(id?,dependencies?,factory);
其中id和dependencies不是必須的。
模塊的格式:
模塊名用來唯一標識定義中模塊,它們同樣在依賴數組中使用。AMD的模塊名規范是CommonJS模塊名規范的超集。引用如下:
- 模塊名是由一個或多個單詞以正斜杠為分隔符拼接成的字符串
- 單詞須為駝峰形式,或者".",".."
- 模塊名不允許文件擴展名的形式,如".js"
- 模塊名可以為 "相對的" 或 "頂級的"。如果首字符為"."或".."則為"相對的"模塊名
- 頂級的模塊名從根命名空間的概念模塊解析
- 相對的模塊名從 "require" 書寫和調用的模塊解析
上文引用的CommonJS模塊id屬性常被用於JavaScript模塊。
相對模塊名解析示例:
- 如果模塊
"a/b/c"
請求"../d"
, 則解析為"a/d"
- 如果模塊
"a/b/c"
請求"./e"
, 則解析為"a/b/e"
如果AMD的實現支持加載器插件(Loader-Plugins),則"!"符號用於分隔加載器插件模塊名和插件資源名。由於插件資源名可以非常自由地命名,大多數字符都允許在插件資源名使用。
define.amd (Object)用來標識有amd模塊加載器的存在
例子:
// 創建一個名為"alpha"的模塊,使用了require,exports,和名為"beta"的模塊: define("alpha", ["require", "exports", "beta"], function (require, exports, beta) { exports.verb = function() { return beta.verb(); //Or: return require("beta").verb(); } }); //一個返回對象的匿名模塊: define(["alpha"], function (alpha) { return { verb: function(){ return alpha.verb() + 2; } }; }); //一個沒有依賴性的模塊可以直接定義對象: define({ add: function(x, y){ return x + y; } });
// 一個使用了簡單CommonJS轉換的模塊定義: define(function (require, exports, module) { var a = require('a'), b = require('b'); exports.action = function () {}; });
4、AMD規范的實踐者requirejs
requirejs是AMD規范的實踐者,RequireJS 是一個JavaScript模塊加載器。它非常適合在瀏覽器中使用,但它也可以用在其他腳本環境, 就像 Rhino and Node. 使用RequireJS加載模塊化腳本將提高代碼的加載速度和質量。
在使用requirejs時,可以查看官方文檔
requirejs非常簡單,我們只需要定義在頁面加載的時候,引入requirejs並且,把mainjs指定在data-main中,在mainjs中引入我們的requirejs.config和我們需要用到的頁面js,requirejs會根據我們的模塊去加載相應的依賴,然后執行代碼。
// 頁面引入 <script data-main="main" src="./amdjs/require.js"></script> // 模塊,這里使用AMD定義模塊的方式,例如,定義一個模塊module1 define('module1', ['zepto'], function($) { console.log('this is module1') }) //mainjs內容 require.config({ baseUrl: 'amdjs/modules', paths: { main: 'amdjs/main' zepto: 'http://zeptojs.com/zepto.min' }, shim: { }, waitSeconds: 15 }); // 你的模塊 requirejs(['module1'],function($) { console.log('load success!') })
config文件里面有許多參數,這里我把常用的解釋下,具體的請查看requirejs文檔。
baseUrl:所有模塊的查找根路徑。
paths :path映射那些不直接放置於baseUrl下的模塊名。設置path時起始位置是相對於baseUrl的,除非該path設置以"/"開頭或含有URL協議(如http:)。
shim: 為那些沒有使用define()來聲明依賴關系、設置模塊的"瀏覽器全局變量注入"型腳本做依賴和導出配置。shim配置僅設置了代碼的依賴關系,想要實際加載shim指定的或涉及的模塊,仍然需要一個常規的require/define調用。設置shim本身不會觸發代碼的加載。
deps: 指定要加載的一個依賴數組。當將require設置為一個config object在加載require.js之前使用時很有用。一旦require.js被定義,這些依賴就已加載。使用deps就像調用require([]),但它在loader處理配置完畢之后就立即生效。它並不阻塞其他的require()調用,它僅是指定某些模塊作為config塊的一部分而異步加載的手段而已。
5、CMD規范
cmd是commonjs另外的一種模塊加載方案,這個規范本身偏向於commonjs的規范。他以一個文件就是一個模塊和ES6中標准的commonjs規范類似。
它定義了以個define(factory)函數。define
接受 factory
參數,factory
可以是一個函數,也可以是一個對象或字符串。
如果factory為函數時,它有三個參數:require,exports,module。
define.cmd是一個cmd模塊加載器的標識
require方法:同步加載模塊
define(function(require, exports) { // 獲取模塊 a 的接口 var a = require('./a'); // 調用模塊 a 的方法 a.doSomething(); });
require.async:用來在模塊的內部異步加載模塊,並且完成后執行指定回掉。
define(function(require, exports, module) { // 異步加載一個模塊,在加載完成時,執行回調 require.async('./b', function(b) { b.doSomething(); }); // 異步加載多個模塊,在加載完成時,執行回調 require.async(['./c', './d'], function(c, d) { c.doSomething(); d.doSomething(); }); });
require.resolve:使用模塊系統內部的路徑解析機制來解析並返回模塊路徑。該函數不會加載模塊,只返回解析后的絕對路徑。
define(function(require, exports) { console.log(require.resolve('./b')); // ==> http://example.com/path/to/b.js });
exports:
是一個對象,用來向外提供模塊接口。
define(function(require) { // 通過 return 直接提供接口 return { foo: 'bar', doSomething: function() {} }; });
module.exports:模塊暴露的出口
define(function(require, exports, module) { // 正確寫法 module.exports = { foo: 'bar', doSomething: function() {} }; });
6、CMD規范的實踐者seaJS
seajs和requirejs的加載方式類似,在頁面引入seajs文件后,加載seajs.config,並且之后加載mainjs。詳細信息請查看
加載seajs
// 引入seajs <script src="../sea-modules/seajs/seajs/2.2.0/sea.js"></script> // 配置文件 <script> // Set configuration seajs.config({ base: "../sea-modules/", alias: { "jquery": "jquery/jquery/1.10.1/jquery.js" } }); // 入口 // For development if (location.href.indexOf("?dev") > 0) { seajs.use("../static/hello/src/main"); } // For production else { seajs.use("examples/hello/1.0.0/main"); } </script>
API:
seajs.config: 配置信息
seajs.config({ // 設置路徑,方便跨目錄調用 paths: { 'arale': 'https://a.alipayobjects.com/arale', 'jquery': 'https://a.alipayobjects.com/jquery' }, // 設置別名,方便調用 alias: { 'class': 'arale/class/1.0.0/class', 'jquery': 'jquery/jquery/1.10.1/jquery' } });
更多的詳細配置選項請參考頁面:seajs config詳細
seajs.use:頁面加載一個或者多個模塊
// 加載一個模塊 seajs.use('./a'); // 加載一個模塊,在加載完成時,執行回調 seajs.use('./a', function(a) { a.doSomething(); }); // 加載多個模塊,在加載完成時,執行回調 seajs.use(['./a', './b'], function(a, b) { a.doSomething(); b.doSomething(); });
這里值得一提的是seajs.use的用法,它替代了AMD中require(【】,factory)的用法。
define:用來定義模塊。
define(function(require, exports, module) { // 模塊代碼 });
require: 用來獲取指定模塊的接口。
define(function(require) { // 獲取模塊 a 的接口 var a = require('./a'); // 調用模塊 a 的方法 a.doSomething(); });
require.async:用來在模塊內部異步加載一個或多個模塊。
define(function(require) { // 異步加載一個模塊,在加載完成時,執行回調 require.async('./b', function(b) { b.doSomething(); }); // 異步加載多個模塊,在加載完成時,執行回調 require.async(['./c', './d'], function(c, d) { c.doSomething(); d.doSomething(); }); });
exports: 用來在模塊內部對外提供接口
define(function(require, exports) { // 對外提供 foo 屬性 exports.foo = 'bar'; // 對外提供 doSomething 方法 exports.doSomething = function() {}; });
module.exports:與 exports
類似,用來在模塊內部對外提供接口
define(function(require, exports, module) { // 對外提供接口 module.exports = { name: 'a', doSomething: function() {}; }; });
seajs API具體的用法,請參考seajs API
7、CMD和AMD之間的差異
1)AMD(異步加載模塊),CMD(通用模塊),AMD是需要通過異步加載的形式把依賴加載進來,然而CMD在require依賴的時候,可以通過同步的形式(require),也可以通過異步的形式(require.async)。當然AMD也可以通過特殊的寫法支持CMD,但是不推崇。
2)CMD 推崇依賴就近,AMD 推崇依賴前置。在AMD中,我們需要把依賴前置在依賴數組中。而在cmd中,我們只需要在使用這個模塊前,把依賴的模塊require進來。
3)設計理念不一樣,在 SeaJS 里,API 的設計理念是:
- 保持簡單,職責單一。
- 遵守規范,但不拘泥。
- 適度靈活
requirejs中,require的用法多樣,比如:
require('a') -- gets exports of module a
require(['a']) -- fetch module a according to module name scheme
require(['a.js']) -- fetch a.js directly relative to current page
require({...}) -- set loader config
4)聚焦點有差異
seajs專注於瀏覽器環境下的模塊加載,而requirejs集成了在node環境以及Rhino 環境下的代碼,這導致requirejs比seajs更大。
參考文獻:
CommonJS Modules/1.0 規范 & AMD 規范中文版
注:本人見識短淺,有不夠准確的地方萬望指正。拜謝!