JavaScript是世界上使用頻率最高的程式語言之一,它是Web世界的通用語言,被所有瀏覽器所使用。 JavaScript的誕生要追溯到Netscape那個時代,它的核心內容被倉促的開發出來,用以對抗Microsoft,參與當時白熱化的瀏覽器大戰。由於過早的發布,無可避免的造成了它的一些不太好的特性。
儘管它的開發時間很短,但是JavaScript依然具備了許多強大的特性,不過,每個腳本共享一個全域命名空間這個特性除外。
一旦Web頁面載入了JavaScript程式碼,它就會被注入到全域命名空間,會和其他所有已載入的腳本公用同一個位址空間,這會導致很多安全性問題,衝突,以及一些常見問題,讓bug即難以追蹤又很難解決。
不過謝天謝地,Node為伺服器端JavaScript定了一些規範,也實作了CommonJS的模組標準,在這個標準裡,每個模組有自己的上下文,和其他模組相區分。這意味著,模組不會污染全域作用域,因為根本沒有所謂的全域作用域,模組之間也不會互相干擾。
本章,我們將了解幾種不同的模組以及如何載入它們。
把程式碼拆分成一系列定義良好的模組可以幫你掌控你的應用程序,下面我們將學習如何創建和使用你自己的模組。
了解Node如何載入模組
Node裡,可以透過檔案路徑來引用模組,也可以透過模組名引用,如果用名稱引用非核心模組,Node最後會把模組名影射到對應的模組檔案路徑。而那些包含了核心函數的核心模組,會在Node啟動時預先載入。
非核心模組包括使用NPM(Node Package Manager)安裝的第三方模組,以及你或你的同事創建的本機模組。
每個被目前腳本導入的模組都會向程式設計師揭露一組公開API,使用模組前,需要用require函數來導入它,像這樣:
上面的程式碼會導入一個名為module_name的模組,它可能是個核心模組,也可以是用NPM安裝的模組,require函數傳回一個包含模組所有公用API的物件。隨模組的不同,傳回的物件可能是任何JavaScript值,可以是一個函數,也可以是一個包含了一系列屬性(函數,陣列或任何JavaScript物件)的物件。
導出模組
CommonJS模組系統是Node下檔案間共用物件和函數的唯一方式。對於一個很複雜的程序,你應該把一些類,物件或函數重構成一系列良好定義的可重複使用模組。對於模組使用者來說,模組僅對外暴露出那些你指定的程式碼。
在下面的例子裡你將會了解到,在Node里文件和模組是一一對應的,我們創建了一個叫circle.js的文件,它僅對外導出了Circle建構子。
function r_squared() {
return Math.pow(r, 2);
}
function area() {
return Math.PI * r_squared();
}
return {area: area};
}
module.exports = Circle;
程式碼裡最重要的是最後一行,它定義了模組對外導出了什麼內容。 module是個特殊的變量,它代表當前模組自身,而module.exports是模組對外導出的對象,它可以是任何對象,在這個例子裡,我們把Circle的建構子導出了,這樣模組使用者就可以用這個模組來創建Circle實例。
你也可以導出一些複雜的對象,module.exports被初始化成一個空對象,你把任何你想暴露給外界的內容,作為module.exports對象的屬性來導出。例如,你設計了一個模組,它對外暴露了一組函數:
console.log('A');
}
function printB() {
console.log('B');
}
function printC() {
console.log('C');
}
module.exports.printA = printA;
module.exports.printB = printB;
module.exports.pi = Math.PI;
這個模組導出了兩個函數(printA和printB)和一個數字(pi),呼叫程式碼看起來像這樣:
myModule2.printA(); // -> A
myModule2.printB(); // -> B
console.log(myModule2.pi); // -> 3.141592653589793
載入模組
前面提到過,你可以使用require函數來載入模組,不用擔心在程式碼裡呼叫require會影響全域命名空間,因為Node裡就沒有全域命名空間這個概念。如果模組存在且沒有任何語法或初始化錯誤,require函數就會傳回這個模組對象,你也可以這個物件賦值給任何一個局部變數。
模組有幾種不同的類型,大概可以分為核心模組,本地模組和透過NPM安裝的第三方模組,根據模組的類型,有幾種引用模組的方式,下面我們就來了解下這些知識。
載入核心模組
Node有一些被編譯到二進位檔案裡的模組,稱為核心模組,它們不能透過路徑來引用,只能用模組名。核心模組擁有最高的載入優先權,即使已經有了一個同名的第三方模組,核心模組也會被優先載入。
例如,如果你想載入和使用http核心模組,可以這樣做:
這將傳回一個包含了http模組對象,它包含了Node API文件裡定義的那些htpp模組的API。
載入檔案模組
你也可以使用絕對路徑從檔案系統載入模組:
var myModule2 = require('./lib/my_module_2');
注意上面的程式碼,你可以省略文件名的副檔名,如果Node找不到這個文件,會嘗試在文件名後加上js後綴再找(譯者註:其實除了js,還會找json和node,具體可以看官網文檔),因此,如果在當前目錄下存在一個叫my_module.js的文件,會有下面兩種加載方式:
var myModule = require('./my_module.js');
載入目錄模組
你也可以使用目錄的路徑來載入模組:
Node會假定這個目錄是個模組包,並嘗試在這個目錄下搜尋包定義檔package.json。
如果沒找到,Node會假設包的入口點是index.js檔(譯者註:除了index.js還會找index.node,.node檔是Node的二進位擴充包,具體見官方文件) ,以上面程式碼為例,Node會嘗試查找./myModuleDir/index.js檔。
反之,如果找到了package.json文件,Node會嘗試解析它,並查找套件定義裡的main屬性,然後把main屬性的值當作入口點的相對路徑。以此例來說,如果package.json定義如下:
"name" : "myModule",
"main" : "./lib/myModule.js"
}
Node就會嘗試載入./myModuleDir/lib/myModule.js檔
從node_modules目錄載入
如果require函式的參數不是相對路徑,也不是核心模組名,Node會在目前目錄的node_modules子目錄下找,例如下面的程式碼,Node會嘗試找檔案./node_modules/myModule.js:
你可以使用這個特性來管理node_modules目錄的內容或模組,不過最好還是把模組的管理任務交給NPM(見第一章),本地node_modules目錄是NPM安裝模組的預設位置,這個設計把Node和NPM關聯在了一起。通常,作為開發人員不必太關心這個特性,你可以簡單的使用NPM安裝,更新和刪除包,它會幫你維護node_modules目錄
快取模組
模組在第一次成功載入後會被快取起來,就是說,如果模組名稱被解析到同一個檔案路徑,那麼每次呼叫require(‘myModule')都確切地會傳回同一個模組。
例如,有一個叫my_module.js的模組,包含下面的內容:
module.exports = function() {
console.log('Hi!');
};
console.log('my_module initialized.');
然後用下面的程式碼載入這個模組:
它會產生下面的輸出:
my_module initialized
如果我們兩次導入它:
var myModuleInstance2 = require('./my_module');
輸出依然是:
my_module initialized
也就是說,模組的初始化程式碼只執行了一次。當你建立自己的模組時,如果模組的初始化程式碼含有可能產生副作用的程式碼,一定要特別注意這個特性。
小結
Node取消了JavaScript的預設全域作用域,轉而採用CommonJS模組系統,這樣你可以更好的組織你的程式碼,也因此避免了很多安全性問題和bug。可以使用require函數來載入核心模組,第三方模組,或從檔案及目錄載入自己的模組
也可以用相對路徑或絕對路徑來載入非核心模組,如果把模組放到了node_modules目錄下或是用NPM安裝的模組,你也可以直接使用模組名稱來載入。
譯者註:
建議讀者把官方文件的模組章節閱讀一遍,個人感覺比作者講得更清晰明了,而且還附加了一個非常具有代表性的例子,對理解Node模組加載會很有幫助。下面把那個例子也引用過來:
1. 如果X是核心模組,
a. 載入並返回核心模組
b. 結束
2. 如果X以 './' 或 '/' 或 '../ 開始'
a. LOAD_AS_FILE(Y X)
b. LOAD_AS_DIRECTORY(Y X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. 拋出異常:"not found"
LOAD_AS_FILE(X)
1. 如果X是個文件,把 X當作JavaScript 腳本載入,載入完畢後結束
2. 如果X.js是個文件,把X.js 作為JavaScript 腳本加載,加載完畢後結束
3. 如果X.node是個文件,把X.node 作為Node二進位插件加載,加載完畢後結束
LOAD_AS_DIRECTORY(X)
1. 如果 X/package.json檔案存在,
a. 解析X/package.json, 並尋找 "main"欄位.
b. 另M = X (main欄位的值)
c. LOAD_AS_FILE(M)
2. 如果X/index.js檔案存在,把 X/index.js當作JavaScript 腳本載入,載入完畢後結束
3. 如果X/index.node檔案存在,把load X/index.node當作Node二進位插件載入,載入完畢後結束
LOAD_NODE_MODULES(X, START)
1. 另DIRS=NODE_MODULES_PATHS(START)
2. 對DIRS下的每個目錄DIR做如下操作:
a. LOAD_AS_FILE(DIR/X)
b. LOAD_AS_DIRECTORY(DIR/X)
NODE_MODULES_PATHS(START)
1. 另PARTS = path split(START)
2. 另ROOT = index of first instance of "node_modules" in PARTS, or 0
3. 另I = count of PARTS - 1
4. 另DIRS = []
5. while I > ROOT,
a. 如果 PARTS[I] = "node_modules" 則繼續後續操作,否則下次循環
c. DIR = path join(PARTS[0 .. I] "node_modules")
b. DIRS = DIRS DIR
c. 另I = I - 1
6. 返回DIRS