深入理解Commonjs規(guī)范及Node模塊實(shí)現(xiàn)
前面的話
Node在實(shí)現(xiàn)中并非完全按照CommonJS規(guī)范實(shí)現(xiàn),而是對(duì)模塊規(guī)范進(jìn)行了一定的取舍,同時(shí)也增加了少許自身需要的特性。本文將詳細(xì)介紹NodeJS的模塊實(shí)現(xiàn)
引入
nodejs是區(qū)別于javascript的,在javascript中的頂層對(duì)象是window,而在node中的頂層對(duì)象是global
[注意]實(shí)際上,javascript也存在global對(duì)象,只是其并不對(duì)外訪問,而使用window對(duì)象指向global對(duì)象而已
在javascript中,通過var a = 100;是可以通過window.a來得到100的

但在nodejs中,是不能通過global.a來訪問,得到的是undefined

這是因?yàn)関ar a = 100;這個(gè)語句中的變量a,只是模塊范圍內(nèi)的變量a,而不是global對(duì)象下的a
在nodejs中,一個(gè)文件就是一個(gè)模塊,每個(gè)模塊都有自己的作用域。使用var來聲明的一個(gè)變量,它并不是全局的,而是屬于當(dāng)前模塊下
如果要在全局作用域下聲明變量,則如下所示

概述
Node中模塊分為兩類:一類是Node提供的模塊,稱為核心模塊;另一類是用戶編寫的模塊,稱為文件模塊
核心模塊部分在Node源代碼的編譯過程中,編譯進(jìn)了二進(jìn)制執(zhí)行文件。在Node進(jìn)程啟動(dòng)時(shí),部分核心模塊就被直接加載進(jìn)內(nèi)存中,所以這部分核心模塊引入時(shí),文件定位和編譯執(zhí)行這兩個(gè)步驟可以省略掉,并且在路徑分析中優(yōu)先判斷,所以它的加載速度是最快的
文件模塊則是在運(yùn)行時(shí)動(dòng)態(tài)加載,需要完整的路徑分析、文件定位、編譯執(zhí)行過程,速度比核心模塊慢
接下來,我們展開詳細(xì)的模塊加載過程
模塊加載
在javascript中,加載模塊使用script標(biāo)簽即可,而在nodejs中,如何在一個(gè)模塊中,加載另一個(gè)模塊呢?
使用require()方法來引入

