Nodejs快速入門:PART3

3.3 模塊和包

3.3.1 什麼是模塊

模塊(module)和包(package)是Nodejs的重要支柱。開發一個具有一定規模的程序不可能只用一個文件,通常需要把各個功能拆分、封裝然後組合起來,模塊正是爲了實現這種方式誕生的。

模塊是Nodejs最基本的組成部分,文件和模塊是一一對應的。即一個Nodejs文件就是一個模塊。包是實現了某個特定功能的模塊的集合。包和模塊的概念時常混用。

Nodejs提供的require函數可以調用其他模塊,這符合CommonJS的標準。前面章節所提到的HTTP模塊是Nodejs的核心模塊,其內部是用C++實現的,外部用JS封裝。所以,node模塊對應的模塊除了JS文件外還可能是編譯過的JSON或者C/C++括展。

3.3.2 如何創建模塊

Nodejs中,一個文件就是一個模塊,我們要關注的問題僅僅在於如何在其他文件中獲取這個模塊。Nodejs提供了exports和requirejs兩個對象,其中exports是模塊公開的接口,require用於從外部獲取一個模塊的接口,即獲取模塊的exports對象。

建立一個文件夾,在下面放置如下兩個文件:

module.js:

var name = "Kyle";

function setName(newName) {
name = newName;
}

function sayHello() {
console.log("my name is "+name);
}

exports.setName = setName;
exports.sayHello = sayHello;

getModule.js:

var module = require('./module');

module.setName("Tom");
module.sayHello();

require取模塊時要注意模塊的路徑,模塊名即爲文件名,具體請向上翻閱,見3.1.2節。module.js通過exports對象把setName和sayHello作爲模塊的訪問接口,然後就可以直接訪問module.js中exports對象的成員函數了。這種簡單但不失優雅的方式搭建了npm提供的上萬個模塊。

3.3.3 模塊單次加載

模塊的加載和創建對象有本質的區別,因爲require不會重複的加載模塊,也就是說,無論調用多少次require,只要是其參數,即加載的模塊名相同,無論調用多少次require,獲得的模塊總是同一個。

下面的代碼在getModule.js稍作改動:

var module1 = require('./module');
var module2 = require('./module');

module1.setName("Jack");
module2.setName("Jeff");

module1.sayHello();

輸出 my name is Jeff。前者被後者覆蓋。

3.3.4 導出一個構造函數——導出模塊的另一種方式

有時候我們只想把一個構造函數封裝到模塊中,例如:

//Student.js
function Student(newName) {
this.name = "";

this.setName = function (newName) {
this.name = newName;
};

this.sayHello = function () {
  console.log("Hello everyone, my name is" + this.name );
}
}

exports.Student = Student;

此時通過require(“./Student.js”).Student來獲取Student構造函數就顯得有些多餘,因爲實際上我們只導出了一個構造函數,再以對象.屬性的方式獲取反倒沒有必要。Nodejs提供了另一種導出模塊的方法:

//Student.js
function Student(newName) {
this.name = "";

this.setName = function (newName) {
this.name = newName;
};

this.sayHello = function () {
  console.log("Hello everyone, my name is" + this.name );
}
}
module.exports = Student;

獲取模塊的方式也有所變化
//getModule.js

var Student = require("./Student");    
var student = new Student();

student.setName("Jack");
student.sayHello();

此時require()的返回值,也就是Student模塊導出的東西直接就是Student構造函數,所以直接獲取之就可以。這種方式和上一種爲exports設置子屬性的方式的區別在於這種方法更適合那種只導出一個事物的模塊——一般用這種導出構造函數(當然你可以用這種方式導出任意一種東西),在獲取模塊內容時就無需再以對象.屬性的方式獲取。實際上這兩種方法的本質是一樣的。我們可以用第二種導出構造函數的方式來實現模塊多次加載的效果,因爲每一次new出來的對象都是新的。

3.3.5 深入理解exports對象

exports本身僅僅是一個普通的空對象,即{},它專門用來聲明接口。因爲它沒有任何特殊的地方,所以可以用其他任何東西來代替之。

但是,require函數的返回值,即其加載模塊輸出的東西實際上是module.exports,而不是exports。
exports是對module.exports的局部引用,也就是說exports只是與module.exports指向同一個對象的變量,二者共同佔用一塊內存空間。你修改了exports對象,module.exports的內容也會被修改。exports會在模塊執行結束後釋放,而最終導出的是module.exports,換句話說exports.myFun只是module.exports.myFun的縮寫。

所以,像下面這種直接給exports賦值情況是不允許的

exports = Student;

因爲這樣你改變了exports指向的內存地址,它不再與module.exports指向同一個地方,所以導出時導出的module.export沒有變化。而像這樣

