主流模塊規範

在es6之前,尚未提出一套官方的規範,從社區和框架推廣程度而言,目前通行的javascript模塊規範有兩種:CommonJS 和 AMDjavascript

CommonJS規範

2009年,美國程序員Ryan Dahl創造了node.js項目,將javascript語言用於服務器端編程。html

這標誌」Javascript模塊化編程」正式誕生。前端的複雜程度有限,沒有模塊也是能夠的,可是在服務器端,必定要有模塊,與操做系統和其餘應用程序互動,不然根本無法編程。前端

node編程中最重要的思想之一就是模塊,而正是這個思想,讓JavaScript的大規模工程成爲可能。模塊化編程在js界流行,也是基於此,隨後在瀏覽器端,requirejs和seajs之類的工具包也出現了,能夠說在對應規範下,require統治了ES6以前的全部模塊化編程,即便如今,在ES6 module被徹底實現以前,仍是這樣。java

在CommonJS中,暴露模塊使用module.exports和exports,不少人不明白暴露對象爲何會有兩個,後面會介紹區別node

在CommonJS中,有一個全局性方法require(),用於加載模塊。假定有一個數學模塊math.js,就能夠像下面這樣加載。jquery

1
var math = require('math');

而後,就能夠調用模塊提供的方法:程序員

1
2
  var math = require('math');
 math.add( 2,3); // 5

正是因爲CommonJS 使用的require方式的推進,纔有了後面的AMD、CMD 也採用的require方式來引用模塊的風格es6

AMD規範

有了服務器端模塊之後,很天然地,你們就想要客戶端模塊。並且最好二者可以兼容,一個模塊不用修改,在服務器和瀏覽器均可以運行。express

可是,因爲一個重大的侷限,使得CommonJS規範不適用於瀏覽器環境。仍是上一節的代碼,若是在瀏覽器中運行,會有一個很大的問題編程

1
2
var  math = require('math');
math.add(2, 3);

第二行math.add(2, 3),在第一行require(‘math’)以後運行,所以必須等math.js加載完成。也就是說,若是加載時間很長,整個應用就會停在那裏等。

 

這對服務器端不是一個問題,由於全部的模塊都存放在本地硬盤,能夠同步加載完成,等待時間就是硬盤的讀取時間。可是,對於瀏覽器,這倒是一個大問題,由於模塊都放在服務器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於」假死」狀態。
所以,瀏覽器端的模塊,不能採用」同步加載」(synchronous),只能採用」異步加載」(asynchronous)。這就是AMD規範誕生的背景。

AMD是」Asynchronous Module Definition」的縮寫,意思就是」異步模塊定義」。它採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。全部依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成以後,這個回調函數纔會運行。

 

模塊必須採用特定的define()函數來定義。

1
define(id?, dependencies?, factory)
  • id:字符串,模塊名稱(可選)
  • dependencies: 是咱們要載入的依賴模塊(可選),使用相對路徑。,注意是數組格式
  • factory: 工廠方法,返回一個模塊函數

若是一個模塊不依賴其餘模塊,那麼能夠直接定義在define()函數之中。

1
2
3
4
5
6
7
8
9
// math.js
  define( function (){
     var add = function (x,y){
       return x+y;
    };
     return {
       add: add
    };
  });

若是這個模塊還依賴其餘模塊,那麼define()函數的第一個參數,必須是一個數組,指明該模塊的依賴性。

1
2
3
4
5
6
7
8
define([ 'Lib'], function(Lib){
     function foo(){
      Lib.doSomething();
    }
     return {
       foo : foo
    };
  });

當require()函數加載上面這個模塊的時候,就會先加載Lib.js文件。

 

AMD也採用require()語句加載模塊,可是不一樣於CommonJS,它要求兩個參數:

1
require([module], callback);

第一個參數[module],是一個數組,裏面的成員就是要加載的模塊;第二個參數callback,則是加載成功以後的回調函數。若是將前面的代碼改寫成AMD形式,就是下面這樣:

1
2
3
require(['math'], function (math) {
  math.add(2, 3);
});

math.add()與math模塊加載不是同步的,瀏覽器不會發生假死。因此很顯然,AMD比較適合瀏覽器環境。

目前,主要有兩個Javascript庫實現了AMD規範:require.jscurl.js

 

CMD規範

CMD (Common Module Definition), 是seajs推崇的規範,CMD則是依賴就近,用的時候再require。它寫起來是這樣的:

