在計算機程序的開發過程中,隨著程序代碼越寫越多,在一個文件里代碼就會越來越長,越來越不容易維護。
為了編寫可維護的代碼,我們把很多函數分組,分別放到不同的文件里,這樣,每個文件包含的代碼就相對較少,很多編程語言都采用這種組織代碼的方式。在Node環境中,一個.js
文件就稱之為一個模塊(module
)。
使用模塊有什么好處?
最大的好處是大大提高了代碼的可維護性。其次,編寫代碼不必從零開始。當一個模塊編寫完畢,就可以被其他地方引用。我們在編寫程序的時候,也經常引用其他模塊,包括Node內置的模塊和來自第三方的模塊。
使用模塊還可以避免函數名和變量名沖突。相同名字的函數和變量完全可以分別存在不同的模塊中,因此,我們自己在編寫模塊時,不必考慮名字會與其他模塊沖突。
在上一節,我們編寫了一個hello.js文件,這個hello.js文件就是一個模塊,模塊的名字就是文件名(去掉.js后綴),所以hello.js文件就是名為hello的模塊。
我們把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');
就拿到了引用模塊的變量。
Node模塊的原理
當我們編寫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仍然是空對象{}。
總結
- 在Node環境中,一個.js文件就稱之為一個模塊(module)。
- module大大提高了代碼的可維護性;可以被其他地方引用;使用模塊還可以避免函數名和變量名沖突
- 要在模塊中對外輸出變量,用:
module.exports = variable;
輸出的變量可以是任意對象、函數、數組等等。
- 引入其他模塊輸出的對象,用:
var foo = require('other_module');
引入的對象具體是什么,取決于引入模塊輸出的對象。
- 兩種方法輸出變量
//第一種
module.exports = {
hello: hello,
greet: greet
};
//第二種
exports.hello = hello;
exports.greet = greet;
- 直接對
module.exports
賦值,可以應對任何情況。