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 索引,這樣就能夠方便的在瀏覽器環境中解析了,能夠參考 require1k 和 tiny-browser-require 的源碼來理解其解析(resolve)的過程。json
在 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.id 模塊的識別符,一般是帶有絕對路徑的模塊文件名。
module.filename 模塊的文件名,帶有絕對路徑。
module.loaded 返回一個布爾值,表示模塊是否已經完成加載。
module.parent 返回一個對象,表示調用該模塊的模塊。
module.children 返回一個數組,表示該模塊要用到的其餘模塊。
module.exports 表示模塊對外輸出的值。
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 有一個簡單的模塊加載系統。 在 Node.js 中,文件和模塊是一一對應的(每一個文件被視爲一個獨立的模塊)。廢話很少說,小夥伴們讓咱們一塊兒開啓 Node.js Module 的探索之旅吧,此次旅程咱們會帶着如下問題:
模塊中的 module、exports、__dirname、__filename 和 require 來自何方?
module.exports 與 exports 有什麼區別?
模塊出現循環依賴了,會出現死循環麼?
require 函數支持導入哪幾類文件?
require 函數執行的主要流程是什麼?
在此次旅程結束後,但願小夥伴對上述的問題,可以有一個較爲清楚的認識。
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-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 這些對象都是函數的輸入參數,在調用包裝後的函數時傳入。這時第一個問題先告一段落,咱們繼續探究第二個問題。
先不急着解釋它們之間的區別,咱們先來看一行代碼:
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 函數,支持的文件類型主要有 .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-16或UTF-32來將UCS/統一碼字符所組成的字符串編碼時,這個字符被用來標示其字節序。它常被用來當作標示文件是以UTF-8、UTF-16或UTF-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 函數執行的主要流程是什麼?
在加載對應模塊前,咱們首先須要定位文件的路徑,文件的定位是經過 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 模塊的加載過程。
若是傳遞給 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 模塊的相關知識,若是有寫得很差的地方,請各位小夥伴多多見諒。