【緩存加載】
再展開介紹require()方法的標(biāo)識(shí)符分析之前,需要知道,與前端瀏覽器會(huì)緩存靜態(tài)腳本文件以提高性能一樣,Node對(duì)引入過的模塊都會(huì)進(jìn)行緩存,以減少二次引入時(shí)的開銷。不同的地方在于,瀏覽器僅僅緩存文件,而Node緩存的是編譯和執(zhí)行之后的對(duì)象
不論是核心模塊還是文件模塊,require()方法對(duì)相同模塊的二次加載都一律采用緩存優(yōu)先的方式,這是第一優(yōu)先級(jí)的。不同之處在于核心模塊的緩存檢查先于文件模塊的緩存檢查
【標(biāo)識(shí)符分析】
require()方法接受一個(gè)標(biāo)識(shí)符作為參數(shù)。在Node實(shí)現(xiàn)中,正是基于這樣一個(gè)標(biāo)識(shí)符進(jìn)行模塊查找的。模塊標(biāo)識(shí)符在Node中主要分為以下幾類:[1]核心模塊,如http、fs、path等;[2].或..開始的相對(duì)路徑文件模塊;[3]以/開始的絕對(duì)路徑文件模塊;[4]非路徑形式的文件模塊,如自定義的connect模塊
根據(jù)參數(shù)的不同格式,require命令去不同路徑尋找模塊文件
1、如果參數(shù)字符串以“/”開頭,則表示加載的是一個(gè)位于絕對(duì)路徑的模塊文件。比如,require('/home/marco/foo.js')將加載/home/marco/foo.js
2、如果參數(shù)字符串以“./”開頭,則表示加載的是一個(gè)位于相對(duì)路徑(跟當(dāng)前執(zhí)行腳本的位置相比)的模塊文件。比如,require('./circle')將加載當(dāng)前腳本同一目錄的circle.js
3、如果參數(shù)字符串不以“./“或”/“開頭,則表示加載的是一個(gè)默認(rèn)提供的核心模塊(位于Node的系統(tǒng)安裝目錄中),或者一個(gè)位于各級(jí)node_modules目錄的已安裝模塊(全局安裝或局部安裝)
[注意]如果是當(dāng)前路徑下的文件模塊,一定要以./開頭,否則nodejs會(huì)試圖去加載核心模塊,或node_modules內(nèi)的模塊
//a.js
console.log('aaa');
//b.js
require('./a');//'aaa'
require('a');//報(bào)錯(cuò)
【文件擴(kuò)展名分析】
require()在分析標(biāo)識(shí)符的過程中,會(huì)出現(xiàn)標(biāo)識(shí)符中不包含文件擴(kuò)展名的情況。CommonJS模塊規(guī)范也允許在標(biāo)識(shí)符中不包含文件擴(kuò)展名,這種情況下,Node會(huì)先查找是否存在沒有后綴的該文件,如果沒有,再按.js、.json、.node的次序補(bǔ)足擴(kuò)展名,依次嘗試
在嘗試的過程中,需要調(diào)用fs模塊同步阻塞式地判斷文件是否存在。因?yàn)镹ode是單線程的,所以這里是一個(gè)會(huì)引起性能問題的地方。小訣竅是:如果是.node和.json文件,在傳遞給require()的標(biāo)識(shí)符中帶上擴(kuò)展名,會(huì)加快一點(diǎn)速度。另一個(gè)訣竅是:同步配合緩存,可以大幅度緩解Node單線程中阻塞式調(diào)用的缺陷
【目錄分析和包】
在分析標(biāo)識(shí)符的過程中,require()通過分析文件擴(kuò)展名之后,可能沒有查找到對(duì)應(yīng)文件,但卻得到一個(gè)目錄,這在引入自定義模塊和逐個(gè)模塊路徑進(jìn)行查找時(shí)經(jīng)常會(huì)出現(xiàn),此時(shí)Node會(huì)將目錄當(dāng)做一個(gè)包來處理
在這個(gè)過程中,Node對(duì)CommonJS包規(guī)范進(jìn)行了一定程度的支持。首先,Node在當(dāng)前目錄下查找package.json(CommonJS包規(guī)范定義的包描述文件),通過JSON.parse()解析出包描述對(duì)象,從中取出main屬性指定的文件名進(jìn)行定位。如果文件名缺少擴(kuò)展名,將會(huì)進(jìn)入擴(kuò)展名分析的步驟
而如果main屬性指定的文件名錯(cuò)誤,或者壓根沒有package.json文件,Node會(huì)將index當(dāng)做默認(rèn)文件名,然后依次查找index.js、index.json、index.node
如果在目錄分析的過程中沒有定位成功任何文件,則自定義模塊進(jìn)入下一個(gè)模塊路徑進(jìn)行查找。如果模塊路徑數(shù)組都被遍歷完畢,依然沒有查找到目標(biāo)文件,則會(huì)拋出查找失敗的異常
訪問變量
如何在一個(gè)模塊中訪問另外一個(gè)模塊中定義的變量呢?
【global】
最容易想到的方法,把一個(gè)模塊定義的變量復(fù)制到全局環(huán)境global中,然后另一個(gè)模塊訪問全局環(huán)境即可
//a.js
var a = 100;
global.a = a;
//b.js
require('./a');
console.log(global.a);//100
這種方法雖然簡(jiǎn)單,但由于會(huì)污染全局環(huán)境,不推薦使用
【module】
而常用的方法是使用nodejs提供的模塊對(duì)象Module,該對(duì)象保存了當(dāng)前模塊相關(guān)的一些信息
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 = [];
}
- module.id 模塊的識(shí)別符,通常是帶有絕對(duì)路徑的模塊文件名。
- module.filename 模塊的文件名,帶有絕對(duì)路徑。
- module.loaded 返回一個(gè)布爾值,表示模塊是否已經(jīng)完成加載。
- module.parent 返回一個(gè)對(duì)象,表示調(diào)用該模塊的模塊。
- module.children 返回一個(gè)數(shù)組,表示該模塊要用到的其他模塊。
- module.exports 表示模塊對(duì)外輸出的值。

