深刻學習 Node.js Module

Node.js 遵循 CommonJS規範,該規範的核心思想是容許模塊經過 require 方法來同步加載所要依賴的其餘模塊,而後經過 exports 或 module.exports 來導出須要暴露的接口。CommonJS 規範是爲了解決 JavaScript 的做用域問題而定義的模塊形式,可使每一個模塊它自身的命名空間中執行。html

示例

add.js前端

module.exports =(a, b) =>a + b;node


calculate.jsgit


constadd =require("./add");github

console.log("Result: ", add(2,3));express

CommonJS 也有瀏覽器端的實現,其原理是現將全部模塊都定義好並經過 id 索引,這樣就能夠方便的在瀏覽器環境中解析了,能夠參考 require1ktiny-browser-require 的源碼來理解其解析(resolve)的過程。json

Node.js 模塊分類

在 Node.js 中包含如下幾類模塊:api

builtin module: Node.js 中以 C++ 形式提供的模塊,如 tcp_wrap、contextify 等數組

constants module: Node.js 中定義常量的模塊,用來導出如 signal,openssl 庫、文件訪問權限等常量的定義。如文件訪問權限中的 O_RDONLY,O_CREAT、signal 中的 SIGHUP,SIGINT 等。瀏覽器

native module: Node.js 中以 JavaScript 形式提供的模塊,如 http、https、fs 等。有些 native module 須要藉助於 builtin module 實現背後的功能。如對於 native 模塊 buffer , 仍是須要藉助 builtin node_buffer.cc 中提供的功能來實現大容量內存申請和管理,目的是可以脫離 V8 內存大小使用限制

3rd-party module: 以上模塊能夠統稱 Node.js 內建模塊,除此以外爲第三方模塊,典型的如 express 模塊。

module 對象

每一個模塊內部,都有一個 module 對象,表明當前模塊。它有如下屬性。

module.id 模塊的識別符,一般是帶有絕對路徑的模塊文件名。

module.filename 模塊的文件名,帶有絕對路徑。

module.loaded 返回一個布爾值,表示模塊是否已經完成加載。

module.parent 返回一個對象,表示調用該模塊的模塊。

module.children 返回一個數組,表示該模塊要用到的其餘模塊。

module.exports 表示模塊對外輸出的值。

Node.js vm

vm 模塊提供了一系列 API 用於在 V8 虛擬機環境中編譯和運行代碼。JavaScript 代碼能夠被編譯並當即運行,或編譯、保存而後再運行。

vm.runInThisContext(code[, options])

vm.runInThisContext() 在當前的global 對象的上下文中編譯並執行 code,最後返回結果。運行中的代碼沒法獲取本地做用域,但能夠獲取當前的 global 對象。

示例

constvm =require('vm');

letlocalVar ='initial value';

constvmResult = vm.runInThisContext('localVar = "vm";');

console.log('vmResult:', vmResult);

console.log('localVar:', localVar);

constevalResult =eval('localVar = "eval";');

console.log('evalResult:', evalResult);

console.log('localVar:', localVar);

// vmResult: 'vm', localVar: 'initial value'

// evalResult: 'eval', localVar: 'eval'

正因 vm.runInThisContext() 沒法獲取本地做用域,故 localVar 的值不變。相反,eval() 確實能獲取本地做用域,因此localVar的值被改變了。

Node.js Module

Node.js 有一個簡單的模塊加載系統。 在 Node.js 中,文件和模塊是一一對應的(每一個文件被視爲一個獨立的模塊)。廢話很少說,小夥伴們讓咱們一塊兒開啓 Node.js Module 的探索之旅吧,此次旅程咱們會帶着如下問題:

模塊中的 module、exports、__dirname、__filename 和 require 來自何方?

module.exports 與 exports 有什麼區別?

模塊出現循環依賴了,會出現死循環麼?

require 函數支持導入哪幾類文件?

require 函數執行的主要流程是什麼?

在此次旅程結束後,但願小夥伴對上述的問題,可以有一個較爲清楚的認識。

Module 基本使用

foo.js 模塊


constcircle =require('./circle.js');

console.log(`半徑爲 4 的圓的面積是${circle.area(4)}`);

circle.js 模塊


const{ PI } =Math;

exports.area =(r) =>PI * r **2;

exports.circumference =(r) =>2* PI * r;

