模塊(一) CommonJs,AMD, CMD, UMD

系列文章導(dǎo)航 模塊(二) es6 module typescript module

本文參考
Javascript模塊化編程(一):模塊的寫法
Javascript模塊化編程(二):AMD規(guī)范
Javascript模塊化編程(三):require.js的用法

隨著網(wǎng)站逐漸變成"互聯(lián)網(wǎng)應(yīng)用程序",嵌入網(wǎng)頁的Javascript代碼越來越龐大,越來越復(fù)雜。網(wǎng)頁越來越像桌面程序,需要一個團(tuán)隊分工協(xié)作、進(jìn)度管理、單元測試等等......開發(fā)者不得不使用軟件工程的方法,管理網(wǎng)頁的業(yè)務(wù)邏輯。Javascript模塊化編程,已經(jīng)成為一個迫切的需求。理想情況下,開發(fā)者只需要實現(xiàn)核心的業(yè)務(wù)邏輯,其他都可以加載別人已經(jīng)寫好的模塊。但是,Javascript不是一種模塊化編程語言,它不支持"類"(class),更遑論"模塊"(module)了。(正在制定中的ECMAScript標(biāo)準(zhǔn)第六版,將正式支持"類"和"模塊",但還需要很長時間才能投入實用。)Javascript社區(qū)做了很多努力,在現(xiàn)有的運行環(huán)境中,實現(xiàn)"模塊"的效果。本文總結(jié)了當(dāng)前"Javascript模塊化編程"的最佳實踐,說明如何投入實用。雖然這不是初級教程,但是只要稍稍了解Javascript的基本語法,就能看懂。

一、原始寫法

模塊就是實現(xiàn)特定功能的一組方法。只要把不同的函數(shù)(以及記錄狀態(tài)的變量)簡單地放在一起,就算是一個模塊。

  function m1(){
    //...
  }
  function m2(){
    //...
  }

上面的函數(shù)m1()和m2(),組成一個模塊。使用的時候,直接調(diào)用就行了。這種做法的缺點很明顯:"污染"了全局變量,無法保證不與其他模塊發(fā)生變量名沖突,而且模塊成員之間看不出直接關(guān)系。

二、對象寫法

為了解決上面的缺點,可以把模塊寫成一個對象,所有的模塊成員都放到這個對象里面。

  var module1 = new Object({
    _count : 0,
    m1 : function (){
      //...
    },
    m2 : function (){
      //...
    }
  });

上面的函數(shù)m1()和m2(),都封裝在module1對象里。使用的時候,就是調(diào)用這個對象的屬性。module1.m1();但是,這樣的寫法會暴露所有模塊成員,內(nèi)部狀態(tài)可以被外部改寫。比如,外部代碼可以直接改變內(nèi)部計數(shù)器的值。module1._count = 5;

其實這種寫法在ES5中又叫命名空間,參考 JS命名空間的使用

var MYNAMESPACE = MYNAMESPACE || {};

MYNAMESPACE.person = function(name) {
    this.name = name;
};

MYNAMESPACE.person.prototype.getName = function() {
    return this.name;
};

// 使用方法
var p = new MYNAMESPACE.person("doc");
p.getName();

在沒有類似 AMD 這樣的模塊化的格式和規(guī)范時,在前端的 JavaScript 中,命名空間是是一個非常棒的實踐,用于避免全局變量污染以及用于組織代碼模塊的邏輯性,擴(kuò)展性,可讀性和可維護(hù)性。通常選擇一個頂級的應(yīng)用命名空間并且這個命名空間(即對象)是唯一一個掛載到全局對象上的對象(在瀏覽器中是 window,在 Node.js 應(yīng)用中是 global)。當(dāng)選擇了應(yīng)用的頂級命名空間,嵌套命名空間可被用于一個模塊化的架構(gòu)。例如,考慮下面:

var myMasterNS = myMasterNS || {};
myMasterNS.mySubNS = myMasterNS.mySubNS || {};
myMasterNS.mySubNS.someFunction = function(){
 //插入邏輯 
};

