在計(jì)算機(jī)程序的開發(fā)過程中,隨著程序代碼越寫越多,在一個(gè)文件里代碼就會(huì)越來越長,越來越不容易維護(hù)。
為了編寫可維護(hù)的代碼,我們把很多函數(shù)分組,分別放到不同的文件里,這樣,每個(gè)文件包含的代碼就相對(duì)較少,很多編程語言都采用這種組織代碼的方式。在Node環(huán)境中,一個(gè).js文件就稱之為一個(gè)模塊(module)。
使用模塊有什么好處?
最大的好處是大大提高了代碼的可維護(hù)性。其次,編寫代碼不必從零開始。當(dāng)一個(gè)模塊編寫完畢,就可以被其他地方引用。我們?cè)诰帉懗绦虻臅r(shí)候,也經(jīng)常引用其他模塊,包括Node內(nèi)置的模塊和來自第三方的模塊。
使用模塊還可以避免函數(shù)名和變量名沖突。相同名字的函數(shù)和變量完全可以分別存在不同的模塊中,因此,我們自己在編寫模塊時(shí),不必考慮名字會(huì)與其他模塊沖突。
在上一節(jié),我們編寫了一個(gè)hello.js文件,這個(gè)hello.js文件就是一個(gè)模塊,模塊的名字就是文件名(去掉.js后綴),所以hello.js文件就是名為hello的模塊。
我們把hello.js改造一下,創(chuàng)建一個(gè)函數(shù),這樣我們就可以在其他地方調(diào)用這個(gè)函數(shù):
'use strict';
var s = 'Hello';
function greet(name) {
console.log(s + ', ' + name + '!');
}
module.exports = greet;
函數(shù)greet()是我們?cè)趆ello模塊中定義的,你可能注意到最后一行是一個(gè)奇怪的賦值語句,它的意思是,把函數(shù)greet作為模塊的輸出暴露出去,這樣其他模塊就可以使用greet函數(shù)了。
問題是其他模塊怎么使用hello模塊的這個(gè)greet函數(shù)呢?我們?cè)倬帉懸粋€(gè)main.js文件,調(diào)用hello模塊的greet函數(shù):
'use strict';
// 引入hello模塊:
var greet = require('./hello');
var s = 'Michael';
greet(s); // Hello, Michael!
注意到引入hello模塊用Node提供的require函數(shù):
var greet = require('./hello');
引入的模塊作為變量保存在greet變量中,那greet變量到底是什么東西?其實(shí)變量greet就是在hello.js中我們用module.exports = greet;輸出的greet函數(shù)。所以,main.js就成功地引用了hello.js模塊中定義的greet()函數(shù),接下來就可以直接使用它了。
在使用require()引入模塊的時(shí)候,請(qǐng)注意模塊的相對(duì)路徑。因?yàn)閙ain.js和hello.js位于同一個(gè)目錄,所以我們用了當(dāng)前目錄.:
var greet = require('./hello'); // 不要忘了寫相對(duì)目錄!
如果只寫模塊名:
var greet = require('hello');
則Node會(huì)依次在內(nèi)置模塊、全局模塊和當(dāng)前模塊下查找hello.js,你很可能會(huì)得到一個(gè)錯(cuò)誤:
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
遇到這個(gè)錯(cuò)誤,你要檢查:
- 模塊名是否寫對(duì)了;
- 模塊文件是否存在;
- 相對(duì)路徑是否寫對(duì)了。
CommonJS規(guī)范
這種模塊加載機(jī)制被稱為CommonJS規(guī)范。在這個(gè)規(guī)范下,每個(gè).js文件都是一個(gè)模塊,它們內(nèi)部各自使用的變量名和函數(shù)名都互不沖突,例如,hello.js和main.js都申明了全局變量var s = 'xxx',但互不影響。
一個(gè)模塊想要對(duì)外暴露變量(函數(shù)也是變量),可以用module.exports = variable;,一個(gè)模塊要引用其他模塊暴露的變量,用var ref = require('module_name');就拿到了引用模塊的變量。
Node模塊的原理
當(dāng)我們編寫JavaScript代碼時(shí),我們可以申明全局變量:
var s = 'global';
在瀏覽器中,大量使用全局變量可不好。如果你在a.js中使用了全局變量s,那么,在b.js中也使用全局變量s,將造成沖突,b.js中對(duì)s賦值會(huì)改變a.js的運(yùn)行邏輯。
也就是說,JavaScript語言本身并沒有一種模塊機(jī)制來保證不同模塊可以使用相同的變量名。
那Node.js是如何實(shí)現(xiàn)這一點(diǎn)的?
其實(shí)要實(shí)現(xiàn)“模塊”這個(gè)功能,并不需要語法層面的支持。Node.js也并不會(huì)增加任何JavaScript語法。實(shí)現(xiàn)“模塊”功能的奧妙就在于JavaScript是一種函數(shù)式編程語言,它支持閉包。如果我們把一段JavaScript代碼用一個(gè)函數(shù)包裝起來,這段代碼的所有“全局”變量就變成了函數(shù)內(nèi)部的局部變量。
請(qǐng)注意我們編寫的hello.js代碼是這樣的:
var s = 'Hello';
var name = 'world';
console.log(s + ' ' + name + '!');
Node.js加載了hello.js后,它可以把代碼包裝一下,變成這樣執(zhí)行:
(function () {
// 讀取的hello.js代碼:
var s = 'Hello';
var name = 'world';
console.log(s + ' ' + name + '!');
// hello.js代碼結(jié)束
})();
這樣一來,原來的全局變量s現(xiàn)在變成了匿名函數(shù)內(nèi)部的局部變量。如果Node.js繼續(xù)加載其他模塊,這些模塊中定義的“全局”變量s也互不干擾。
所以,Node利用JavaScript的函數(shù)式編程的特性,輕而易舉地實(shí)現(xiàn)了模塊的隔離。
但是,模塊的輸出module.exports怎么實(shí)現(xiàn)?
這個(gè)也很容易實(shí)現(xiàn),Node可以先準(zhǔn)備一個(gè)對(duì)象module:
// 準(zhǔn)備module對(duì)象:
var module = {
id: 'hello',
exports: {}
};
var load = function (module) {
// 讀取的hello.js代碼:
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = greet;
// hello.js代碼結(jié)束
return module.exports;
};
var exported = load(module);
// 保存module:
save(module, exported);
可見,變量module是Node在加載js文件前準(zhǔn)備的一個(gè)變量,并將其傳入加載函數(shù),我們?cè)趆ello.js中可以直接使用變量module原因就在于它實(shí)際上是函數(shù)的一個(gè)參數(shù):
module.exports = greet;
通過把參數(shù)module傳遞給load()函數(shù),hello.js就順利地把一個(gè)變量傳遞給了Node執(zhí)行環(huán)境,Node會(huì)把module變量保存到某個(gè)地方。
由于Node保存了所有導(dǎo)入的module,當(dāng)我們用require()獲取module時(shí),Node找到對(duì)應(yīng)的module,把這個(gè)module的exports變量返回,這樣,另一個(gè)模塊就順利拿到了模塊的輸出:
var greet = require('./hello');
以上是Node實(shí)現(xiàn)JavaScript模塊的一個(gè)簡單的原理介紹。
module.exports vs exports
很多時(shí)候,你會(huì)看到,在Node環(huán)境中,有兩種方法可以在一個(gè)模塊中輸出變量:
- 方法一:對(duì)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;
但是你不可以直接對(duì)exports賦值:
// 代碼可以執(zhí)行,但是模塊并沒有輸出任何變量:
exports = {
hello: hello,
greet: greet
};
如果你對(duì)上面的寫法感到十分困惑,不要著急,我們來分析Node的加載機(jī)制:
首先,Node會(huì)把整個(gè)待加載的hello.js文件放入一個(gè)包裝函數(shù)load中執(zhí)行。在執(zhí)行這個(gè)load()函數(shù)前,Node準(zhǔn)備好了module變量:
var module = {
id: 'hello',
exports: {}
};
load()函數(shù)最終返回module.exports:
var load = function (exports, module) {
// hello.js的文件內(nèi)容
...
// load函數(shù)返回:
return module.exports;
};
var exported = load(module.exports, module);
也就是說,默認(rèn)情況下,Node準(zhǔn)備的exports變量和module.exports變量實(shí)際上是同一個(gè)變量,并且初始化為空對(duì)象{},于是,我們可以寫:
exports.foo = function () { return 'foo'; };
exports.bar = function () { return 'bar'; };
也可以寫:
module.exports.foo = function () { return 'foo'; };
module.exports.bar = function () { return 'bar'; };
換句話說,Node默認(rèn)給你準(zhǔn)備了一個(gè)空對(duì)象{},這樣你可以直接往里面加?xùn)|西。
但是,如果我們要輸出的是一個(gè)函數(shù)或數(shù)組,那么,只能給module.exports賦值:
module.exports = function () { return 'foo'; };
給exports賦值是無效的,因?yàn)橘x值后,module.exports仍然是空對(duì)象{}。
總結(jié)
- 在Node環(huán)境中,一個(gè).js文件就稱之為一個(gè)模塊(module)。
- module大大提高了代碼的可維護(hù)性;可以被其他地方引用;使用模塊還可以避免函數(shù)名和變量名沖突
- 要在模塊中對(duì)外輸出變量,用:
module.exports = variable;
輸出的變量可以是任意對(duì)象、函數(shù)、數(shù)組等等。
- 引入其他模塊輸出的對(duì)象,用:
var foo = require('other_module');
引入的對(duì)象具體是什么,取決于引入模塊輸出的對(duì)象。
- 兩種方法輸出變量
//第一種
module.exports = {
hello: hello,
greet: greet
};
//第二種
exports.hello = hello;
exports.greet = greet;
- 直接對(duì)
module.exports賦值,可以應(yīng)對(duì)任何情況。