circle.js 模塊導出了 area() 和 circumference() 兩個函數。 經過在特殊的 exports 對象上指定額外的屬性,函數和對象能夠被添加到模塊的根部。

在 circle.js 文件中,咱們使用了特殊的 exports 對象。其實除了 exports 以外,在模塊中咱們還能夠 module、__dirname、__filename 和 require 這些對象,那它們是從哪裏來的呢?好的,咱們來解答第一個問題。

模塊中的 module、exports、__dirname、__filename 和 require 來自何方?

固然首先我要先知道它們是什麼,這裏咱們新建一個模塊 module-var.js,輸入如下內容:


console.log(module);

console.log(exports);

console.log(__dirname);

console.log(__filename);

console.log(require);

執行完以上代碼,控制檯的輸出以下(忽略輸出對象中的大部分屬性):


Module { -------------------------------------------------> module

id: '.',

exports: {},

paths: [] // 模塊查找路徑

}


{} -------------------------------------------------> exports

/Users/fer/VSCProjects/learn-node/module -----------------------------> __dirname

/Users/fer/VSCProjects/learn-node/module/module-var.js -----------------> __filename

{ [Function: require] -------------------------------------------------> require

resolve: [Function: resolve],

main: Module { } // Module對象

}

經過控制檯的輸出值,咱們能夠清楚地看出每一個變量的值。這裏先不細究它們,咱們先來調查一下它們的來源。

CommonJS 規範是爲了解決 JavaScript 的做用域問題而定義的模塊形式,可使每一個模塊它自身的命名空間中執行。

那麼 CommonJS 規範是如何解決 JavaScript 的做用域問題,並讓每一個模塊在自身的命名空間中執行呢?不知道小夥伴們是否還記得,在前端的模塊方案出來以前,爲了不污染變量污染,咱們經過如下方式來建立獨立的運行空間:

global){

// some code

})(window)

那麼 Node.js 是否是也是經過這種方式來解決做用域問題和代碼封裝呢?

俗話說眼見爲實,咱們輸入 node --inspect-brk module-var.js 命令,調試一下前面建立的 module-var.js 文件:

經過上圖咱們能夠發現,module-var.js 文件中定義的內容,以 (function(){}) 這種形式被包裝了。這裏,咱們就清楚了,模塊中的 module、exports、__dirname、__filename 和 require 這些對象都是函數的輸入參數,在調用包裝後的函數時傳入。這時第一個問題先告一段落,咱們繼續探究第二個問題。

module.exports 與 exports 有什麼區別?

先不急着解釋它們之間的區別,咱們先來看一行代碼:


console.log(module.exports === exports);

運行完上面的代碼,控制檯會輸出 true。那好,咱們繼續往下看:

exports.id =1;// 方式一:能夠正常導出

exports = {id:1};// 方式二:沒法正常導出

module.exports = {id:1};// 方式三:能夠正常導出

爲何方式二沒法正常導出呢?讓咱們回顧一下運行 module-var.js 文件時, module 和 exports的輸出結果:

Module { -------------------------------------------------> module

id: '.',

exports: {},

paths: [] // 模塊查找路徑

}


{} -------------------------------------------------> exports

若是 module.exports === exports 執行的結果爲 true,那麼表示模塊中的 exports 變量與 module.exports 屬性是指向同一個對象。當使用方式二 exports = { id: 1 } 的方式會改變 exports 變量的指向,這時與module.exports 屬性指向不一樣的變量,而當咱們導入某個模塊時,是導入 module.exports 屬性指向的對象,具體緣由後面會細說。

但願經過上面的分析,小夥伴們可以清晰地瞭解 module.exports 與 exports 之間的區別和聯繫。接下來,咱們繼續第三個問題。

模塊出現循環依賴了,會出現死循環麼?

首先咱們先簡單解釋一下循環依賴,當模塊 a 執行時須要依賴模塊 b 中定義的屬性或方法,而在導入模塊 b 中,發現模塊 b 同時也依賴模塊 a 中的屬性或方法,即兩個模塊之間互相依賴,這種現象咱們稱之爲循環依賴。

介紹完循環依賴的概念,那出現這種狀況會出現死循環麼?咱們立刻來驗證一下:

module1.js


exports.a =1;

exports.b =2;

require("./module2");

exports.c =3;