這里我們聲明了一個名為 myMasterNS 的簡單全局對象,又分配了一個子命名空間對象作為原來命名空間的一個屬性,這個例子中是 mySubNS。現(xiàn)在我們能夠在頂級命名空間下實現(xiàn)任何所需的功能,并且任何后面的子命名空間都不會污染全局作用域。請注意,這不是使用 JavaScript 實現(xiàn)命名空間的唯一模式。我在這個例子中選擇它,是因為這種模式的簡單和可讀性。

三、立即執(zhí)行函數(shù)寫法

使用"立即執(zhí)行函數(shù)"(Immediately-Invoked Function Expression,IIFE),可以達(dá)到不暴露私有成員的目的。

  var module1 = (function(){
    var _count = 0;
    var m1 = function(){
      //...
    };
    var m2 = function(){
      //...
    };
    return {
      m1 : m1,
      m2 : m2
    };
  })();

使用上面的寫法,外部代碼無法讀取內(nèi)部的_count變量。console.info(module1._count); //undefined
module1就是Javascript模塊的基本寫法。下面,再對這種寫法進(jìn)行加工。

四、放大模式

如果一個模塊很大,必須分成幾個部分,或者一個模塊需要繼承另一個模塊,這時就有必要采用"放大模式"(augmentation)。

  var module1 = (function (mod){
    mod.m3 = function () {
      //...
    };
    return mod;
  })(module1);

上面的代碼為module1模塊添加了一個新方法m3(),然后返回新的module1模塊。

五、寬放大模式(Loose augmentation)

在瀏覽器環(huán)境中,模塊的各個部分通常都是從網(wǎng)上獲取的,有時無法知道哪個部分會先加載。如果采用上一節(jié)的寫法,第一個執(zhí)行的部分有可能加載一個不存在空對象,這時就要采用"寬放大模式"。

  var module1 = ( function (mod){
    //...
    return mod;
  })(window.module1 || {});

與"放大模式"相比,"寬放大模式"就是"立即執(zhí)行函數(shù)"的參數(shù)可以是空對象。

六、輸入全局變量

獨立性是模塊的重要特點,模塊內(nèi)部最好不與程序的其他部分直接交互。為了在模塊內(nèi)部調(diào)用全局變量,必須顯式地將其他變量輸入模塊。

  var module1 = (function ($, YAHOO) {
    //...

  })(jQuery, YAHOO);

上面的module1模塊需要使用jQuery庫和YUI庫,就把這兩個庫(其實是兩個模塊)當(dāng)作參數(shù)輸入module1。這樣做除了保證模塊的獨立性,還使得模塊之間的依賴關(guān)系變得明顯。這方面更多的討論,參見Ben Cherry的著名文章《JavaScript Module Pattern: In-Depth》。

七、模塊的規(guī)范

先想一想,為什么模塊很重要?因為有了模塊,我們就可以更方便地使用別人的代碼,想要什么功能,就加載什么模塊。但是,這樣做有一個前提,那就是大家必須以同樣的方式編寫模塊,否則你有你的寫法,我有我的寫法,豈不是亂了套!考慮到Javascript模塊現(xiàn)在還沒有官方規(guī)范,這一點就更重要了。目前,通行的Javascript模塊規(guī)范共有兩種:CommonJSAMD。我主要介紹AMD,但是要先從CommonJS講起。

八、CommonJS

2009年,美國程序員Ryan Dahl創(chuàng)造了node.js項目,將javascript語言用于服務(wù)器端編程。這標(biāo)志"Javascript模塊化編程"正式誕生。因為老實說,在瀏覽器環(huán)境下,沒有模塊也不是特別大的問題,畢竟網(wǎng)頁程序的復(fù)雜性有限;但是在服務(wù)器端,一定要有模塊,與操作系統(tǒng)和其他應(yīng)用程序互動,否則根本沒法編程。

node.js的模塊系統(tǒng),就是參照CommonJS規(guī)范實現(xiàn)的。在CommonJS中,有一個全局性方法require(),用于加載模塊。假定有一個數(shù)學(xué)模塊math.js,就可以像下面這樣加載。

var math = require('math');

然后,就可以調(diào)用模塊提供的方法:

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

關(guān)于CommonJs后文詳述

九、瀏覽器環(huán)境

