前言:工欲善其事,必先利其器。模塊系統是nodejs組織管理代碼的利器也是調用第三方代碼的途徑,本文將詳細講解nodejs的模塊系統。在文章最後實例分析一下exprots和module.exprots。
nodejs模塊的歷史淵源
CommonJS規範
早在Netscape誕生不久後,JavaScript就一直在探索本地編程的路,Rhino是其代表產物。無奈那時服務端JavaScript走的路均是參考衆多服務器端語言來實現的,在這樣的背景之下,一沒有特色,二沒有實用價值。但是隨着JavaScript在前端的應用越來越廣泛,以及服務端JavaScript的推動,JavaScript現有的規範十分薄弱,不利於JavaScript大規模的應用。那些以JavaScript爲宿主語言的環境中,只有本身的基礎原生對象和類型,更多的對象和API都取決於宿主的提供,所以,我們可以看到JavaScript缺少這些功能:
- JavaScript沒有模塊系統。沒有原生的支持密閉作用域或依賴管理。
- JavaScript沒有標準庫。除了一些核心庫外,沒有文件系統的API,沒有IO流API等。
- JavaScript沒有標準接口。沒有如Web Server或者數據庫的統一接口。
- JavaScript沒有包管理系統。不能自動加載和安裝依賴。
於是便有了CommonJS(http://www.commonjs.org)規範的出現,其目標是爲了構建JavaScript在包括Web服務器,桌面,命令行工具,及瀏覽器方面的生態系統。
CommonJS制定瞭解決這些問題的一些規範,而Node.js就是這些規範的一種實現。Node.js自身實現了require方法作爲其引入模塊的方法,同時NPM也基於CommonJS定義的包規範,實現了依賴管理和模塊自動安裝等功能。這裏我們將深入一下Node.js的require機制,npm會在下一篇文章進行講解。
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 。
後記
不知道我這樣講解大家能不能理解,如果有什麼地方不能理解或者有什麼地方有錯誤請及時和我聯繫。
相應的代碼放在github上:https://github.com/CleverFan/nodejs-class/tree/master/class3–nodejs%E6%A8%A1%E5%9D%97