火星人學node(基礎篇)---nodejs的模塊系統(實例分析exprots和module.exprots)

前言:工欲善其事,必先利其器。模塊系統是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

發佈了46 篇原創文章 · 獲贊 675 · 訪問量 75萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章