有了服務(wù)器端模塊以后,很自然地,大家就想要客戶端模塊。而且最好兩者能夠兼容,一個模塊不用修改,在服務(wù)器和瀏覽器都可以運行。但是,由于一個重大的局限,使得CommonJS規(guī)范不適用于瀏覽器環(huán)境。還是上一節(jié)的代碼,如果在瀏覽器中運行,會有一個很大的問題,你能看出來嗎?

  var math = require('math');

  math.add(2, 3);

第二行math.add(2, 3),在第一行require('math')之后運行,因此必須等math.js加載完成。也就是說,如果加載時間很長,整個應(yīng)用就會停在那里等。這對服務(wù)器端不是一個問題,因為所有的模塊都存放在本地硬盤,可以同步加載完成,等待時間就是硬盤的讀取時間。但是,對于瀏覽器,這卻是一個大問題,因為模塊都放在服務(wù)器端,等待時間取決于網(wǎng)速的快慢,可能要等很長時間,瀏覽器處于"假死"狀態(tài)。因此,瀏覽器端的模塊,不能采用"同步加載"(synchronous),只能采用"異步加載"(asynchronous)。這就是AMD規(guī)范誕生的背景。

十、AMD

AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"。它采用異步方式加載模塊,模塊的加載不影響它后面語句的運行。所有依賴這個模塊的語句,都定義在一個回調(diào)函數(shù)中,等到加載完成之后,這個回調(diào)函數(shù)才會運行。AMD也采用require()語句加載模塊,但是不同于CommonJS,它要求兩個參數(shù):

require([module], callback);

第一個參數(shù)[module],是一個數(shù)組,里面的成員就是要加載的模塊;第二個參數(shù)callback,則是加載成功之后的回調(diào)函數(shù)。如果將前面的代碼改寫成AMD形式,就是下面這樣:

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

math.add()與math模塊加載不是同步的,瀏覽器不會發(fā)生假死。所以很顯然,AMD比較適合瀏覽器環(huán)境。目前,主要有兩個Javascript庫實現(xiàn)了AMD規(guī)范:require.jscurl.js。本系列的第三部分,將通過介紹require.js,進(jìn)一步講解AMD的用法,以及如何將模塊化編程投入實戰(zhàn)。

十一、為什么要用require.js?

最早的時候,所有Javascript代碼都寫在一個文件里面,只要加載這一個文件就夠了。后來,代碼越來越多,一個文件不夠了,必須分成多個文件,依次加載。下面的網(wǎng)頁代碼,相信很多人都見過。

<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<script src="4.js"></script>
<script src="5.js"></script>
<script src="6.js"></script>

這段代碼依次加載多個js文件。這樣的寫法有很大的缺點。首先,加載的時候,瀏覽器會停止網(wǎng)頁渲染,加載文件越多,網(wǎng)頁失去響應(yīng)的時間就會越長;其次,由于js文件之間存在依賴關(guān)系,因此必須嚴(yán)格保證加載順序(比如上例的1.js要在2.js的前面),依賴性最大的模塊一定要放到最后加載,當(dāng)依賴關(guān)系很復(fù)雜的時候,代碼的編寫和維護(hù)都會變得困難。require.js的誕生,就是為了解決這兩個問題:
(1)實現(xiàn)js文件的異步加載,避免網(wǎng)頁失去響應(yīng);
(2)管理模塊之間的依賴性,便于代碼的編寫和維護(hù)。

十二、require.js的加載

使用require.js的第一步,是先去官方網(wǎng)站下載最新版本。下載后,假定把它放在js子目錄下面,就可以加載了。

<script src="js/require.js"></script>

有人可能會想到,加載這個文件,也可能造成網(wǎng)頁失去響應(yīng)。解決辦法有兩個,一個是把它放在網(wǎng)頁底部加載,另一個是寫成下面這樣:

<script src="js/require.js" defer async="true" ></script>

async屬性表明這個文件需要異步加載,避免網(wǎng)頁失去響應(yīng)。IE不支持這個屬性,只支持defer,所以把defer也寫上。加載require.js以后,下一步就要加載我們自己的代碼了。假定我們自己的代碼文件是main.js,也放在js目錄下面。那么,只需要寫成下面這樣就行了:

<script src="js/require.js" data-main="js/main"></script>

data-main屬性的作用是,指定網(wǎng)頁程序的主模塊。在上例中,就是js目錄下面的main.js,這個文件會第一個被require.js加載。由于require.js默認(rèn)的文件后綴名是js,所以可以把main.js簡寫成main。

