前言
本人記憶力一般,為了讓自己理解《深入淺出Node.js-朴靈》一書,會在博客里記錄一些關鍵知識,以后忘了也可以在這里找到,快速回想起來
Node通過require、exports、module實現CommonJS模塊規范的
路徑分析
require('http') //如http、fs、path,速度僅次於緩存加載,它在node源代碼編譯過程中已經被編譯成二進制代碼,其加載速度最快
require('./a.txt') //以.或者..開始的相對路徑模塊
require('/a.txt') //以/開始的絕對路徑模塊
//以上兩種都當做文件模塊來處理,在分析路徑模塊時require()方法會將路徑轉化為真實路徑,並以真實路徑為索引,將編譯執行后的結果放到緩存中,以使二次加載更快。
//因為路徑模塊給了確切文件位置,所以在查找過程中可以節約大量時間,其加載慢於核心模塊。
require(*) //非路徑形式的文件模塊,如自定義的connect模塊//自己沒太理解這塊,因為可能沒實現過自己的自定義模塊所以舉不出例子
//特殊的文件模塊,可能是一個文件或者包的形式。這類模塊查找最費時,也是所有方式中最慢的。原因是和js原型鏈一樣要一層層node_modules找
文件定位
從緩存加載的優化策略使得二次引入不需要分析路徑分析、文件定位和編譯執行的過程,大大提高了再次加載時的效率
- 文件擴展名分析
require() 允許參數不帶后綴,在這種情況下,node會按照.js、.json、.node次序補足擴展名依次嘗試,在嘗試過程中需要調用fs模塊同步阻塞式判斷文件是否存在。
所以在后兩種引入方式時推薦加上后綴名
- 目錄分析和包
require() 通過分析文件擴展名之后,可能沒有查找到對應文件,但卻得到一個目錄,這在引入自定義模塊和逐個模塊路徑進行查找時經常會出現,此時node會將目錄當做一個包來處理。
在這個過程中,node對commonjs包規范進行了一定程度的支持。首先,node在當前目錄下查找package.json,通過json.parse解析出包描述對象從中取出Main屬性指定的文件名進行定位。
如果文件缺少擴展名,將會進入擴展名分析的步驟。
而如果main屬性指定的文件名錯誤或者壓根沒有package.json文件,node將會將index當做默認文件名,依次添加擴展名查找
如果在目錄分析的過程中沒有定位成功任務文件則自定義模塊進入下一個模塊路徑進行查找。如果模塊路徑數組都被遍歷完畢,依然沒有查找到目標文件,則會拋出查找失敗異常。
編譯執行
在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解析返回結果
// 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;
}
};
- 其余拓展名文件 都被當做js文件載入
每一個編譯成功的模塊都會將其文件路徑作為索引緩存在Modules._cache對象上,以提高二次加載速度
實踐
我們有個area.js
文件看看node的commonjs規范流程
- 暴露文件給node並會給下面js包裝
var math = require('math');
exports.area = function (radius) {
return Math.PI * radius * radius;
};
變為
(function (exports, require, module, __filename, __dirname) {
var math = require('math');
exports.area = function (radius) {
return Math.PI * radius * radius;
};
});
這樣每個模塊文件之間都進行了作用域隔離。包裝后的代碼會通過vm原生模塊的runInThisContext
方法執行(類似eval
,只是有明確的上下文,不污染全局),返回一個具體function對象。
最后將當前模塊對象的exports屬性、require方法、module(模塊自身)以及在文件定位中得到的完整文件路徑和文件目錄作為參數傳遞給這個function執行,執行后模塊的exports屬性返回給了調用方,
其他變量方法無法被調用。
- 外部文件引用
require('./area');
node通過上述的(路徑分析
)來查這個'./area'
,通過fs找到area.js文件(文件定位
)並讀取編譯執行(編譯執行
)
AMD與它的區別
AMD需要用define明確定義一個模塊,而在node實現中是隱形包裝的,目的是作用於隔離,僅在需要時被引入,避免掉過去那種全局變量或者命名空間的方式,防止被污染,另一個區別是內容需要通過返回的方式實現導出。
define(id?, dependencies?, factory);
define(function() {
var exports = {}; exports.sayHello = function() {
alert('Hello from module: ' + module.id); };
return exports; });
CMD與它的區別
AMD需要在聲明模塊的時候定義所有依賴,通過形參傳遞到模塊內容中,CMD支持動態引入依賴,require、exports、module通過形參傳遞給模塊,在需要依賴模塊時,隨時調用require引入即可
define(['dep1', 'dep2'], function (dep1, dep2) { return function () {};
});
define(function(require, exports, module) {
});