1
2
3
4
define( function(require, exports, module) {
var clock = require('clock') ;
clock.start() ;
}) ;

CMD與AMD同樣,也是採用特定的define()函數來定義,用require方式來引用模塊

1
define(id?, dependencies?, factory)
  • id:字符串,模塊名稱(可選)
  • dependencies: 是咱們要載入的依賴模塊(可選),使用相對路徑。,注意是數組格式
  • factory: 工廠方法,返回一個模塊函數
1
2
3
4
5
define( 'hello', ['jquery'], function(require, exports, module) {
 
// 模塊代碼
 
});

若是一個模塊不依賴其餘模塊,那麼能夠直接定義在define()函數之中。

1
2
3
define( function(require, exports, module) {
// 模塊代碼
});

注意:帶 id 和 dependencies 參數的 define 用法不屬於 CMD 規範,而屬於 Modules/Transport 規範。

 

 

CMD與AMD區別

AMD和CMD最大的區別是對依賴模塊的執行時機處理不一樣,而不是加載的時機或者方式不一樣,兩者皆爲異步加載模塊。

AMD依賴前置,js能夠方便知道依賴模塊是誰,當即加載;

而CMD就近依賴,須要使用把模塊變爲字符串解析一遍才知道依賴了那些模塊,這也是不少人詬病CMD的一點,犧牲性能來帶來開發的便利性,實際上解析模塊用的時間短到能夠忽略。

現階段的標準

ES6標準發佈後,module成爲標準,標準使用是以export指令導出接口,以import引入模塊,可是在咱們一向的node模塊中,咱們依然採用的是CommonJS規範,使用require引入模塊,使用module.exports導出接口。

export導出模塊

export語法聲明用於導出函數、對象、指定文件(或模塊)的原始值。

注意:在node中使用的是exports,不要混淆了

export有兩種模塊導出方式:命名式導出(名稱導出)默認導出(定義式導出),命名式導出每一個模塊能夠多個,而默認導出每一個模塊僅一個。