十三、require.js主模塊的寫法

上一節(jié)的main.js,我把它稱為"主模塊",意思是整個網(wǎng)頁的入口代碼。它有點像C語言的main()函數(shù),所有代碼都從這兒開始運行。下面就來看,怎么寫main.js。如果我們的代碼不依賴任何其他模塊,那么可以直接寫入javascript代碼。

  // main.js
  alert("加載成功!");

但這樣的話,就沒必要使用require.js了。真正常見的情況是,主模塊依賴于其他模塊,這時就要使用AMD規(guī)范定義的的require()函數(shù)。

  // main.js
  require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // some code here
  });

require()函數(shù)接受兩個參數(shù)。第一個參數(shù)是一個數(shù)組,表示所依賴的模塊,上例就是['moduleA', 'moduleB', 'moduleC'],即主模塊依賴這三個模塊;第二個參數(shù)是一個回調(diào)函數(shù),當(dāng)前面指定的模塊都加載成功后,它將被調(diào)用。加載的模塊會以參數(shù)形式傳入該函數(shù),從而在回調(diào)函數(shù)內(nèi)部就可以使用這些模塊。require()異步加載moduleA,moduleB和moduleC,瀏覽器不會失去響應(yīng);它指定的回調(diào)函數(shù),只有前面的模塊都加載成功后,才會運行,解決了依賴性的問題。
更多細(xì)節(jié)參考
http://www.requirejs.cn
【JavaScript】RequireJS模塊化之HelloWorld

十四、CMD

參考
淺析JS模塊規(guī)范:AMD,CMD,CommonJS
AMD 和 CMD 的區(qū)別有哪些?
AMD雖然實現(xiàn)了異步加載,但是開始就把所有依賴寫出來是不符合書寫的邏輯順序的,能不能像commonJS那樣用的時候再require,而且還支持異步加載后再執(zhí)行呢?CMD (Common Module Definition), 是seajs推崇的規(guī)范,CMD則是依賴就近,用的時候再require。它寫起來是這樣的:

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

AMD和CMD最大的區(qū)別是對依賴模塊的執(zhí)行時機(jī)處理不同,而不是加載的時機(jī)或者方式不同,二者皆為異步加載模塊。
AMD依賴前置,js可以方便知道依賴模塊是誰,立即加載;而CMD就近依賴,需要使用把模塊變?yōu)樽址馕鲆槐椴胖酪蕾嚵四切┠K,這也是很多人詬病CMD的一點,犧牲性能來帶來開發(fā)的便利性,實際上解析模塊用的時間短到可以忽略。

十五、UMD

UMD是AMD和CommonJS的糅合。AMD模塊以瀏覽器第一的原則發(fā)展,異步加載模塊。CommonJS模塊以服務(wù)器第一原則發(fā)展,選擇同步加載,它的模塊無需包裝(unwrapped modules)。這迫使人們又想出另一個更通用的模式UMD (Universal Module Definition)。希望解決跨平臺的解決方案。

UMD先判斷是否支持Node.js的模塊(exports)是否存在,存在則使用Node.js模塊模式。在判斷是否支持AMD(define是否存在),存在則使用AMD方式加載模塊。

(function (window, factory) {
    if (typeof exports === 'object') {
     
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
     
        define(factory);
    } else {
     
        window.eventUtil = factory();
    }
})(this, function () {
    //module ...
});
十六、JavaScript模塊化編程探索

CommonJS團(tuán)隊定義了module格式來解決JavaScript作用域問題,這樣確保了每一個module都在自己的命名空間下執(zhí)行。根據(jù)CommonJS的規(guī)范,每個文件就是一個模塊,有自己的作用域。在一個文件里面定義的變量、函數(shù)、類,都是私有的,對其他文件不可見。CommonJS規(guī)范規(guī)定,每個模塊內(nèi)部,module變量代表當(dāng)前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,其實是加載該模塊的module.exports屬性。CommonJS給出2個工具來實現(xiàn)模塊之間的依賴:

  • require() 用于在當(dāng)前作用域引入已有的模塊
  • module object 用于從當(dāng)前作用域?qū)С鲆恍〇|東