module2.js


constModule1 =require('./module1');

console.log('Module1 is partially loaded here', Module1);

當咱們在命令行中輸入 node lib/module1.js 命令,你會發現程序正常運行,而且在控制檯輸出瞭如下內容:


Module1 is partially loaded here { a: 1, b: 2 }

經過實際驗證,咱們發現出現循環依賴的時候,程序並不會出現死循環,但只會輸出相應模塊已加載的部分數據。

解釋完模塊循環依賴的問題,咱們繼續下一個問題。

require 函數支持導入哪幾類文件?

模塊內的 require 函數,支持的文件類型主要有 .js 、.json 和 .node。其中 .js 和 .json 文件,相信你們都很熟悉了,.node 後綴的文件是 Node.js 的二進制文件。然而爲何 require 函數,只支持這三種文件格式呢?其實答案在模塊內輸出的 require 函數對象中:


{

[Function:require]

resolve: [Function: resolve],

main: Module {},

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

}

在 require 函數對象中,有一個 extensions 屬性,顧名思義表示它支持的擴展名。細心的小夥伴,可能已經看到了,每種擴展名對應的值都是函數對象。既然發現了它們的蹤影,咱們就來看一下它們的真面目。其實模塊內的 require 函數對象是經過 lib/internal/module.js 文件中的 makeRequireFunction 函數建立的,那咱們就來看一下該函數(代碼片斷):


functionmakeRequireFunction(mod){

constModule = mod.constructor;


functionrequire(path){

try{

exports.requireDepth +=1;

returnmod.require(path);

}finally{

exports.requireDepth -=1;

}

}


// Enable support to add extra extension types.

require.extensions = Module._extensions;

require.cache = Module._cache;

returnrequire;

}

經過以上咱們發現,模塊內的 require 函數對象,在導入模塊時,最終仍是經過調用 Module 對象的 require() 方法來實現模塊導入。此時,咱們的重點在 require.extensions = Module._extensions; 這行代碼上,哈哈,終於定位到了源頭。

繼續打開 lib/module.js 文件,咱們發現瞭如下的定義:


// Native extension for .js

Module._extensions['.js'] =function(module, filename){

varcontent = fs.readFileSync(filename,'utf8');

module._compile(internalModule.stripBOM(content), filename);

};

// Native extension for .json

Module._extensions['.json'] =function(module, filename){

varcontent = fs.readFileSync(filename,'utf8');

try{

module.exports =JSON.parse(internalModule.stripBOM(content));

}catch(err) {

err.message = filename +': '+ err.message;

throwerr;

}

};

//Native extension for .node

Module._extensions['.node'] =function(module, filename){

returnprocess.dlopen(module, path.toNamespacedPath(filename));

};

.json 的文件的處理邏輯很簡單,咱們就不進一步說明了。而 .node 文件的處理方式,由於涉及到 bindings 這個後面會有專門的文章介紹這塊內容。這裏咱們就來重點介紹 .js 文件的處理方式。


// Native extension for .js

Module._extensions['.js'] =function(module, filename){

varcontent = fs.readFileSync(filename,'utf8');// (1)

module._compile(internalModule.stripBOM(content), filename);// (2)

};

函數體中的第一行,咱們以同步的方式讀取對應的文件內容。而第二行中,咱們會對文件的內容進行編譯,然而在編譯前咱們會對內容進行處理,好比移除 BOM (Byte Order Mark),stripBOM 的具體實現以下:


functionstripBOM(content){

if(content.charCodeAt(0) ===0xFEFF) {

content = content.slice(1);

}

returncontent;

}

字節順序標記(英語:byte-order mark,BOM)是位於碼點U+FEFF的統一碼字符的名稱。當以UTF-16UTF-32來將UCS/統一碼字符所組成的字符串編碼時,這個字符被用來標示其字節序。它常被用來當作標示文件是以UTF-8UTF-16UTF-32編碼的記號。 —— 維基百科

接下來咱們就來重點看一下 _compile() 方法(代碼片斷):


