node.js中“模組”Module的概念和介紹
模組 Module
在計算機程式的開發過程中,隨著程式程式碼越寫越多,在一個檔案裡程式碼就會越來越長,越來越不容易維護。
為了編寫可維護的程式碼,我們把很多函式分組,分別放到不同的檔案裡,這樣,每個檔案包含的程式碼就相對較少,很多程式語言都採用這種組織程式碼的方式。在Node環境中,一個.js檔案就稱之為一個模組(module)。
使用模組有什麼好處?
最大的好處是大大提高了程式碼的可維護性。其次,編寫程式碼不必從零開始。當一個模組編寫完畢,就可以被其他地方引用。我們在編寫程式的時候,也經常引用其他模組,包括Node內建的模組和來自第三方的模組。
使用模組還可以避免函式名和變數名衝突。相同名字的函式和變數完全可以分別存在不同的模組中,因此,我們自己在編寫模組時,不必考慮名字會與其他模組衝突。
例如下面的hello.js
檔案就是一個模組,模組的名字就是檔名(去掉.js
字尾),所以hello.js
檔案就是名為hello
的模組。
我們在hello.js檔案中
建立一個函式,這樣我們就可以在其他地方呼叫這個函式:
hello.js:
'use strict';
var s = 'Hello';
function greet(name) {
console.log(s + ', ' + name + '!');
}
module.exports = greet;
函式greet()
是我們在hello
模組中定義的,你可能注意到最後一行是一個奇怪的賦值語句,它的意思是,把函式greet
greet
函數了。
問題是其他模組怎麼使用hello
模組的這個greet
函式呢?我們再編寫一個main.js
檔案,呼叫hello
模組的greet
函式:
'use strict';
// 引入hello模組:
var greet = require('./hello');
var s = 'Michael';
greet(s); // Hello, Michael!
注意到引入hello
模組用Node提供的require
函式:
var greet = require('./hello');
引入的模組作為變數儲存在greet
變數中,那greet
greet
就是在hello.js
中我們用module.exports = greet;
輸出的greet
函式。所以,main.js
就成功地引用了hello.js
模組中定義的greet()
函式,接下來就可以直接使用它了。
在使用require()
引入模組的時候,請注意模組的相對路徑。因為main.js
和hello.js
位於同一個目錄,所以我們用了當前目錄.
:
var greet = require('./hello'); // 不要忘了寫相對目錄!
如果只寫模組名:
var greet = require('hello');
則Node會依次在內建模組、全域性模組和當前模組下查詢hello.js
,你很可能會得到一個錯誤:
module.js
throw err;
^
Error: Cannot find module 'hello'
at Function.Module._resolveFilename
at Function.Module._load
...
at Function.Module._load
at Function.Module.runMain
遇到這個錯誤,你要檢查:
- 模組名是否寫對了;
- 模組檔案是否存在;
- 相對路徑是否寫對了。
CommonJS規範
這種模組載入機制被稱為CommonJS規範。在這個規範下,每個.js
檔案都是一個模組,它們內部各自使用的變數名和函式名都互不衝突,例如,hello.js
和main.js
都申明瞭全域性變數var s = 'xxx'
,但互不影響。
一個模組想要對外暴露變數(函式也是變數),可以用module.exports = variable;
,一個模組要引用其他模組暴露的變數,用var ref = require('module_name');
就拿到了引用模組的變數。
結論
要在模組中對外輸出變數,用:
module.exports = variable;
輸出的變數可以是任意物件、函式、陣列等等。
要引入其他模組輸出的物件,用:
var foo = require('other_module');
引入的物件具體是什麼,取決於引入模組輸出的物件。
深入瞭解模組原理
如果你想詳細地瞭解CommonJS的模組實現原理,請繼續往下閱讀。如果不想了解,請直接跳到最後做練習。
當我們編寫JavaScript程式碼時,我們可以申明全域性變數:
var s = 'global';
在瀏覽器中,大量使用全域性變數可不好。如果你在a.js
中使用了全域性變數s
,那麼,在b.js
中也使用全域性變數s
,將造成衝突,b.js
中對s
賦值會改變a.js
的執行邏輯。
也就是說,JavaScript語言本身並沒有一種模組機制來保證不同模組可以使用相同的變數名。
那Node.js是如何實現這一點的?
其實要實現“模組”這個功能,並不需要語法層面的支援。Node.js也並不會增加任何JavaScript語法。實現“模組”功能的奧妙就在於JavaScript是一種函數語言程式設計語言,它支援閉包。如果我們把一段JavaScript程式碼用一個函式包裝起來,這段程式碼的所有“全域性”變數就變成了函式內部的區域性變數。
請注意我們編寫的hello.js
程式碼是這樣的:
var s = 'Hello';
var name = 'world';
console.log(s + ' ' + name + '!');
Node.js載入了hello.js
後,它可以把程式碼包裝一下,變成這樣執行:
(function () {
// 讀取的hello.js程式碼:
var s = 'Hello';
var name = 'world';
console.log(s + ' ' + name + '!');
// hello.js程式碼結束
})();
這樣一來,原來的全域性變數s
現在變成了匿名函式內部的區域性變數。如果Node.js繼續載入其他模組,這些模組中定義的“全域性”變數s
也互不干擾。
所以,Node利用JavaScript的函數語言程式設計的特性,輕而易舉地實現了模組的隔離。
但是,模組的輸出module.exports
怎麼實現?
這個也很容易實現,Node可以先準備一個物件module
:
// 準備module物件:
var module = {
id: 'hello',
exports: {}
};
var load = function (module) {
// 讀取的hello.js程式碼:
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = greet;
// hello.js程式碼結束
return module.exports;
};
var exported = load(module);
// 儲存module:
save(module, exported);
可見,變數module
是Node在載入js檔案前準備的一個變數,並將其傳入載入函式,我們在hello.js
中可以直接使用變數module
原因就在於它實際上是函式的一個引數:
module.exports = greet;
通過把引數module
傳遞給load()
函式,hello.js
就順利地把一個變數傳遞給了Node執行環境,Node會把module
變數儲存到某個地方。
由於Node儲存了所有匯入的module
,當我們用require()
獲取module時,Node找到對應的module
,把這個module
的exports
變數返回,這樣,另一個模組就順利拿到了模組的輸出:
var greet = require('./hello');
以上是Node實現JavaScript模組的一個簡單的原理介紹。
module.exports VS exports
很多時候,你會看到,在Node環境中,有兩種方法可以在一個模組中輸出變數:
方法一:對module.exports賦值:
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = {
hello: hello,
greet: greet
};
方法二:直接使用exports:
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
function hello() {
console.log('Hello, world!');
}
exports.hello = hello;
exports.greet = greet;
但是你不可以直接對exports
賦值:
// 程式碼可以執行,但是模組並沒有輸出任何變數:
exports = {
hello: hello,
greet: greet
};
如果你對上面的寫法感到十分困惑,不要著急,我們來分析Node的載入機制:
首先,Node會把整個待載入的hello.js
檔案放入一個包裝函式load
中執行。在執行這個load()
函式前,Node準備好了module變數:
var module = {
id: 'hello',
exports: {}
};
load()
函式最終返回module.exports
:
var load = function (exports, module) {
// hello.js的檔案內容
...
// load函式返回:
return module.exports;
};
var exported = load(module.exports, module);
也就是說,預設情況下,Node準備的exports
變數和module.exports
變數實際上是同一個變數,並且初始化為空物件{}
,於是,我們可以寫:
exports.foo = function () { return 'foo'; };
exports.bar = function () { return 'bar'; };
也可以寫:
module.exports.foo = function () { return 'foo'; };
module.exports.bar = function () { return 'bar'; };
換句話說,Node預設給你準備了一個空物件{}
,這樣你可以直接往裡面加東西。
但是,如果我們要輸出的是一個函式或陣列,那麼,只能給module.exports
賦值:
module.exports = function () { return 'foo'; };
給exports
賦值是無效的,因為賦值後,module.exports
仍然是空物件{}
。
結論
如果要輸出一個鍵值物件{}
,可以利用exports
這個已存在的空物件{}
,並繼續在上面新增新的鍵值;
如果要輸出一個函式或陣列,必須直接對module.exports
物件賦值。
所以我們可以得出結論:直接對module.exports
賦值,可以應對任何情況:
module.exports = {
foo: function () { return 'foo'; }
};
或者:
module.exports = function () { return 'foo'; };
最終,我們強烈建議使用module.exports = xxx
的方式來輸出模組變數,這樣,你只需要記憶一種方法。
練習
編寫hello.js
,輸出一個或多個函式;
編寫main.js
,引入hello
模組,呼叫其函式。
參考原始碼:
① hello.js
'use strict';
var s = 'hello';
function greet(name){
console.log(s+','+name+'!');
}
function hi(name){
console.log('Hi, '+name+'!');
}
function goodbye(name){
console.log('Goodbye, '+name+'!');
}
module.exports={
greet:greet,
hi:hi,
goodbye:goodbye
};
② main.js
'use strict';
const hello = require('./hello');
var s = 'michael';
hello.greet(s);
hello.hi(s);
hello.goodbye(s);
③ 執行結果:
hello,michael!
Hi, michael!
Goodbye, michael!
注意一點:在這兩個js檔案中都定義了全域性變數s,但是互不影響,也證明了上面的觀點。