那就先搞一個Hello world的小栗子來試下吧!新建一個項目文件夾吧,雖然項目很小。。。起名commonjs,在里邊新建2個JavaScript文件,分別命名為world.js和salute.js,代碼如下:

// salute.js 打招呼
var MySalute = "Hello";
module.exports = MySalute;

/*注意上下是分別寫在2個文件js文件里哦*/
// world.js
var MySalute = require("./salute");
var Result = MySalute + " world!";
console.log(Result);

然后無知的我有新建了一個demo.html,(想要在瀏覽器里打開看看是什么樣子)內(nèi)容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script type="text/javascript" src="world.js"></script>
</head>
<body>
    
</body>
</html>

結(jié)果在瀏覽器中打開,查看控制臺大失所望,報了一個錯誤world.js:2 Uncaught ReferenceError: require is not defined
發(fā)現(xiàn)瀏覽器不兼容CommonJS的根本原因,在于缺少四個Node.js環(huán)境的變量:

  • module
  • exports
  • require
  • global

只要能夠提供這四個變量,瀏覽器就能加載 CommonJS 模塊,問題是可以解決的,但是好像并不怎么好玩,有興趣的朋友可以去阮老師博客里逛逛啊,瀏覽器加載 CommonJS 模塊的原理與實現(xiàn),里面還講了Browserify的原理。

這里使用命令行node world.js就可以了。

十七、Node.js中的模塊和包

參考《Node.js開發(fā)指南 ByVoid》 Page34

模塊(Module)和包(Package)是 Node.js 最重要的支柱。開發(fā)一個具有一定規(guī)模的程序不可能只用一個文件,通常需要把各個功能拆分、封裝,然后組合起來,模塊正是為了實現(xiàn)這種方式而誕生的。在瀏覽器 JavaScript 中,腳本模塊的拆分和組合通常使用 HTML 的script 標(biāo)簽來實現(xiàn)。Node.js 提供了 require 函數(shù)來調(diào)用其他模塊,而且模塊都是基于文件的,機(jī)制十分簡單。

Node.js 的模塊和包機(jī)制的實現(xiàn)參照了 CommonJS 的標(biāo)準(zhǔn),但并未完全遵循。不過兩者的區(qū)別并不大,一般來說你大可不必?fù)?dān)心,只有當(dāng)你試圖制作一個除了支持 Node.js之外還要支持其他平臺的模塊或包的時候才需要仔細(xì)研究。通常,兩者沒有直接沖突的地方。我們經(jīng)常把 Node.js 的模塊和包相提并論,因為模塊和包是沒有本質(zhì)區(qū)別的,兩個概念也時常混用。如果要辨析,那么可以把包理解成是實現(xiàn)了某個功能模塊的集合,用于發(fā)布和維護(hù)。對使用者來說,模塊和包的區(qū)別是透明的,因此經(jīng)常不作區(qū)分。本節(jié)中我們會詳細(xì)介紹:
1.什么是模塊
模塊是 Node.js 應(yīng)用程序的基本組成部分,文件和模塊是一一對應(yīng)的。換言之,一個Node.js 文件就是一個模塊,這個文件可能是 JavaScript 代碼、JSON 或者編譯過的 C/C++ 擴(kuò)展。在前面章節(jié)的例子中,我們曾經(jīng)用到了 var http = require('http'), 其中 http是 Node.js 的一個核心模塊,其內(nèi)部是用 C++ 實現(xiàn)的,外部用 JavaScript 封裝。我們通過require 函數(shù)獲取了這個模塊,然后才能使用其中的對象。

2.創(chuàng)建模塊
在 Node.js 中,創(chuàng)建一個模塊非常簡單,因為一個文件就是一個模塊,我們要關(guān)注的問題僅僅在于如何在其他文件中獲取這個模塊。Node.js 提供了 exports 和 require 兩個對象,其中 exports 是模塊公開的接口, require 用于從外部獲取一個模塊的接口,即所獲取模塊的 exports 對象。讓我們以一個例子來了解模塊。創(chuàng)建一個 module.js 的文件,內(nèi)容是:

//module.js
var name;
exports.setName = function(thyName) {
name = thyName;
};
exports.sayHello = function() {
console.log('Hello ' + name);
};

在同一目錄下創(chuàng)建 getmodule.js,內(nèi)容是:

//getmodule.js
var myModule = require('./module');
myModule.setName('BYVoid');
myModule.sayHello();