Module.prototype._compile =function(content, filename){

// 在計算機科學中,Shebang(也稱爲 Hashbang )是一個由井號和歎號構成的字符序列#!

content = internalModule.stripShebang(content);

// create wrapper function

varwrapper = Module.wrap(content);// (1)

varcompiledWrapper = vm.runInThisContext(wrapper, {// (2)

filename: filename,

lineOffset:0,

displayErrors:true

});

vardirname = path.dirname(filename);

varrequire= internalModule.makeRequireFunction(this);

vardepth = internalModule.requireDepth;

if(depth ===0) stat.cache =newMap();

varresult;

if(inspectorWrapper) {

result = inspectorWrapper(compiledWrapper,this.exports,this.exports,

require,this, filename, dirname);

}else{

result = compiledWrapper.call(this.exports,this.exports,require,this,

filename, dirname);

}

if(depth ===0) stat.cache =null;

returnresult;

};

這裏咱們先來看 (1) 這一行,var wrapper = Module.wrap(content); ,即調用 Module 內部的封裝函數對模塊的原始內容進行封裝。Module.wrap 函數實現很簡單,具體以下:


Module.wrap =function(script){

returnModule.wrapper[0] + script + Module.wrapper[1];

};

Module.wrapper = [

'(function (exports, require, module, __filename, __dirname) { ',

'\n});'

];

看到這裏你是否是已經恍然大悟,原來模塊中的原始內容是在這個階段進行包裝的。包裝後的格式爲:


(function(exports, require, module, __filename, __dirname){

// 模塊原始內容

});

通過 Module.wrap 函數包裝後返回的字符串,會做爲 vm.runInThisContext() 方法的輸入參數,並調用該方法。

而後咱們把方法的返回值保存在 compiledWrapper 變量上,接着咱們會準備 compiledWrapper 對應函數對象的調用參數,最後經過 call() 方法調用該函數。

OK,咱們繼續下一個問題 —— require 函數執行的主要流程是什麼?

require 函數執行的主要流程是什麼?

在加載對應模塊前,咱們首先須要定位文件的路徑,文件的定位是經過 Module 內部的 _resolveFilename() 方法來實現,相關的僞代碼描述以下:


從 Y 路徑的模塊 require(X)

1. 若是 X 是一個核心模塊,

a. 返回核心模塊

b. 結束

2. 若是 X 是以 '/' 開頭

a. 設 Y 爲文件系統根目錄

3. 若是 X 是以 './' 或 '/' 或 '../' 開頭

a. 加載文件(Y + X)

b. 加載目錄(Y + X)

4. 加載Node模塊(X, dirname(Y))

5. 拋出 "未找到"

加載文件(X)

1. 若是 X 是一個文件,加載 X 做爲 JavaScript 文本。結束

2. 若是 X.js 是一個文件,加載 X.js 做爲 JavaScript 文本。結束

3. 若是 X.json 是一個文件,解析 X.json 成一個 JavaScript 對象。結束

4. 若是 X.node 是一個文件,加載 X.node 做爲二進制插件。結束

加載索引(X)

1. 若是 X/index.js 是一個文件,加載 X/index.js 做爲 JavaScript 文本。結束

3. 若是 X/index.json 是一個文件,解析 X/index.json 成一個 JavaScript 對象。結束

4. 若是 X/index.node 是一個文件,加載 X/index.node 做爲二進制插件。結束

加載目錄(X)

1. 若是 X/package.json 是一個文件,

a. 解析 X/package.json,查找 "main" 字段

b. let M = X + (json main 字段)

c. 加載文件(M)

d. 加載索引(M)

2. 加載索引(X)

加載Node模塊(X, START)

1. let DIRS=NODE_MODULES_PATHS(START)

2. for each DIR in DIRS:

a. 加載文件(DIR/X)

b. 加載目錄(DIR/X)

NODE_MODULES_PATHS(START)

1. let PARTS = path split(START)

2. let I = count of PARTS - 1

3. let DIRS = []

4. while I >= 0,

a. if PARTS[I] = "node_modules" CONTINUE

b. DIR = path join(PARTS[0 .. I] + "node_modules")

c. DIRS = DIRS + DIR

d. let I = I - 1

5. return DIRS

_resolveFilename() 方法內部的判斷邏輯比較複雜,感興趣的小夥伴,能夠斷點跟蹤一下整個執行過程。這裏咱們須要注意的是加載文件、加載索引和加載目錄的主要執行過程。

接下來咱們來看一下內部的 Module 對象的 require() 方法:


// Loads a module at the given file path. Returns that module's

// `exports` property.