exports.Student = Student;

則是允許的,因爲exports只是新增了屬性,而其指向的內存地址沒有變化,與此同時,因爲exportsmodule.exports指向的內存地址相同,所以module.exports的內容也被修改了。

簡而言之,在定義模塊導出時,實際上導出的是module.exports。設置module.exports.屬性exports.屬性是等價的,因爲它們不涉及內存地址的改變,exports.屬性只是Nodejs爲導出提供的一個簡寫。而exports是不能被修改的,否則就會打破module.exports的引用關係,所以如果你只想導出一個構造函數,需要設置module.exports的值而不是exports的值。

3.3.6 創建一個最簡的單包

前面我們說過,Nodejs包和模塊的概念往往不用分的那麼清楚。最最基本的原則是一個模塊是一個文件。然而,當你的“模塊”實現的功能非常多,代碼量非常龐大的時候,爲了便於組織,往往需要分成很多“子模塊”,否則很難承載龐大的結構。這樣我們的“模塊”就被分成了多個文件,似乎違反了Nodejs模塊的原則。然而,Nodejs認爲,文件夾(目錄)也是一個文件,所以,當我們的模塊由許多“子模塊”文件組成時,我們就把這些子文件放在一個文件夾下,成爲一個“目錄模塊”,即“包”——子模塊的集合。

最簡單的包,就是這樣一個作爲文件夾(目錄)的模塊。現在我們先試着創建一個最簡單的包。
創建一個叫做somepackage的文件夾,在裏面創建一個叫做index.js的文件:

exports.sayHello = function () {
    console.log("Hello everyone, I love you")
};

這樣,一個最簡單的包就製作完成了。注意包的名字要小寫,作爲包的文件夾名字中如果出現大寫字母的話,在引入模塊時會被當做小寫解析。現在我們試着調用這個包。在這個包的外部建立一個叫做getPackage.js的文件:

var somePackage = require('./somepackage');
somePackage.sayHello();

運行getPackage.js文件,輸出:Hello everyone, I love you

3.3.7 導入一個包究竟導入了什麼東西

上面的案例裏,包中只有一個叫做index.js的文件,當我們引入該包時,該包的接口文件就是index.js,我們引入包的時候,就像是require函數裏的參數是./somepackage/index一樣。那麼問題來了:

  1. 首先,我們明明引入的是這個包,怎麼和單獨引入./somepackage/index的效果一樣?
  2. 當我們的包中不只有index.js一個文件,而是有多個文件的時候,又該怎麼辦呢?

請思考,包本身就是一個文件夾,本身不能承載任何代碼,所以單獨引入它是沒有意義的。Nodejs引入包的規則是:當你引入一個包時,Nodejs會尋找包(文件夾)一級目錄下的index.js文件作爲接口文件,你導入獲得的對象就是這個文件相應的模塊。當然我們能夠改變這個局面。

3.3.8 爲包設置接口模塊

每個包都需要一個接口文件,即定義這個模塊的文件,否則這個包就是一個裝着一大堆文件的文件夾,沒有意義。如果我們不願將就,不想讓index.js作爲接口文件,或者不想讓我們的接口文件放在文件夾的一級目錄下面,又該怎麼辦呢?

package.json可以幫助我們解決這個問題。通過定製package.json,我們可以創建更復雜,更完善,更符合規範的包用於發佈。有關package.json的介紹我們後面會詳細講解。

在前面的somepackage文件夾下,創建一個叫做package.json的文件,內容如下所示:

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

在somepackage下創建一個文件夾名叫lib,將index.js改名爲interface.js並移動至lib文件夾下。重新運行getPackage.js文件,與前面運行的結果相同。

Nodejs在調用某個包時,會首先解析包中的package.json文件,我們可以通過在package.json文件中添加main屬性(元素)來指定包的接口模塊是哪個,如果按照這個路徑找不到相應的文件,則會報錯。如果包中不存在package.json,那麼Nodejs就會默認認爲接口文件是文件夾一級目錄下的index.js或index.node,如果也找不到index.js或index.node,則會報錯。如果包中存在package.json但是這個文件中不存在main字段,在解析時也會報錯。

尋找接口模塊的流程如下圖所示:

3.3.9 commonJS對包的規範

Nodejs的包是一個目錄,其中包含一個名叫package.json的包說明文件。下面是CommonJS對包的規範:
1. package.json必須在頂層目錄下
2. 二進制文件應該在bin目錄下
3. JS代碼應該在lib目錄下
4. 文檔應該在doc目錄下
5. 單元測試應該在test目錄下

nodejs本身並沒有這麼嚴格的要求,只要頂層目錄有package.json並符合一些規範即可。當然爲了提高兼容性,你還是被建議嚴格遵守CommonJS製作包的規範

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章