運行node getmodule.js,結(jié)果是:

Hello BYVoid

在以上示例中,module.js 通過 exports 對象把 setName 和 sayHello 作為模塊的訪問接口,在 getmodule.js 中通過 require('./module') 加載這個模塊,然后就可以直接訪問 module.js 中 exports 對象的成員函數(shù)了。這種接口封裝方式比許多語言要簡潔得多,同時也不失優(yōu)雅,未引入違反語義的特性,符合傳統(tǒng)的編程邏輯。在這個基礎(chǔ)上,我們可以構(gòu)建大型的應(yīng)用程序,npm 提供的上萬個模塊都是通過這種簡單的方式搭建起來的。
這里有個疑問,可以參考exports 和 module.exports 的區(qū)別module.exports與exports??關(guān)于exports的總結(jié)

  • module.exports 初始值為一個空對象 {}
  • exports 是指向的 module.exports 的引用
  • require() 返回的是 module.exports 而不是 exports

我們經(jīng)??吹竭@樣的寫法:exports = module.exports = somethings
上面的代碼等價于:

module.exports = somethings
exports = module.exports

原理很簡單,即 module.exports 指向新的對象時,exports 斷開了與 module.exports 的引用,那么通過 exports = module.exports 讓 exports 重新指向 module.exports 即可。

3.單次加載
上面這個例子有點類似于創(chuàng)建一個對象,但實際上和對象又有本質(zhì)的區(qū)別,因為require 不會重復(fù)加載模塊,也就是說無論調(diào)用多少次 require, 獲得的模塊都是同一個。我們在 getmodule.js 的基礎(chǔ)上稍作修改:

//loadmodule.js
var hello1 = require('./module');
hello1.setName('BYVoid');
var hello2 = require('./module');
hello2.setName('BYVoid 2');
hello1.sayHello();

運行后發(fā)現(xiàn)輸出結(jié)果是 Hello BYVoid 2 ,這是因為變量 hello1 和 hello2 指向的是同一個實例,因此 hello1.setName 的結(jié)果被 hello2.setName 覆蓋,最終輸出結(jié)果是由后者決定的。

  1. 覆蓋 exports

有時候我們只是想把一個對象封裝到模塊中,例如:

//singleobject.js
function Hello() {
var name;
this.setName = function (thyName) {
name = thyName;
};
this.sayHello = function () {
console.log('Hello ' + name);
};
};
exports.Hello = Hello;

此時我們在其他文件中需要通過 require('./singleobject').Hello 來獲取Hello 對象,這略顯冗余,可以用下面方法稍微簡化:

//hello.js
function Hello() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function() {
console.log('Hello ' + name);
};
};
module.exports = Hello;

這樣就可以直接獲得這個對象了:

//gethello.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('BYVoid');
hello.sayHello();

注意,模塊接口的唯一變化是使用 module.exports = Hello 代替了 exports.Hello=Hello 。在外部引用該模塊時,其接口對象就是要輸出的 Hello 對象本身,而不是原先的exports 。事實上, exports 本身僅僅是一個普通的空對象,即 {} ,它專門用來聲明接口,本質(zhì)上是通過它為模塊閉包的內(nèi)部建立了一個有限的訪問接口。因為它沒有任何特殊的地方,所以可以用其他東西來代替,譬如我們上面例子中的 Hello 對象。

注意,不可以通過對 exports 直接賦值代替對 module.exports 賦值。exports 實際上只是一個和 module.exports 指向同一個對象的變量,它本身會在模塊執(zhí)行結(jié)束后釋放,但 module 不會,因此只能通過指定module.exports 來改變訪問接口。

