學(xué)習(xí)Node.js模塊機(jī)制
一、CommonJS的模塊規(guī)范
Node與瀏覽器以及 W3C組織、CommonJS組織、ECMAScript之間的關(guān)系
Node借鑒CommonJS的Modules規(guī)范實(shí)現(xiàn)了一套模塊系統(tǒng),所以先來看看CommonJS的模塊規(guī)范。
CommonJS對模塊的定義十分簡單,主要分為模塊引用、模塊定義和模塊標(biāo)識3個(gè)部分。
1. 模塊引用
模塊引用的示例代碼如下:
var math = require('math');
在CommonJS規(guī)范中,存在require()方法,這個(gè)方法接受模塊標(biāo)識,以此引入一個(gè)模塊的API到當(dāng)前上下文中。
2. 模塊定義
在模塊中,上下文提供require()方法來引入外部模塊。對應(yīng)引入的功能,上下文提供了exports對象用于導(dǎo)出當(dāng)前模塊的方法或者變量,并且它是唯一導(dǎo)出的出口。在模塊中,還存在一個(gè)module對象,它代表模塊自身,而exports是module的屬性。在Node中,一個(gè)文件就是一個(gè)模塊,將方法掛載在exports對象上作為屬性即可定義導(dǎo)出的方式:
// math.js exports.add = function () { var sum = 0, i = 0, args = arguments, l = args.length; while (i < l) { sum += args[i++]; } return sum; };
在另一個(gè)文件中,我們通過require()方法引入模塊后,就能調(diào)用定義的屬性或方法了:
// program.js var math = require('math'); exports.increment = function (val) { return math.add(val, 1);};
3.模塊標(biāo)識
模塊標(biāo)識其實(shí)就是傳遞給require()方法的參數(shù),它必須是符合小駝峰命名的字符串,或者以.、..開頭的相對路徑,或者絕對路徑。它可以沒有文件名后綴.js。模塊的定義十分簡單,接口也十分簡潔。它的意義在于將類聚的方法和變量等限定在私有的作用域中,同時(shí)支持引入和導(dǎo)出功能以順暢地連接上下游依賴。每個(gè)模塊具有獨(dú)立的空間,它們互不干擾,在引用時(shí)也顯得干凈利落。
二、Node的模塊實(shí)現(xiàn)
Node在實(shí)現(xiàn)中并非完全按照規(guī)范實(shí)現(xiàn),而是對模塊規(guī)范進(jìn)行了一定的取舍,同時(shí)也增加了少許自身需要的特性。盡管規(guī)范中exports、require和module聽起來十分簡單,但是Node在實(shí)現(xiàn)它們的過程中究竟經(jīng)歷了什么,這個(gè)過程需要知曉。
在Node中引入模塊,需要經(jīng)歷如下3個(gè)步驟。
1. 路徑分析
2. 文件定位
3. 編譯執(zhí)行
在Node中,模塊分為兩類:一類是Node提供的模塊,稱為核心模塊;另一類是用戶編寫的模塊,稱為文件模塊。
• 核心模塊部分在Node源代碼的編譯過程中,編譯進(jìn)了二進(jìn)制執(zhí)行文件。在Node進(jìn)程啟動時(shí),部分核心模塊就被直接加載進(jìn)內(nèi)存中,所以這部分核心模塊引入時(shí),文件定位和編譯執(zhí)行這兩個(gè)步驟可以省略掉,并且在路徑分析中優(yōu)先判斷,所以它的加載速度是最快的。
• 文件模塊則是在運(yùn)行時(shí)動態(tài)加載,需要完整的路徑分析、文件定位、編譯執(zhí)行過程,速度比核心模塊慢。
1.優(yōu)先從緩存加載
與前端瀏覽器會緩存靜態(tài)腳本文件以提高性能一樣,Node對引入過的模塊都會進(jìn)行緩存,以減少二次引入時(shí)的開銷。不同的地方在于,瀏覽器僅僅緩存文件,而Node緩存的是編譯和執(zhí)行之后的對象。不論是核心模塊還是文件模塊,require()方法對相同模塊的二次加載都一律采用緩存優(yōu)先的方式,這是第一優(yōu)先級的。不同之處在于核心模塊的緩存檢查先于文件模塊的緩存檢查。
2.路徑分析和文件定位
因?yàn)闃?biāo)識符有幾種形式,對于不同的標(biāo)識符,模塊的查找和定位有不同程度上的差異。
1). 模塊標(biāo)識符分析
Node基于一個(gè)模塊標(biāo)識符進(jìn)行模塊查找。模塊標(biāo)識符在Node中主要分為以下幾類。
核心模塊,如http、fs、path等。
.或..開始的相對路徑文件模塊。
以/開始的絕對路徑文件模塊。
非路徑形式的文件模塊,如自定義的connect模塊。
• 核心模塊
核心模塊的優(yōu)先級僅次于緩存加載,它在Node的源代碼編譯過程中已經(jīng)編譯為二進(jìn)制代碼,其加載過程最快。如果試圖加載一個(gè)與核心模塊標(biāo)識符相同的自定義模塊,那是不會成功的。如果自己編寫了一個(gè)http用戶模塊,想要加載成功,必須選擇一個(gè)不同的標(biāo)識符或者換用路徑的方式。
• 路徑形式的文件模塊
以.、..和/開始的標(biāo)識符,這里都被當(dāng)做文件模塊來處理。在分析路徑模塊時(shí),require()方法會將路徑轉(zhuǎn)為真實(shí)路徑,并以真實(shí)路徑作為索引,將編譯執(zhí)行后的結(jié)果存放到緩存中,以使二次加載時(shí)更快。由于文件模塊給Node指明了確切的文件位置,所以在查找過程中可以節(jié)約大量時(shí)間,其加載速度慢于核心模塊。
• 自定義模塊
自定義模塊指的是非核心模塊,也不是路徑形式的標(biāo)識符。它是一種特殊的文件模塊,可能是一個(gè)文件或者包的形式。這類模塊的查找是最費(fèi)時(shí)的,也是所有方式中最慢的一種。
2).文件定位
從緩存加載的優(yōu)化策略使得二次引入時(shí)不需要路徑分析、文件定位和編譯執(zhí)行的過程,大大提高了再次加載模塊時(shí)的效率。但在文件的定位過程中,還有一些細(xì)節(jié)需要注意,這主要包括文件擴(kuò)展名的分析、目錄和包的處理。
• 文件擴(kuò)展名分析
CommonJS模塊規(guī)范也允許在標(biāo)識符中不包含文件擴(kuò)展名,這種情況下,Node會按.js、.json、.node的次序補(bǔ)足擴(kuò)展名,依次嘗試。在嘗試的過程中,需要調(diào)用fs模塊同步阻塞式地判斷文件是否存在。因?yàn)镹ode是單線程的,所以這里是一個(gè)會引起性能問題的地方。小訣竅是:如果是.node和.json文件,在傳遞給require()的標(biāo)識符中帶上擴(kuò)展名,會加快一點(diǎn)速度。
• 目錄分析和包
在分析標(biāo)識符的過程中,require()通過分析文件擴(kuò)展名之后,可能沒有查找到對應(yīng)文件,但卻得到一個(gè)目錄,此時(shí)Node會將目錄當(dāng)做一個(gè)包來處理。
在這個(gè)過程中,Node對CommonJS包規(guī)范進(jìn)行了一定程度的支持。首先,Node在當(dāng)前目錄下查找package.json(CommonJS包規(guī)范定義的包描述文件),通過JSON.parse()解析出包描述對象,從中取出main屬性指定的文件名進(jìn)行定位。如果文件名缺少擴(kuò)展名,將會進(jìn)入擴(kuò)展名分析的步驟。而如果main屬性指定的文件名錯(cuò)誤,或者壓根沒有package.json文件,Node會將index當(dāng)做默認(rèn)文件名,然后依次查找index.js、index.node、index.json。
如果在目錄分析的過程中沒有定位成功任何文件,則自定義模塊進(jìn)入下一個(gè)模塊路徑進(jìn)行查找。如果模塊路徑數(shù)組都被遍歷完畢,依然沒有查找到目標(biāo)文件,則會拋出查找失敗的異常。
3).模塊編譯
在Node中,每個(gè)文件模塊都是一個(gè)對象,它的定義如下:
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; if (parent && parent.children) { parent.children.push(this); } this.filename = null; this.loaded = false; this.children = []; }
編譯和執(zhí)行是引入文件模塊的最后一個(gè)階段。定位到具體的文件后,Node會新建一個(gè)模塊對象,然后根據(jù)路徑載入并編譯。對于不同的文件擴(kuò)展名,其載入方法也有所不同,具體如下所示。
• .js文件。
通過fs模塊同步讀取文件后編譯執(zhí)行。
• .node文件。
這是用C/C++編寫的擴(kuò)展文件,通過dlopen()方法加載最后編譯生成的文件。
• .json文件。
通過fs模塊同步讀取文件后,用JSON.parse()解析返回結(jié)果。
• 其余擴(kuò)展名文件。
它們都被當(dāng)做.js文件載入。
每一個(gè)編譯成功的模塊都會將其文件路徑作為索引緩存在Module._cache對象上,以提高二次引入的性能。
JavaScript模塊的編譯
回到CommonJS模塊規(guī)范,我們知道每個(gè)模塊文件中存在著require、exports、module這3個(gè)變量,但是它們在模塊文件中并沒有定義,那么從何而來呢?甚至在Node的API文檔中,我們知道每個(gè)模塊中還有__filename、__dirname這兩個(gè)變量的存在,它們又是從何而來的呢?如果我們把直接定義模塊的過程放諸在瀏覽器端,會存在污染全局變量的情況。
事實(shí)上,在編譯的過程中,Node對獲取的JavaScript文件內(nèi)容進(jìn)行了頭尾包裝。在頭部添加了(function (exports, require, module, __filename, __dirname) {\n,在尾部添加了\n});。一個(gè)正常的JavaScript文件會被包裝成如下的樣子:
(function (exports, require, module, __filename, __dirname) { var math = require('math'); exports.area = function (radius) { return Math.PI * radius * radius; }; });
這樣每個(gè)模塊文件之間都進(jìn)行了作用域隔離。包裝之后的代碼會通過vm原生模塊的runInThisContext()方法執(zhí)行(類似eval,只是具有明確上下文,不污染全局),返回一個(gè)具體的function對象。最后,將當(dāng)前模塊對象的exports屬性、require()方法、module(模塊對象自身),以及在文件定位中得到的完整文件路徑和文件目錄作為參數(shù)傳遞給這個(gè)function()執(zhí)行。
3.包和NPM
在模塊之外,包和NPM則是將模塊聯(lián)系起來的一種機(jī)制。
CommonJS的包規(guī)范的定義其實(shí)也十分簡單,它由包結(jié)構(gòu)和包描述文件兩個(gè)部分組成,前者用于組織包中的各種文件,后者則用于描述包的相關(guān)信息,以供外部讀取分析。
1.包結(jié)構(gòu)
包實(shí)際上是一個(gè)存檔文件,即一個(gè)目錄直接打包為.zip或tar.gz格式的文件,安裝后解壓還原為目錄。完全符合CommonJS規(guī)范的包目錄應(yīng)該包含如下這些文件。
package.json:包描述文件。
bin:用于存放可執(zhí)行二進(jìn)制文件的目錄。
lib:用于存放JavaScript代碼的目錄。
doc:用于存放文檔的目錄。
test:用于存放單元測試用例的代碼。
2.包描述文件
包描述文件用于表達(dá)非代碼相關(guān)的信息,它是一個(gè)JSON格式的文件——package.json,位于包的根目錄下,是包的重要組成部分。而NPM的所有行為都與包描述文件的字段息息相關(guān)。
這個(gè)可以看看NPM官網(wǎng)對package.json的定義規(guī)范。
可以通過npm adduser, npm publish把自己的package上傳到npm倉庫。
三、題外話: AMD、CMD、兼容多種模塊規(guī)范的類庫
1. AMD
是CommonJS模塊規(guī)范的一個(gè)延伸,它的模塊定義如下:
define(id?, dependencies?, factory);
2.CMD
3.兼容
為了讓同一個(gè)模塊可以運(yùn)行在前后端,在寫作過程中需要考慮兼容前端也實(shí)現(xiàn)了模塊規(guī)范的環(huán)境。為了保持前后端的一致性,類庫開發(fā)者需要將類庫代碼包裝在一個(gè)閉包內(nèi)。以下代碼演示如何將hello()方法定義到不同的運(yùn)行環(huán)境中,它能夠兼容Node、AMD、CMD以及常見的瀏覽器環(huán)境中:
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
nodejs實(shí)現(xiàn)超簡單生成二維碼的方法
這篇文章主要介紹了nodejs實(shí)現(xiàn)超簡單生成二維碼的方法,結(jié)合實(shí)例形式分析了nodejs基于qr-image插件生成二維碼的相關(guān)操作技巧,需要的朋友可以參考下2018-03-03node.js入門教程之querystring模塊的使用方法
querystring模塊主要用來解析查詢字符串,下面這篇文章主要介紹了關(guān)于node.js中querystring模塊使用方法的相關(guān)資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-02-02