深入理解Commonjs規(guī)范及Node模塊實(shí)現(xiàn)

前面的話

10年積累的成都網(wǎng)站制作、做網(wǎng)站經(jīng)驗(yàn),可以快速應(yīng)對(duì)客戶對(duì)網(wǎng)站的新想法和需求。提供各種問(wèn)題對(duì)應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識(shí)你,你也不認(rèn)識(shí)我。但先做網(wǎng)站設(shè)計(jì)后付款的網(wǎng)站建設(shè)流程,更有懷遠(yuǎn)免費(fèi)網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。

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ì)外訪問(wèn),而使用window對(duì)象指向global對(duì)象而已

在javascript中,通過(guò)var a = 100;是可以通過(guò)window.a來(lái)得到100的

深入理解Commonjs規(guī)范及Node模塊實(shí)現(xiàn)

但在nodejs中,是不能通過(guò)global.a來(lái)訪問(wèn),得到的是undefined

深入理解Commonjs規(guī)范及Node模塊實(shí)現(xiàn)

這是因?yàn)関ar a = 100;這個(gè)語(yǔ)句中的變量a,只是模塊范圍內(nèi)的變量a,而不是global對(duì)象下的a

在nodejs中,一個(gè)文件就是一個(gè)模塊,每個(gè)模塊都有自己的作用域。使用var來(lái)聲明的一個(gè)變量,它并不是全局的,而是屬于當(dāng)前模塊下

如果要在全局作用域下聲明變量,則如下所示

深入理解Commonjs規(guī)范及Node模塊實(shí)現(xiàn)

 概述

Node中模塊分為兩類:一類是Node提供的模塊,稱為核心模塊;另一類是用戶編寫的模塊,稱為文件模塊

核心模塊部分在Node源代碼的編譯過(guò)程中,編譯進(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í)行過(guò)程,速度比核心模塊慢

接下來(lái),我們展開詳細(xì)的模塊加載過(guò)程

模塊加載

在javascript中,加載模塊使用script標(biāo)簽即可,而在nodejs中,如何在一個(gè)模塊中,加載另一個(gè)模塊呢?

使用require()方法來(lái)引入

深入理解Commonjs規(guī)范及Node模塊實(shí)現(xiàn)

【緩存加載】

再展開介紹require()方法的標(biāo)識(shí)符分析之前,需要知道,與前端瀏覽器會(huì)緩存靜態(tài)腳本文件以提高性能一樣,Node對(duì)引入過(guò)的模塊都會(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í)符的過(guò)程中,會(huì)出現(xiàn)標(biāo)識(shí)符中不包含文件擴(kuò)展名的情況。CommonJS模塊規(guī)范也允許在標(biāo)識(shí)符中不包含文件擴(kuò)展名,這種情況下,Node會(huì)先查找是否存在沒(méi)有后綴的該文件,如果沒(méi)有,再按.js、.json、.node的次序補(bǔ)足擴(kuò)展名,依次嘗試

在嘗試的過(guò)程中,需要調(diào)用fs模塊同步阻塞式地判斷文件是否存在。因?yàn)镹ode是單線程的,所以這里是一個(gè)會(huì)引起性能問(wèn)題的地方。小訣竅是:如果是.node和.json文件,在傳遞給require()的標(biāo)識(shí)符中帶上擴(kuò)展名,會(huì)加快一點(diǎn)速度。另一個(gè)訣竅是:同步配合緩存,可以大幅度緩解Node單線程中阻塞式調(diào)用的缺陷

【目錄分析和包】

在分析標(biāo)識(shí)符的過(guò)程中,require()通過(guò)分析文件擴(kuò)展名之后,可能沒(méi)有查找到對(duì)應(yīng)文件,但卻得到一個(gè)目錄,這在引入自定義模塊和逐個(gè)模塊路徑進(jìn)行查找時(shí)經(jīng)常會(huì)出現(xiàn),此時(shí)Node會(huì)將目錄當(dāng)做一個(gè)包來(lái)處理

在這個(gè)過(guò)程中,Node對(duì)CommonJS包規(guī)范進(jìn)行了一定程度的支持。首先,Node在當(dāng)前目錄下查找package.json(CommonJS包規(guī)范定義的包描述文件),通過(guò)JSON.parse()解析出包描述對(duì)象,從中取出main屬性指定的文件名進(jìn)行定位。如果文件名缺少擴(kuò)展名,將會(huì)進(jìn)入擴(kuò)展名分析的步驟

而如果main屬性指定的文件名錯(cuò)誤,或者壓根沒(méi)有package.json文件,Node會(huì)將index當(dāng)做默認(rèn)文件名,然后依次查找index.js、index.json、index.node