5.創(chuàng)建包
包是在模塊基礎(chǔ)上更深一步的抽象,Node.js 的包類似于 C/C++ 的函數(shù)庫或者 Java/.Net的類庫。它將某個獨立的功能封裝起來,用于發(fā)布、更新、依賴管理和版本控制。Node.js 根據(jù) CommonJS 規(guī)范實現(xiàn)了包機(jī)制,開發(fā)了 npm來解決包的發(fā)布和獲取需求。Node.js 的包是一個目錄,其中包含一個 JSON 格式的包說明文件 package.json。嚴(yán)格符合 CommonJS 規(guī)范的包應(yīng)該具備以下特征:
? package.json 必須在包的頂層目錄下;
? 二進(jìn)制文件應(yīng)該在 bin 目錄下;
? JavaScript 代碼應(yīng)該在 lib 目錄下;
? 文檔應(yīng)該在 doc 目錄下;
? 單元測試應(yīng)該在 test 目錄下。
Node.js 對包的要求并沒有這么嚴(yán)格,只要頂層目錄下有 package.json,并符合一些規(guī)范即可。當(dāng)然為了提高兼容性,我們還是建議你在制作包的時候,嚴(yán)格遵守 CommonJS 規(guī)范。

模塊與文件是一一對應(yīng)的。文件不僅可以是 JavaScript 代碼或二進(jìn)制代碼,還可以是一個文件夾。最簡單的包,就是一個作為文件夾的模塊。下面我們來看一個例子,建立一個叫做 somepackage 的文件夾,在其中創(chuàng)建 index.js,內(nèi)容如下:

//somepackage/index.js
exports.hello = function() {
console.log('Hello.');
};

然后在 somepackage 之外建立 getpackage.js,內(nèi)容如下:

//getpackage.js
var somePackage = require('./somepackage');
somePackage.hello();

運行 node getpackage.js,控制臺將輸出結(jié)果 Hello. 。我們使用這種方法可以把文件夾封裝為一個模塊,即所謂的包。包通常是一些模塊的集合,在模塊的基礎(chǔ)上提供了更高層的抽象,相當(dāng)于提供了一些固定接口的函數(shù)庫。通過定制package.json,我們可以創(chuàng)建更復(fù)雜、更完善、更符合規(guī)范的包用于發(fā)布。

在前面例子中的 somepackage 文件夾下,我們創(chuàng)建一個叫做 package.json 的文件,內(nèi)容如下所示:

{
"main" : "./lib/interface.js"
}

然后將 index.js 重命名為 interface.js 并放入 lib 子文件夾下。以同樣的方式再次調(diào)用這個包,依然可以正常使用。Node.js 在調(diào)用某個包時,會首先檢查包中 package.json 文件的 main 字段,將其作為包的接口模塊,如果 package.json 或 main 字段不存在,會嘗試尋找 index.js 或 index.node 作為包的接口。

package.json 是 CommonJS 規(guī)定的用來描述包的文件,完全符合規(guī)范的 package.json 文件應(yīng)該含有以下字段。
? name :包的名稱,必須是唯一的,由小寫英文字母、數(shù)字和下劃線組成,不能包含空格。
? description :包的簡要說明。
? version :符合語義化版本識別規(guī)范的版本字符串。
? keywords :關(guān)鍵字?jǐn)?shù)組,通常用于搜索。
? maintainers :維護(hù)者數(shù)組,每個元素要包含 name 、 email (可選)、 web (可選)字段。
? contributors :貢獻(xiàn)者數(shù)組,格式與 maintainers 相同。包的作者應(yīng)該是貢獻(xiàn)者數(shù)組的第一個元素。
? bugs :提交bug的地址,可以是網(wǎng)址或者電子郵件地址。
? licenses :許可證數(shù)組,每個元素要包含 type (許可證的名稱)和 url (鏈接到許可證文本的地址)字段。
? repositories :倉庫托管地址數(shù)組,每個元素要包含 type (倉庫的類型,如 git )、url (倉庫的地址)和 path (相對于倉庫的路徑,可選)字段。
? dependencies :包的依賴,一個關(guān)聯(lián)數(shù)組,由包名稱和版本號組成。
下面是一個完全符合 CommonJS 規(guī)范的 package.json 示例:

{
"name": "mypackage",
"description": "Sample package for CommonJS. This package demonstrates the required
elements of a CommonJS package.",
"version": "0.7.0",
"keywords": [
"package",
"example"
],
"maintainers": [
{
"name": "Bill Smith",
"email": "bills@example.com",
}
],
"contributors": [
{
"name": "BYVoid",
"web": "http://www.byvoid.com/"
}
],
"bugs": {
"mail": "dev@example.com",
"web": "http://www.example.com/bugs"
},
"licenses": [
{
"type": "GPLv2",
"url": "http://www.example.org/licenses/gpl.html"
}
],
"repositories": [
{
"type": "git",
"url": "http://github.com/BYVoid/mypackage.git"
}
],
"dependencies": {
"webkit": "1.2",
"ssl": {
"gnutls": ["1.0", "2.0"],
"openssl": "0.9.8"
}
}
}
十六、Node.js模塊加載機(jī)制