1
2
3
4
5
6
7
8
9
10
11
12
13
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // also var
export let name1 = …, name2 = …, …, nameN; // also var, const
 
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
 
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
  • name1… nameN-導出的「標識符」。導出後,能夠經過這個「標識符」在另外一個模塊中使用import引用
  • default-設置模塊的默認導出。設置後import不經過「標識符」而直接引用默認導入
  • -繼承模塊並導出繼承模塊全部的方法和屬性
  • as-重命名導出「標識符」
  • from-從已經存在的模塊、腳本文件…導出
  • 命名式導入

    咱們能夠經過指定名稱,就是將這些成員插入到看成用域中。導出時,能夠導入單個成員或多個成員:

    注意,花括號裏面的變量與export後面的變量一一對應

    1
    2
    import {myMember} from "my-module";
    import {foo, bar} from "my-module";

    經過*符號,咱們能夠導入模塊中的所有屬性和方法。當導入模塊所有導出內容時,就是將導出模塊(’my-module.js’)全部的導出綁定內容,插入到當前模塊(’myModule’)的做用域中:

    1
    import * as myModule from "my-module";

    導入模塊對象時,也可使用as對導入成員重命名,以方便在當前模塊內使用:

    1
    import {reallyReallyLongModuleMemberName as shortName} from "my-module";

    導入多個成員時,一樣可使用別名:

    1
    import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module";

    導入一個模塊,但不進行任何綁定:

    1
    import "my-module";

    默認導入

    在模塊導出時,可能會存在默認導出。一樣的,在導入時可使用import指令導出這些默認值。

    直接導入默認值:

    1
    import myDefault from "my-module";

    也能夠在命名空間導入和名稱導入中,同時使用默認導入:

    1
    2
    3
    4
    import myDefault, * as myModule from "my-module"; // myModule 作爲命名空間使用
     
    import myDefault, {foo, bar} from "my-module"; // 指定成員導入

    import使用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // --file.js--
    function getJSON(url, callback) {
    let xhr = new XMLHttpRequest();
    xhr.onload =  function () {
    callback( this.responseText)
    };
    xhr.open( "GET", url, true);
    xhr.send();
    }
     
    export function getUsefulContents(url, callback) {
    getJSON(url,  data => callback(JSON.parse(data)));
    }
     
    // --main.js--
    import { getUsefulContents } from "file";
    getUsefulContents( "http://itbilu.com", data => {
    doSomethingUseful(data);
    });

    default關鍵字

    1
    2
    3
    4
    5
    6
    // d.js
    export default function() {}
     
    // 等效於:
    function a() {};
    export {a as default};

    在import的時候,能夠這樣用:

    1
    2
    3
    4
    import a from './d';
     
    // 等效於,或者說就是下面這種寫法的簡寫,是同一個意思
    import {default as a} from './d';

    這個語法糖的好處就是import的時候,能夠省去花括號{}。

    簡單的說,若是import的時候,你發現某個變量沒有花括號括起來(沒有*號),那麼你在腦海中應該把它還原成有花括號的as語法。

    因此,下面這種寫法你也應該理解了吧:

    1
    import $,{each,map} from 'jquery';

    import後面第一個$是{defalut as $}的替代寫法。

    as關鍵字

    as簡單的說就是取一個別名,export中能夠用,import中其實能夠用:

    1
    2
    3
    4
    5
    6
    7
    // a.js
    var a = function() {};
    export {a  as fun};
     
    // b.js
    import {fun as a} from './a';
    a();

    上面這段代碼,export的時候,對外提供的接口是fun,它是a.js內部a這個函數的別名,可是在模塊外面,認不到a,只能認到fun。

    import中的as就很簡單,就是你在使用模塊裏面的方法的時候,給這個方法取一個別名,好在當前的文件裏面使用。之因此是這樣,是由於有的時候不一樣的兩個模塊可能經過相同的接口,好比有一個c.js也經過了fun這個接口:

    1
    2
    // c.js
    export function  fun() {};

    若是在b.js中同時使用a和c這兩個模塊,就必須想辦法解決接口重名的問題,as就解決了。

    CommonJS中module.exports 與 exports的區別

    Module.exports

    The module.exports object is created by the Module system. Sometimes this is not acceptable; many want their module to be an instance of some class. To do this, assign the desired export object to module.exports. Note that assigning the desired object to exports will simply rebind the local exports variable, which is probably not what you want to do.

    譯文:module.exports對象是由模塊系統建立的。 有時這是難以接受的;許多人但願他們的模塊成爲某個類的實例。 爲了實現這個,須要將指望導出的對象賦值給module.exports。 注意,將指望的對象賦值給exports會簡單地從新綁定到本地exports變量上,這可能不是你想要的。

    Module.exports

    The exports variable is available within a module’s file-level scope, and is assigned the value of module.exports before the module is evaluated. It allows a shortcut, so that module.exports.f = … can be written more succinctly as exports.f = …. However, be aware that like any variable, if a new value is assigned to exports, it is no longer bound to module.exports:

    譯文:exports變量是在模塊的文件級別做用域內有效的,它在模塊被執行前被賦於 module.exports 的值。它有一個快捷方式,以便 module.exports.f = … 能夠被更簡潔地寫成exports.f = …。 注意,就像任何變量,若是一個新的值被賦值給exports,它就再也不綁定到module.exports(實際上是exports.屬性會自動掛載到沒有命名衝突的module.exports.屬性)

    Api文檔上面的能夠看出,從require導入方式去理解,關鍵有兩個變量(全局變量module.exports,局部變量exports)、一個返回值(module.exports)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function require(...) {
    var module = { exports: {} };
    ((module, exports) => {
    // 你的被引入代碼 Start
    // var exports = module.exports = {}; (默認都有的)
    function some_func() {};
    exports = some_func;
    // 此時,exports再也不掛載到module.exports,
    // export將導出{}默認對象
    module.exports = some_func;
    // 此時,這個模塊將導出some_func對象,覆蓋exports上的some_func
    // 你的被引入代碼 End
    })( module, module.exports);
    // 不論是 exports仍是module.exports,最後返回的仍是module.exports
    return module.exports;
    }

    demo.js:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    console.log(exports); // {}
    console.log(module.exports); // {}
    console.log(exports === module.exports); // true
    console.log(exports == module.exports); // true
    console.log(module);
    /**
    Module {
    id:  '.',
    exports: {},
    parent:  null,
    filename:  '/Users/larben/Desktop/demo.js',
    loaded:  false,
    children: [],
    paths:
    '/Users/larben/Desktop/node_modules',
    '/Users/larben/node_modules',
    '/Users/node_modules',
    '/node_modules' ] }
    * /

    注意

      1. 每一個js文件一建立,都有一個var exports = module.exports = {},使exports和module.exports都指向一個空對象。
      2. module.exports和exports所指向的內存地址相同
相關文章
相關標籤/搜索