在計(jì)算機(jī)程序的開發(fā)過程中忙干,隨著程序代碼越寫越多,在一個文件里代碼就會越來越長培遵,越來越不容易維護(hù)浙芙。
為了編寫可維護(hù)的代碼,我們把很多函數(shù)分組籽腕,分別放到不同的文件里嗡呼,這樣,每個文件包含的代碼就相對較少皇耗,很多編程語言都采用這種組織代碼的方式南窗。在Node環(huán)境中,一個.js
文件就稱之為一個模塊(module
)郎楼。
使用模塊有什么好處万伤?
最大的好處是大大提高了代碼的可維護(hù)性。其次呜袁,編寫代碼不必從零開始敌买。當(dāng)一個模塊編寫完畢,就可以被其他地方引用阶界。我們在編寫程序的時候虹钮,也經(jīng)常引用其他模塊,包括Node內(nèi)置的模塊和來自第三方的模塊膘融。
使用模塊還可以避免函數(shù)名和變量名沖突芙粱。相同名字的函數(shù)和變量完全可以分別存在不同的模塊中,因此氧映,我們自己在編寫模塊時春畔,不必考慮名字會與其他模塊沖突。
在上一節(jié)岛都,我們編寫了一個hello.js文件律姨,這個hello.js文件就是一個模塊,模塊的名字就是文件名(去掉.js后綴)疗绣,所以hello.js文件就是名為hello的模塊线召。
我們把hello.js改造一下,創(chuàng)建一個函數(shù)多矮,這樣我們就可以在其他地方調(diào)用這個函數(shù):
'use strict';
var s = 'Hello';
function greet(name) {
console.log(s + ', ' + name + '!');
}
module.exports = greet;
函數(shù)greet()是我們在hello模塊中定義的缓淹,你可能注意到最后一行是一個奇怪的賦值語句,它的意思是塔逃,把函數(shù)greet作為模塊的輸出暴露出去讯壶,這樣其他模塊就可以使用greet函數(shù)了。
問題是其他模塊怎么使用hello模塊的這個greet函數(shù)呢湾盗?我們再編寫一個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()引入模塊的時候胜卤,請注意模塊的相對路徑。因?yàn)閙ain.js和hello.js位于同一個目錄赁项,所以我們用了當(dāng)前目錄.:
var greet = require('./hello'); // 不要忘了寫相對目錄!
如果只寫模塊名:
var greet = require('hello');
則Node會依次在內(nèi)置模塊葛躏、全局模塊和當(dāng)前模塊下查找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規(guī)范
這種模塊加載機(jī)制被稱為CommonJS規(guī)范摩窃。在這個規(guī)范下,每個.js文件都是一個模塊芬骄,它們內(nèi)部各自使用的變量名和函數(shù)名都互不沖突偶芍,例如,hello.js和main.js都申明了全局變量var s = 'xxx'德玫,但互不影響匪蟀。
一個模塊想要對外暴露變量(函數(shù)也是變量),可以用module.exports = variable;
宰僧,一個模塊要引用其他模塊暴露的變量材彪,用var ref = require('module_name');
就拿到了引用模塊的變量。
Node模塊的原理
當(dāng)我們編寫JavaScript代碼時琴儿,我們可以申明全局變量:
var s = 'global';
在瀏覽器中段化,大量使用全局變量可不好。如果你在a.js
中使用了全局變量s造成,那么显熏,在b.js
中也使用全局變量s,將造成沖突晒屎,b.js
中對s賦值會改變a.js
的運(yùn)行邏輯喘蟆。
也就是說,JavaScript語言本身并沒有一種模塊機(jī)制來保證不同模塊可以使用相同的變量名鼓鲁。
那Node.js是如何實(shí)現(xiàn)這一點(diǎn)的蕴轨?
其實(shí)要實(shí)現(xiàn)“模塊”這個功能,并不需要語法層面的支持骇吭。Node.js也并不會增加任何JavaScript語法橙弱。實(shí)現(xiàn)“模塊”功能的奧妙就在于JavaScript是一種函數(shù)式編程語言,它支持閉包。如果我們把一段JavaScript代碼用一個函數(shù)包裝起來棘脐,這段代碼的所有“全局”變量就變成了函數(shù)內(nèi)部的局部變量斜筐。
請注意我們編寫的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)真朗?
這個也很容易實(shí)現(xiàn),Node可以先準(zhǔn)備一個對象module:
// 準(zhǔn)備module對象:
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)備的一個變量遮婶,并將其傳入加載函數(shù),我們在hello.js中可以直接使用變量module原因就在于它實(shí)際上是函數(shù)的一個參數(shù):
module.exports = greet;
通過把參數(shù)module傳遞給load()函數(shù)湖笨,hello.js就順利地把一個變量傳遞給了Node執(zhí)行環(huán)境旗扑,Node會把module變量保存到某個地方。
由于Node保存了所有導(dǎo)入的module慈省,當(dāng)我們用require()獲取module時臀防,Node找到對應(yīng)的module,把這個module的exports變量返回边败,這樣袱衷,另一個模塊就順利拿到了模塊的輸出:
var greet = require('./hello');
以上是Node實(shí)現(xiàn)JavaScript模塊的一個簡單的原理介紹。
module.exports vs exports
很多時候笑窜,你會看到致燥,在Node環(huán)境中,有兩種方法可以在一個模塊中輸出變量:
- 方法一:對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賦值:
// 代碼可以執(zhí)行排截,但是模塊并沒有輸出任何變量:
exports = {
hello: hello,
greet: greet
};
如果你對上面的寫法感到十分困惑嫌蚤,不要著急,我們來分析Node的加載機(jī)制:
首先断傲,Node會把整個待加載的hello.js文件放入一個包裝函數(shù)load中執(zhí)行搬葬。在執(zhí)行這個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í)際上是同一個變量,并且初始化為空對象{},于是抡锈,我們可以寫:
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)備了一個空對象{},這樣你可以直接往里面加?xùn)|西床三。
但是一罩,如果我們要輸出的是一個函數(shù)或數(shù)組,那么撇簿,只能給module.exports賦值:
module.exports = function () { return 'foo'; };
給exports賦值是無效的聂渊,因?yàn)橘x值后,module.exports仍然是空對象{}四瘫。
總結(jié)
- 在Node環(huán)境中汉嗽,一個.js文件就稱之為一個模塊(module)。
- module大大提高了代碼的可維護(hù)性找蜜;可以被其他地方引用饼暑;使用模塊還可以避免函數(shù)名和變量名沖突
- 要在模塊中對外輸出變量,用:
module.exports = variable;
輸出的變量可以是任意對象洗做、函數(shù)弓叛、數(shù)組等等。
- 引入其他模塊輸出的對象诚纸,用:
var foo = require('other_module');
引入的對象具體是什么撰筷,取決于引入模塊輸出的對象。
- 兩種方法輸出變量
//第一種
module.exports = {
hello: hello,
greet: greet
};
//第二種
exports.hello = hello;
exports.greet = greet;
- 直接對
module.exports
賦值畦徘,可以應(yīng)對任何情況闭专。