參考 《Node.js開發(fā)指南 ByVoid》Page 132
Node.js 的模塊可以分為兩大類,一類是核心模塊,另一類是文件模塊。核心模塊就是Node.js 標(biāo)準(zhǔn) API 中提供的模塊,如 fs 、 http 、 net 、 vm 等,這些都是由 Node.js 官方提供的模塊,編譯成了二進(jìn)制代碼。我們可以直接通過 require 獲取核心模塊,例如require('fs') 。核心模塊擁有最高的加載優(yōu)先級,換言之如果有模塊與其命名沖突,Node.js 總是會加載核心模塊。文件模塊則是存儲為單獨的文件(或文件夾)的模塊,可能是 JavaScript 代碼、JSON 或編譯好的 C/C++ 代碼。文件模塊的加載方法相對復(fù)雜,但十分靈活,尤其是和 npm 結(jié)合使用時。在不顯式指定文件模塊擴(kuò)展名的時候,Node.js 會分別試圖加上.js、.json 和 .node擴(kuò)展名。.js 是 JavaScript 代碼,.json 是 JSON 格式的文本,.node 是編譯好的 C/C++ 代碼。

文件模塊的加載有兩種方式,一種是按路徑加載,一種是查找 node_modules 文件夾。如果 require 參數(shù)以“ / ”開頭,那么就以絕對路徑的方式查找模塊名稱,例如 require('/home/byvoid/module') 將會按照優(yōu)先級依次嘗試加載 /home/byvoid/module.js、/home/byvoid/module.json 和 /home/byvoid/module.node。如果 require 參數(shù)以“ ./ ”或“ ../ ”開頭,那么則以相對路徑的方式來查找模塊,這種方式在應(yīng)用中是最常見的。例如前面的例子中我們用了 require('./hello') 來加載同一文件夾下的hello.js。

如果 require 參數(shù)不以“ / ”、“ ./ ”或“ ../ ”開頭,而該模塊又不是核心模塊,那么就要通過查找 node_modules 加載模塊了。我們使用npm獲取的包通常就是以這種方式加載的。在某個目錄下執(zhí)行命令 npm install express,你會發(fā)現(xiàn)出現(xiàn)了一個叫做node_modules的目錄,里面的結(jié)構(gòu)大概如圖 6-1 所示。


圖6-1 node_modules 目錄結(jié)構(gòu)

在 node_modules 目錄的外面一層,我們可以直接使用 require('express') 來代替require('./node_modules/express') 。這是Node.js模塊加載的一個重要特性:通過查找 node_modules 目錄來加載模塊。當(dāng) require 遇到一個既不是核心模塊,又不是以路徑形式表示的模塊名稱時,會試圖在當(dāng)前目錄下的 node_modules 目錄中來查找是不是有這樣一個模塊。如果沒有找到,則會在當(dāng)前目錄的上一層中的 node_modules 目錄中繼續(xù)查找,反復(fù)執(zhí)行這一過程,直到遇到根目錄為止。舉個例子,我們要在 /home/byvoid/develop/foo.js 中使用 require('bar.js') 命令,Node.js會依次查找:

?  /home/byvoid/develop/node_modules/bar.js
?  /home/byvoid/node_modules/bar.js
?  /home/node_modules/bar.js
?  /node_modules/bar.js

為什么要這樣做呢?因為通常一個工程內(nèi)會有一些子目錄,當(dāng)子目錄內(nèi)的文件需要訪問到工程共同依賴的模塊時,就需要向父目錄上溯了。比如說工程的目錄結(jié)構(gòu)如下:

|- project
|- app.js
|- models
|- ...
|- views
|- ...
|- controllers
|- index_controller.js
|- error_controller.js
|- ...
|- node_modules
|- express

我們不僅要在 project 目錄下的 app.js 中使用 require('express') ,而且可能要在controllers 子目錄下的index_controller.js 中也使用 require('express') ,這時就需要向父目錄上溯一層才能找到 node_modules 中的 express 了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容