【exports】
module.exports屬性表示當(dāng)前模塊對(duì)外輸出的接口,其他文件加載該模塊,實(shí)際上就是讀取module.exports變量
//a.js
var a = 100;
module.exports.a = a;
//b.js
var result = require('./a');
console.log(result);//'{ a: 100 }'
為了方便,Node為每個(gè)模塊提供一個(gè)exports變量,指向module.exports。造成的結(jié)果是,在對(duì)外輸出模塊接口時(shí),可以向exports對(duì)象添加方法
console.log(module.exports === exports);//true
[注意]不能直接將exports變量指向一個(gè)值,因?yàn)檫@樣等于切斷了exports與module.exports的聯(lián)系
模塊編譯
編譯和執(zhí)行是模塊實(shí)現(xiàn)的最后一個(gè)階段。定位到具體的文件后,Node會(huì)新建一個(gè)模塊對(duì)象,然后根據(jù)路徑載入并編譯。對(duì)于不同的文件擴(kuò)展名,其載入方法也有所不同,具體如下所示
js文件——通過fs模塊同步讀取文件后編譯執(zhí)行
node文件——這是用C/C++編寫的擴(kuò)展文件,通過dlopen()方法加載最后編譯生成的文件
json文件——通過fs模塊同步讀取文件后,用JSON.parse()解析返回結(jié)果
其余擴(kuò)展名文件——它們都被當(dāng)做.js文件載入
每一個(gè)編譯成功的模塊都會(huì)將其文件路徑作為索引緩存在Module._cache對(duì)象上,以提高二次引入的性能
根據(jù)不同的文件擴(kuò)展名,Node會(huì)調(diào)用不同的讀取方式,如.json文件的調(diào)用如下:
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
try {
module.exports = JSON.parse(stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};
其中,Module._extensions會(huì)被賦值給require()的extensions屬性,所以通過在代碼中訪問require.extensions可以知道系統(tǒng)中已有的擴(kuò)展加載方式。編寫如下代碼測(cè)試一下:
console.log(require.extensions);
得到的執(zhí)行結(jié)果如下:
{ '.js': [Function], '.json': [Function], '.node': [Function] }
在確定文件的擴(kuò)展名之后,Node將調(diào)用具體的編譯方式來將文件執(zhí)行后返回給調(diào)用者
【JavaScript模塊的編譯】
回到CommonJS模塊規(guī)范,我們知道每個(gè)模塊文件中存在著require、exports、module這3個(gè)變量,但是它們?cè)谀K文件中并沒有定義,那么從何而來呢?甚至在Node的API文檔中,我們知道每個(gè)模塊中還有filename、dirname這兩個(gè)變量的存在,它們又是從何而來的呢?如果我們把直接定義模塊的過程放諸在瀏覽器端,會(huì)存在污染全局變量的情況
事實(shí)上,在編譯的過程中,Node對(duì)獲取的JavaScript文件內(nèi)容進(jìn)行了頭尾包裝。在頭部添加了(function(exports, require, module, filename, dirname) {\n,在尾部添加了\n});
一個(gè)正常的JavaScript文件會(huì)被包裝成如下的樣子
(function (exports, require, module, filename, dirname) {
var math = require('math');
exports.area = function (radius) {
return Math.PI * radius * radius;
};
});
這樣每個(gè)模塊文件之間都進(jìn)行了作用域隔離。包裝之后的代碼會(huì)通過vm原生模塊的runInThisContext()方法執(zhí)行(類似eval,只是具有明確上下文,不污染全局),返回一個(gè)具體的function對(duì)象。最后,將當(dāng)前模塊對(duì)象的exports屬性、require()方法、module(模塊對(duì)象自身),以及在文件定位中得到的完整文件路徑和文件目錄作為參數(shù)傳遞給這個(gè)function()執(zhí)行
這就是這些變量并沒有定義在每個(gè)模塊文件中卻存在的原因。在執(zhí)行之后,模塊的exports屬性被返回給了調(diào)用方。exports屬性上的任何方法和屬性都可以被外部調(diào)用到,但是模塊中的其余變量或?qū)傩詣t不可直接被調(diào)用
至此,require、exports、module的流程已經(jīng)完整,這就是Node對(duì)CommonJS模塊規(guī)范的實(shí)現(xiàn)
【C/C++模塊的編譯】
Node調(diào)用process.dlopen()方法進(jìn)行加載和執(zhí)行。在Node的架構(gòu)下,dlopen()方法在Windows和*nix平臺(tái)下分別有不同的實(shí)現(xiàn),通過libuv兼容層進(jìn)行了封裝
實(shí)際上,.node的模塊文件并不需要編譯,因?yàn)樗蔷帉慍/C++模塊之后編譯生成的,所以這里只有加載和執(zhí)行的過程。在執(zhí)行的過程中,模塊的exports對(duì)象與.node模塊產(chǎn)生聯(lián)系,然后返回給調(diào)用者
C/C++模塊給Node使用者帶來的優(yōu)勢(shì)主要是執(zhí)行效率方面的,劣勢(shì)則是C/C++模塊的編寫門檻比JavaScript高
【JSON文件的編譯】
.json文件的編譯是3種編譯方式中最簡(jiǎn)單的。Node利用fs模塊同步讀取JSON文件的內(nèi)容之后,調(diào)用JSON.parse()方法得到對(duì)象,然后將它賦給模塊對(duì)象的exports,以供外部調(diào)用
JSON文件在用作項(xiàng)目的配置文件時(shí)比較有用。如果你定義了一個(gè)JSON文件作為配置,那就不必調(diào)用fs模塊去異步讀取和解析,直接調(diào)用require()引入即可。此外,你還可以享受到模塊緩存的便利,并且二次引入時(shí)也沒有性能影響
CommonJS
在介紹完Node的模塊實(shí)現(xiàn)之后,回到頭來再學(xué)習(xí)下CommonJS規(guī)范,相對(duì)容易理解
CommonJS規(guī)范的提出,主要是為了彌補(bǔ)當(dāng)前javascript沒有標(biāo)準(zhǔn)的缺陷,使其具備開發(fā)大型應(yīng)用的基礎(chǔ)能力,而不是停留在小腳本程序的階段
CommonJS對(duì)模塊的定義十分簡(jiǎn)單,主要分為模塊引用、模塊定義和模塊標(biāo)識(shí)3個(gè)部分
【模塊引用】
var math = require('math');
在CommonJS規(guī)范中,存在require()方法,這個(gè)方法接受模塊標(biāo)識(shí),以此引入一個(gè)模塊的API到當(dāng)前上下文中
【模塊定義】
在模塊中,上下文提供require()方法來引入外部模塊。對(duì)應(yīng)引入的功能,上下文提供了exports對(duì)象用于導(dǎo)出當(dāng)前模塊的方法或者變量,并且它是唯一導(dǎo)出的出口。在模塊中,還存在一個(gè)module對(duì)象,它代表模塊自身,而exports是module的屬性。在Node中,一個(gè)文件就是一個(gè)模塊,將方法掛載在exports對(duì)象上作為屬性即可定義導(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);
};
【模塊標(biāo)識(shí)】
模塊標(biāo)識(shí)其實(shí)就是傳遞給require()方法的參數(shù),它必須是符合小駝峰命名的字符串,或者以.、..開頭的相對(duì)路徑,或者絕對(duì)路徑。它可以沒有文件名后綴.js
模塊的定義十分簡(jiǎn)單,接口也十分簡(jiǎn)潔。它的意義在于將類聚的方法和變量等限定在私有的作用域中,同時(shí)支持引入和導(dǎo)出功能以順暢地連接上下游依賴。每個(gè)模塊具有獨(dú)立的空間,它們互不干擾,在引用時(shí)也顯得干凈利落
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
用npm install時(shí)報(bào)錯(cuò)node-sass npm ERR command
在用npm install時(shí)報(bào)錯(cuò)npm ERR! path D:…\node-sass和npm ERR! command failed 問題,本文給大家介紹了如何解決這個(gè)問題,文中通過圖文給大家介紹的非常詳細(xì),需要的朋友可以參考下2024-03-03
Nodejs中Express 常用中間件 body-parser 實(shí)現(xiàn)解析
這篇文章主要介紹了Nodejs中Express 常用中間件 body-parser 實(shí)現(xiàn)解析,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05
node.js基于dgram數(shù)據(jù)報(bào)模塊創(chuàng)建UDP服務(wù)器和客戶端操作示例
這篇文章主要介紹了node.js基于dgram數(shù)據(jù)報(bào)模塊創(chuàng)建UDP服務(wù)器和客戶端操作,結(jié)合實(shí)例形式分析了node.js使用dgram數(shù)據(jù)報(bào)模塊創(chuàng)建UDP服務(wù)器和客戶端,以及進(jìn)行UDP廣播、組播相關(guān)操作技巧,需要的朋友可以參考下2020-02-02
ajax +NodeJS 實(shí)現(xiàn)圖片上傳實(shí)例
本篇文章主要介紹了ajax +NodeJS 實(shí)現(xiàn)圖片上傳實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
node-sass@4.14.1報(bào)錯(cuò)的最終解決方案分享
最近在安裝node-sass@4.14.1的時(shí)候遇到了些問題,所以下面這篇文章主要給大家介紹了關(guān)于node-sass@4.14.1報(bào)錯(cuò)的最終解決方案,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09
詳解Puppeteer前端自動(dòng)化測(cè)試實(shí)踐
這篇文章主要介紹了詳解Puppeteer前端自動(dòng)化測(cè)試實(shí)踐,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-02-02