如果在目錄分析的過(guò)程中沒(méi)有定位成功任何文件,則自定義模塊進(jìn)入下一個(gè)模塊路徑進(jìn)行查找。如果模塊路徑數(shù)組都被遍歷完畢,依然沒(méi)有查找到目標(biāo)文件,則會(huì)拋出查找失敗的異常

 訪問(wèn)變量

如何在一個(gè)模塊中訪問(wèn)另外一個(gè)模塊中定義的變量呢?

【global】

最容易想到的方法,把一個(gè)模塊定義的變量復(fù)制到全局環(huán)境global中,然后另一個(gè)模塊訪問(wèn)全局環(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 = [];
}
  1. module.id 模塊的識(shí)別符,通常是帶有絕對(duì)路徑的模塊文件名。
  2. module.filename 模塊的文件名,帶有絕對(duì)路徑。
  3. module.loaded 返回一個(gè)布爾值,表示模塊是否已經(jīng)完成加載。
  4. module.parent 返回一個(gè)對(duì)象,表示調(diào)用該模塊的模塊。
  5. module.children 返回一個(gè)數(shù)組,表示該模塊要用到的其他模塊。
  6. module.exports 表示模塊對(duì)外輸出的值。

深入理解Commonjs規(guī)范及Node模塊實(shí)現(xiàn)

【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文件——通過(guò)fs模塊同步讀取文件后編譯執(zhí)行

node文件——這是用C/C++編寫的擴(kuò)展文件,通過(guò)dlopen()方法加載最后編譯生成的文件

json文件——通過(guò)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屬性,所以通過(guò)在代碼中訪問(wèn)require.extensions可以知道系統(tǒng)中已有的擴(kuò)展加載方式。編寫如下代碼測(cè)試一下:

console.log(require.extensions);

得到的執(zhí)行結(jié)果如下:

{ '.js': [Function], '.json': [Function], '.node': [Function] }

在確定文件的擴(kuò)展名之后,Node將調(diào)用具體的編譯方式來(lái)將文件執(zhí)行后返回給調(diào)用者

【JavaScript模塊的編譯】

回到CommonJS模塊規(guī)范,我們知道每個(gè)模塊文件中存在著require、exports、module這3個(gè)變量,但是它們?cè)谀K文件中并沒(méi)有定義,那么從何而來(lái)呢?甚至在Node的API文檔中,我們知道每個(gè)模塊中還有filename、dirname這兩個(gè)變量的存在,它們又是從何而來(lái)的呢?如果我們把直接定義模塊的過(guò)程放諸在瀏覽器端,會(huì)存在污染全局變量的情況

事實(shí)上,在編譯的過(guò)程中,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ì)通過(guò)vm原生模塊的runInThisContext()方法執(zhí)行(類似eval,只是具有明確上下文,不污染全局),返回一個(gè)具體的function對(duì)象。最后,將當(dāng)前模塊對(duì)象的exports屬性、require()方法、module(模塊對(duì)象自身),以及在文件定位中得到的完整文件路徑和文件目錄作為參數(shù)傳遞給這個(gè)function()執(zhí)行

這就是這些變量并沒(méi)有定義在每個(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),通過(guò)libuv兼容層進(jìn)行了封裝

實(shí)際上,.node的模塊文件并不需要編譯,因?yàn)樗蔷帉慍/C++模塊之后編譯生成的,所以這里只有加載和執(zhí)行的過(guò)程。在執(zhí)行的過(guò)程中,模塊的exports對(duì)象與.node模塊產(chǎn)生聯(lián)系,然后返回給調(diào)用者

C/C++模塊給Node使用者帶來(lái)的優(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í)也沒(méi)有性能影響

 CommonJS

在介紹完Node的模塊實(shí)現(xiàn)之后,回到頭來(lái)再學(xué)習(xí)下CommonJS規(guī)范,相對(duì)容易理解

CommonJS規(guī)范的提出,主要是為了彌補(bǔ)當(dāng)前javascript沒(méi)有標(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()方法來(lái)引入外部模塊。對(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è)文件中,我們通過(guò)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ì)路徑。它可以沒(méi)有文件名后綴.js

模塊的定義十分簡(jiǎn)單,接口也十分簡(jiǎn)潔。它的意義在于將類聚的方法和變量等限定在私有的作用域中,同時(shí)支持引入和導(dǎo)出功能以順暢地連接上下游依賴。每個(gè)模塊具有獨(dú)立的空間,它們互不干擾,在引用時(shí)也顯得干凈利落

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。

當(dāng)前標(biāo)題:深入理解Commonjs規(guī)范及Node模塊實(shí)現(xiàn)
標(biāo)題URL:http://muchs.cn/article2/piopoc.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供微信小程序、網(wǎng)站導(dǎo)航、品牌網(wǎng)站設(shè)計(jì)App設(shè)計(jì)、商城網(wǎng)站、外貿(mào)網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

成都seo排名網(wǎng)站優(yōu)化