Module.prototype.require =function(id){

if(typeofid !=='string') {

thrownewerrors.TypeError('ERR_INVALID_ARG_TYPE','id','string', id);

}

if(id ==='') {

thrownewerrors.Error('ERR_INVALID_ARG_VALUE',

'id', id,'must be a non-empty string');

}

returnModule._load(id,this,/* isMain */false);

};

經過源碼上的註釋,咱們清楚地知道了 require 函數的做用,即用來加載給定文件路徑的模塊,並返回相應模塊對象的 exports 屬性。趁熱打鐵,咱們繼續來看一下 Module._load() 方法(代碼片斷):


// Check the cache for the requested file.

// 1. If a module already exists in the cache: return its exports object.

// 2. If the module is native: call `NativeModule.require()` with the

// filename and return the result.

// 3. Otherwise, create a new module for the file and save it to the cache.

// Then have it load the file contents before returning its exports

// object.

Module._load =function(request, parent, isMain){

// 解析文件的具體路徑

varfilename = Module._resolveFilename(request, parent, isMain);

// 優先從緩存中獲取

varcachedModule = Module._cache[filename];

if(cachedModule) {

updateChildren(parent, cachedModule,true);

// 導出模塊的exports屬性

returncachedModule.exports;

}

// 判斷是否爲native module,如fs、http等

if(NativeModule.nonInternalExists(filename)) {

debug('load native module %s', request);

returnNativeModule.require(filename);

}

// Don't call updateChildren(), Module constructor already does.

// 建立新的模塊對象

varmodule=newModule(filename, parent);

if(isMain) {

process.mainModule =module;

module.id ='.';

}

// 緩存新建的模塊

Module._cache[filename] =module;

// 嘗試進行模塊加載

tryModuleLoad(module, filename);

returnmodule.exports;

};

經過源碼咱們能夠發現,模塊首次被加載後,會被緩存在 Module._cache 屬性中,以提升模塊的導入效率。但有些時候,咱們修改了已被緩存的模塊,但願其它模塊導入時,獲取到更新後的內容,那應該怎麼辦呢?針對這種狀況,咱們可使用如下方法清除指定緩存的模塊,或清理全部已緩存的模塊:


//刪除指定模塊的緩存

deleterequire.cache[require.resolve('/*被緩存的模塊名稱*/')]

// 刪除全部模塊的緩存

Object.keys(require.cache).forEach(function(key){

deleterequire.cache[key];

});

最後咱們再來簡單介紹一下從 node_modules 目錄加載,即經過 require('koa') 導入 Koa 模塊的加載過程。

從 node_modules 目錄加載

若是傳遞給 require() 的模塊標識符不是一個核心模塊,也沒有以 '/' 、 '../' 或 './' 開頭,則 Node.js 會從當前模塊的父目錄開始,嘗試從它的 /node_modules 目錄里加載模塊。 Node.js 不會附加 node_modules 到一個已經以 node_modules 結尾的路徑上。

若是仍是沒有找到,則移動到再上一層父目錄,直到文件系統的根目錄。

好比在 '/home/ry/projects/foo.js' 文件裏調用了 require('bar.js'),則 Node.js 會按如下順序查找:

/home/ry/projects/node_modules/bar.js

/home/ry/node_modules/bar.js

/home/node_modules/bar.js

/node_modules/bar.js

這使得程序本地化它們的依賴,避免它們產生衝突。經過在模塊名後包含一個路徑後綴,能夠請求特定的文件或分佈式的子模塊。 例如,require('example-module/path/to/file') 會把 path/to/file 解析成相對於 example-module 的位置。 後綴路徑一樣遵循模塊的解析語法。

總結

爲了可以更好地理解 Node.js Module 模塊,咱們介紹了 CommonJS、Node 模塊分類、Module 對象等相關的基礎知識。而後以一系列問題爲切入點,按部就班介紹了 module.exports 與 exports 對象的區別、模塊循環依賴、require 支持導入的文件類型及 require 函數執行的主要流程等相關的知識。最後咱們還介紹瞭如何清除已緩存的模塊,從而實現模塊更新和從 node_modules 目錄加載的相關內容。

但願本篇文章,可以幫你更好地理解並掌握 Node.js 模塊的相關知識,若是有寫得很差的地方,請各位小夥伴多多見諒。

相關文章
相關標籤/搜索