在Node中,每個文件模塊都是一個對象,它的定義如下:
~~~
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 = [];
}
~~~
編譯和執行是引入文件模塊的最后一個階段。定位到具體的文件后,Node會新建一個模塊對象,然后根據路徑載入并編譯。對于不同文件的擴展名,其載入方法也有所不同,具體如下所示:
* .js文件。通過fs模塊同步讀取文件后編譯執行。
* .node文件。這是用C/C++編寫的擴展文件,通過dlopen()方法加載最后編譯生成的文件。
* .json文件。通過fs模塊同步讀取文件后,用JSON.parse()方法解析返回結果。
* 其余擴展名文件。它們都會被當作.js文件載入。
每一個編譯成功的模塊都會將其文件路徑作為索引緩存在`Module._cache`對象上,以提高二次引入的性能。
根據不同的文件擴展名,Node會調用不同的讀取方式,如.json文件的調用如下:
~~~
//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會被賦值給require()的extensions屬性,所以通過在代碼中訪問require.extensions可以知道系統中已有的擴展加載方式。編寫如下代碼測試一下:
~~~
console.log(require.extensions);
~~~
得到的執行結果如下:
~~~
{ '.js': [Function], '.json': [Function], '.node': [Function] }
~~~
如果想對自定義的擴展名進行特殊的加載,可以通過類似require.extensions['.ext']的方式實現。早起的CoffeeScript文件就是通過添加require.extensions['.coffee']擴展的方式來實現加載的。但是從v0.10.6版本開始,官方不鼓勵通過這種方式來進行自定義擴展名的加載,而是期望先將其它語言或文件編譯成JavaScript文件后再加載,這樣做的好處在于不將繁瑣的編譯加載等過程引入Node的執行過程中。
在確定文件的擴展名之后,Node將調用具體的編譯方式來將文件執行后返回給調用者。
## 1.JavaScript模塊的編譯
回到CommonJS模塊規范,我們知道每個模塊文件中存在著require、exports、module這三個變量,但是它們在模塊文件中并沒有定義,那么從何而來呢?甚至在Node的API文檔中,我們知道每個模塊還有`__filename`、`__dirname`這兩個變量的存在,它們又是從何而來呢?如果我們把直接定義模塊的過程放諸在瀏覽器端,會存在污染全局變量的情況。
事實上,在編譯過程中,Node對獲取的JavaScript文件內容進行了頭尾包裝。在頭部添加了`(function(exports,require,module,__filename,__dirname){\n`,在尾部添加了`\n});`。
一個正常的JavaScript會被包裝成如下的樣子:
~~~
(function(exports, require,module,__filename,__dirname){
var math = require('math');
exports.area = function(radius){
return Math.PI * radius * radius;
};
});
~~~
這樣每個模塊文件之間都進行了作用于隔離。包裝之后的代碼會通過vm原生模塊的runInThisContex() 方法執行(類似eval,只是具有明確上下文,不污染全局),返回一個具體的function對象。最后,將當前模塊對象的exports屬性、require()方法、module(模塊對象自身),以及在文件定位中得到的完整文件路徑和文件目錄作為參數傳遞給這個function()執行。
這就是這些變量并沒有定義在每個模塊文件中卻存在的原因。在執行之后,模塊的exports屬性被返回給了調用方。exports屬性上的任何方法和屬性都可以被外部調用到,但是模塊中的其余變量或屬性則不可直接被調用。
至此,require、exports、module的流程已經完整,這就是Node對CommonJS規范的實現。
此外,許多初學者都曾經糾結過為何存在exports的情況下,還有module.exports。理想情況下,只要賦值給exports即可:
~~~
exports = function(){
//my class
};
~~~
但是通常都會得到一個失敗的結果。其原因在于,exports對象是通過形參的方式傳入的,直接賦值形參會改變形參的引用,但并不能改變作用域外的值。測試代碼如下:
~~~
var chage = (a)=>{
a=100;
console.log(a); // => 100
};
var a = 10;
chage(a);
console.log(a); // => 10
~~~
如果要達到require引入一個類的效果,請賦值給module.exports對象。這個迂回的方案不改變形參的引用。
## 2.C/C++模塊的編譯
Node調用process.dlopen()方法進行加載和執行。在Node的架構下,dlopen()方法在Windows和 *nix 平臺下分別有不同的實現,通過 libuv兼容層進行了封裝。
實際上,.node的模塊文件并不需要編譯,因為它是編寫C/C++模塊之后編譯生成的,所以這里只有加載和執行的過程。在執行的過程中,模塊的exports對象與.node模塊產生聯系,然后返回給調用者。
C/C++模塊給Node使用者帶來的優勢主要是執行效率方面的,劣勢則是C/C++模塊的編寫門檻比JavaScript高。
## 3.JSON文件的編譯
.json文件的編譯是3種編譯方式中最簡單的。Node利用fs模塊同步讀取JSON文件的內容之后,調用JSON.parse()方法得到對象,然后將它賦給模塊對象的exports,以供外部調用。
JSON文件在用作項目的配置文件時比較有用。如果你定義了一個JSON文件作為配置,那就不必調用fs模塊去異步讀取和解析,直接調用require()引入即可。此外,你還可以享受到模塊緩存的便利,并且二次引入時也沒有性能影響。
這里我們提到的模塊編譯都是指文件模塊,即用戶自己編寫的模塊。在下一節中,我們將展開介紹核心模塊中的JavaScript模塊和C/C++模塊。
- 目錄
- 第1章 Node 簡介
- 1.1 Node 的誕生歷程
- 1.2 Node 的命名與起源
- 1.2.1 為什么是 JavaScript
- 1.2.2 為什么叫 Node
- 1.3 Node給JavaScript帶來的意義
- 1.4 Node 的特點
- 1.4.1 異步 I/O
- 1.4.2 事件與回調函數
- 1.4.3 單線程
- 1.4.4 跨平臺
- 1.5 Node 的應用場景
- 1.5.1 I/O 密集型
- 1.5.2 是否不擅長CPU密集型業務
- 1.5.3 與遺留系統和平共處
- 1.5.4 分布式應用
- 1.6 Node 的使用者
- 1.7 參考資源
- 第2章 模塊機制
- 2.1 CommonJS 規范
- 2.1.1 CommonJS 的出發點
- 2.1.2 CommonJS 的模塊規范
- 2.2 Node 的模塊實現
- 2.2.1 優先從緩存加載
- 2.2.2 路徑分析和文件定位
- 2.2.3 模塊編譯
- 2.3 核心模塊
- 2.3.1 JavaScript核心模塊的編譯過程
- 2.3.2 C/C++核心模塊的編譯過程
- 2.3.3 核心模塊的引入流程
- 2.3.4 編寫核心模塊
- 2.4 C/C++擴展模塊
- 2.4.1 前提條件
- 2.4.2 C/C++擴展模塊的編寫
- 2.4.3 C/C++擴展模塊的編譯
- 2.4.2 C/C++擴展模塊的加載
- 2.5 模塊調用棧
- 2.6 包與NPM
- 2.6.1 包結構
- 2.6.2 包描述文件與NPM
- 2.6.3 NPM常用功能
- 2.6.4 局域NPM
- 2.6.5 NPM潛在問題
- 2.7 前后端共用模塊
- 2.7.1 模塊的側重點
- 2.7.2 AMD規范
- 2.7.3 CMD規范
- 2.7.4 兼容多種模塊規范
- 2.8 總結
- 2.9 參考資源
- 第3章 異步I/O
- 3.1 為什么要異步I/O
- 3.1.1 用戶體驗
- 3.1.2 資源分配
- 3.2 異步I/O實現現狀
- 3.2.1 異步I/O與非阻塞I/O
- 3.2.2 理想的非阻塞異步I/O
- 3.2.3 現實的異步I/O
- 3.3 Node的異步I/O
- 3.3.1 事件循環
- 3.3.2 觀察者
- 3.3.3 請求對象
- 3.3.4 執行回調
- 3.3.5 小結
- 3.4 非I/O的異步API
- 3.4.1 定時器
- 3.5 事件驅動與高性能服務器