前言:工欲善其事,必先利其器。模塊系統是nodejs組織管理代碼的利器也是調用第三方代碼的途徑,本文將詳細講解nodejs的模塊系統。在文章最后實例分析一下exprots和module.exprots。
nodejs的模塊
什么是模塊?
node.js通過實現CommonJS的Modules/1.0標准引入了模塊(module)概念,模塊是Node.js的基本組成部分.一個node.js文件就是一個模塊,也就是說文件和模塊是一一對應的關系.這個文件可以是JavaScript代碼,JSON或者編譯過的C/C++擴展.
Node.js的模塊分為兩類,一類為原生(核心)模塊,一類為文件模塊。
在文件模塊中,又分為3類模塊。這三類文件模塊以后綴來區分,Node.js會根據后綴名來決定加載方法。
- .js。通過fs模塊同步讀取js文件並編譯執行。
- .node。通過C/C++進行編寫的Addon。通過dlopen方法進行加載。
- .json。讀取json文件,調用JSON.parse解析加載。
Node提供了exports和require兩個對象,其中exports是模塊公開的接口,require用於從外部獲取一個模塊接口,即所獲取模塊的exports對象
require和exports
require
require函數用於在當前模塊中加載和使用別的模塊,傳入一個模塊名,返回一個模塊導出對象。require方法接受以下幾種參數的傳遞:
- http、fs、path等。原生模塊。
- ./mod或../mod。相對路徑的文件模塊。
- /a/mod,絕對路徑的文件模塊。
- mod,非原生模塊的文件模塊。
exports
exports對象是當前模塊的導出對象,用於導出模塊公有方法和屬性。別的模塊通過require函數使用當前模塊時得到的就是當前模塊的exports對象。
module
通過module對象可以訪問到當前模塊的一些相關信息,但最多的用途是替換當前模塊的導出對象
-
module.exports :{Object}類型,模塊系統自動產生。
-
module.require(id):
id {String}
Return: {Object} 已解析模塊的 module.exports
這個方法提供了一種像 require() 一樣從最初的模塊加載一個模塊的方法。
-
module.id:{String}類型,用於區別模塊的標識符。通常是完全解析后的文件名
-
module.filename:{String}類型,模塊完全解析后的文件名。
-
module.loaded:{Boolean}類型,判斷該模塊是否加載完畢。
-
module.parent:{Module Object}類型,返回引入了本模塊的其他模塊。
-
module.children:{Array}類型,該模塊所引入的其他子模塊。
demo1 module.exports的使用
sayHello.js:
function sayHello() { console.log('hello'); } module.exports = sayHello;
app.js:
var sayHello = require('./sayHello'); sayHello(); //hello
代碼講解:
定義一個sayHello模塊,模塊里定義了一個sayHello方法,通過替換當前模塊exports對象的方式將sayHello方法導出。
在app.js中加載這個模塊,得到的是一個函數,調用該函數,控制台打印hello。
demo2 匿名替換
sayWorld.js
module.exports = function () { console.log('world'); }
app.js
var sayWorld = require('./sayWorld'); sayWorld(); //world
代碼講解
與上面稍有不同,這次是匿名替換。
demo3 替換為字符串
不僅可以替換為方法,也可以替換為字符串等。
stringMsg.js
module.exports = 'i am a string msg!';
app.js
var string = require('./stringMsg'); console.log(string); //i am a string msg!
demo4 exports導出多個變量
當要導出多個變量怎么辦呢?這個時候替換當前模塊對象的方法就不實用了,我們需要用到exports對象。
useExports.js
exports.a = function () { console.log('a exports'); } exports.b = function () { console.log('b exports'); }
app,js
var useExports = require('./useExports'); useExports.a(); useExports.b(); //a exports //b exports
當然,將useExports.js改成這樣也是可以的:
module.exports.a = function () { console.log('a exports'); } module.exports.b = function () { console.log('b exports'); }
下面通過gif圖進行演示:
module.exports和exports在文章的最后會進行詳細講解。
模塊初始化
一個模塊中的JS代碼僅在模塊第一次被使用時執行一次,並在執行過程中初始化模塊的導出對象。之后,緩存起來的導出對象被重復利用。
舉個例子,count,js:
var i = 0; function count() { return ++i; } exports.count = count;
app.js
var c1 = require('./count'); var c2 = require('./count'); console.log(c1.count()); console.log(c2.count()); console.log(co2.count()); //1 //2 //3
可以看到,count.js並沒有因為被require了兩次而初始化兩次。
主模塊
通過命令行參數傳遞給NodeJS以啟動程序的模塊被稱為主模塊。主模塊負責調度組成整個程序的其它模塊完成工作。例如通過以下命令啟動程序時,我們剛剛一直使用的app.js就是主模塊。
二進制模塊
雖然一般我們使用JS編寫模塊,但NodeJS也支持使用C/C++編寫二進制模塊。編譯好的二進制模塊除了文件擴展名是.node外,和JS模塊的使用方式相同。雖然二進制模塊能使用操作系統提供的所有功能,擁有無限的潛能,但對於不熟悉C/C++的人而言編寫過於困難,並且難以跨平台使用,因此本文不作講解。
模塊的加載優先級
由於Node.js中存在4類模塊(原生模塊和3種文件模塊),盡管require方法極其簡單,但是內部的加載卻是十分復雜的,其加載優先級也各自不同,下面是require加載的邏輯圖:
原生模塊在Node.js源代碼編譯的時候編譯進了二進制執行文件,加載的速度最快。另一類文件模塊是動態加載的,加載速度比原生模塊慢。但是Node.js對原生模塊和文件模塊都進行了緩存,於是在第二次require時,是不會有重復開銷的。
exports與module.exports
這里可能是最容易混淆的地方了。
我們先來看一個例子:
modOne.js
exports.hello = function () { console.log("hello"); } module.exports = function () { console.log('world'); }
app.js
var one = require('./modOne'); //one.hello(); //執行這句話會報錯one.hello is not a function one() //打印world
這是為什么呢?我們得先從exports 與module.exports 說起。
其實,exports 是module.exports的一個引用,exports 的地址指向module.exports。
而我們的modOne.js中通過module.exports = function的方式將module.exports給替換掉了。
而require方法所返回的是module.exports這個實實在在的對象,但是它已經被替換成了function,這就導致了exports指向了空,所以,你所定義的exports.hello是無效的。
用一個通俗易懂的例子來重新解釋一遍。
比如你在電腦的D盤下新建了一個exports文本文檔,然后你右鍵->發送到桌面快捷方式。
D盤就相當於nodejs中的module,這個exports文本文檔就相當於nodejs中模塊的exports對象,快捷方式就相當於nodejs中指向exports對象引用
D:/exportes.txt ==> module.exportes
exportes.txt快捷方式 ==> exportes
然后,你看exportes.txt不爽,把它給刪了,然后新建了一個word文檔–exports.docx。
這個時候你桌面上的快捷方式就沒用了,雖然也叫exports,但是你是訪問不到這個新的word文件的。
對於nodejs也一樣,當你把module.exportes對象覆蓋了,換成了其他東西的時候,exportes這個引用就失效了。
同樣,我們還可以用這個例子來理解為什么exportes也可以用來導出模塊。
我們是這樣使用exportes的:
exports.hello = function () { console.log("hello"); }
這段代碼其實等同於:
module.exports.hello = function () { console.log("hello"); }
怎么理解呢。還是剛才的txt文件,這次沒有刪除。
D:/exportes.txt ==> module.exportes
exportes.txt快捷方式 ==> exportes
你在桌面打開了exportes.txt快捷方式,然后在里面輸入hello,然后保存,關閉。
你再打開D:/exportes.txt,你會發現你可以看到剛剛寫的hello,你又在后面添加了一句“world”,保存關閉。
返回桌面,打開快捷方式,你會看到helloworld。
所以說你使用’exports.屬性’和’module.exportes.屬性’是等同的。
這也就能很好的解釋下面這個問題了:
exports = function() { console.log('hello'); } //這樣寫會報錯
這樣相當於把exprots這個引用覆蓋掉了,你把txt文件的快捷方式改成docx的快捷方式還能打開原來的txt文件么?顯然是不能的。
最后做一個總結:
當我們想讓模塊導出的是一個對象時, 使用exports 和 module.exports 都可以(但 exports 也不能重新覆蓋為一個新的對象),而當我們想導出非對象接口時,就必須也只能覆蓋 module.exports 。