轉載地址:https://segmentfault.com/a/1190000008371376
前言
Node應用是由模塊組成的,Node遵循了CommonJS的模塊規範,來隔離每個模塊的作用域,使每個模塊在它自身的命名空間中執行。
CommonJS規範的主要內容:
模塊必須通過 module.exports 導出對外的變量或接口,通過 require() 來導入其他模塊的輸出到當前模塊作用域中。
CommonJS模塊的特點:
(1)所有代碼運行在當前模塊作用域中,不會污染全局作用域
(2)模塊同步加載,根據代碼中出現的順序依次加載
(3)模塊可以多次加載,但是隻會在第一次加載時運行一次,然後運行結果就被緩存了,以後再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。
一個簡單的例子:
demo.js
module.exports.name = 'Aphasia';
module.exports.getAge = function(age){
console.log(age)
};
//需要引入demo.js的其他文件
var person = require('./demo.js')
module對象
根據CommonJS規範,每一個文件就是一個模塊,在每個模塊中,都會有一個module對象,這個對象就指向當前的模塊。module對象具有以下屬性:
(1)id:當前模塊的bi
(2)exports:表示當前模塊暴露給外部的值
(3)parent: 是一個對象,表示調用當前模塊的模塊
(4)children:是一個對象,表示當前模塊調用的模塊
(5)filename:模塊的絕對路徑
(6)paths:從當前文件目錄開始查找node_modules目錄;然後依次進入父目錄,查找父目錄下的node_modules目錄;依次迭代,直到根目錄下的node_modules目錄
(7)loaded:一個布爾值,表示當前模塊是否已經被完全加載
示例:
module.js
module.exports = {
name: 'Aphasia',
getAge: function(age){
console.log(age)
}
}
console.log(module)
執行node module.js
1、module.exports
從上面的例子我們也能看到,module對象具有一個exports屬性,該屬性就是用來對外暴露變量、方法或整個模塊的。當其他的文件require進來該模塊的時候,實際上就是讀取了該模塊module對象的exports屬性。
簡單的使用示例
module.exports = 'Aphasia';
module.exports.name = 'Aphasia';
module.exports = function(){
//dosomething
}
module.exports = {
name: 'Aphasia',
getAge: function(){
//dosomething
}
}
2、exports對象
一開始我很鬱悶,既然module.exports就能滿足所有的需求,爲什麼還有個exports對象呢?其實,二者之間有下面的關係
(1)首先,exports和module.exports都是引用類型的變量,而且這兩個對象指向同一塊內存地址。在node中,二者一開始都是指向一個空對象的
exports = module.exports = {};
可以在REPL環境中直接運行下面代碼module.exports
,結果會輸出一個{}
(2)其次,exports對象是通過形參的方式傳入的,直接賦值形參會改變形參的引用,但是並不能改變作用域外的值。這句話是什麼意思呢?我們舉個例子。
var module = {
exports: {}
}
var exports = module.exports
function change(exports) {
//爲形參添加屬性,是會同步到外部的module.exports對象的
exports.name = "Aphasia"
//在這裏修改了exports的引用,並不會影響到module.exports
exports = {
age: 24
}
console.log(exports) //{ age: 24 }
}
change(exports)
console.log(module.exports) //{exports: {name: "Aphasia"}}
現在明白了吧?其實我們在模塊中像下面的代碼那樣,直接給exports賦值,會改變當前模塊內部的形參exports對象的引用,也就是說當前的exports已經跟外部的module.exports對象沒有任何關係了,所以這個改變是不會影響到module.exports的。因此,下面的這種方式是沒有任何效果的,所有的屬性和方法都不會被拋出。
//以下操作都是不起作用的
exports = 'Aphasia';
exports = function(){
console.log('Aphasia')
}
其實module.exports就是爲了解決上述exports直接賦值,會導致拋出不成功的問題而產生的。有了它,我們就可以這樣來拋出一個模塊了。
//這些操作都是合法的
exports.name = 'Aphasia';
exports.getName = function(){
console.log('Aphasia')
}
//相當於下面的方式
module.exports = {
name: 'Aphasia',
getName: function(){
console.log('Aphasia')
}
}
這樣就不用每次把要拋出的對象或方法賦值給exports的屬性了 ,直接採用對象字面量的方式更加方便。
模塊實例的require方法
我們都知道,當使用exports或者module.exports拋出一個模塊,通過給require()方法傳入模塊標識符參數,然後node根據一定的規則引入該模塊之後,我們就能使用模塊中定義的方法和屬性了。這裏要講的就是node的模塊引入規則。
1、node中引入模塊的機制
在Node中引入模塊,需要經歷3個步驟
(1)路徑分析
(2)文件定位
(3)編譯執行
在Node中,模塊一般分爲兩種
(1)Node提供的模塊,例如http、fs等,稱爲核心模塊。核心模塊在node源代碼編譯的過程中就編譯進了二進制執行文件,在Node進程啓動的時候,部分核心模塊就直接加載進內存中了,因此這部分模塊是不用經歷上述的(2)(3)兩個步驟的,而且在路徑分析中是優先判斷的,因此加載速度最快。
(2)用戶自己編寫的模塊,稱爲文件模塊。文件模塊是按需加載的,需要經歷上述的三個步驟,速度較慢。
優先從緩存中加載
與瀏覽器會緩存靜態腳本文件以提高頁面性能一樣,Node對引入過的模塊也會進行緩存。不同的地方是,node緩存的是編譯執行之後的對象而不是靜態文件。這一點我們可以用下面的方式來驗證。
modA.js
console.log('模塊modA開始加載...')
exports = function() {
console.log('Hi')
}
console.log('模塊modA加載完畢')
init.js
var mod1 = require('./modA')
var mod2 = require('./modA')
console.log(mod1 === mod2)
執行node init.js
,運行結果:
雖然我們兩次引入modA這個模塊,但是模塊中的代碼其實只執行了一遍。並且mod1和mod2指向了同一個模塊對象。
下面是Module._load的源碼:
Module._load = function(request, parent, isMain) {
// 計算絕對路徑
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有緩存,取出緩存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
// 第二步:是否爲內置模塊
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:生成模塊實例,存入緩存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加載模塊
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:輸出模塊的exports屬性
return module.exports;
};
對應流程如下圖所示:
2、路徑分析和文件定位
路徑分析
模塊標識符分析:
(1)核心模塊,如http、fs、path
(2)以.
或..
開始的相對路徑文件模塊
(3)以/
開始的絕對路徑文件模塊
(4)非路徑形式的文件模塊
1)核心模塊:優先級僅次於緩存,加載速度最快;如果自定義模塊與核心模塊名稱相同,加載是不會成功的。若想加載成功,必須選擇一個不同的名稱或者換用路徑。
2)路徑形式的文件模塊:以.
|| ..
|| /
開始的標識符,都會被當做文件模塊來處理。在加載的過程中,require方法會將路徑轉換爲真實的路徑,加載速度僅次於核心模塊
3) 非路徑形式的自定義模塊:這是一種特殊的文件模塊,可能是一個文件或者包的形式。查找這類模塊的策略類似於JS中作用域鏈,Node會逐個嘗試模塊路徑中的路徑,直到找到目標文件爲止。
模塊路徑: 這是Node在定位文件模塊的具體文件時指定的查找策略,具體表現爲一個路徑組成的數組。
可以在REPL環境中輸出Module對象,查看其path屬性的方式查看上述數組
文件定位:
-
文件擴展名分析
require()分析的標識符可以不包含擴展名,node會按.js、.node、.json的次序補足擴展名,依次嘗試
-
目標分析和包
如果在擴展名分析的步驟中,查找不到文件而是查找到相應目錄,此時node會將目錄當做包來處理,進行下一步分析查找當前目錄下package.json中的main屬性指定的文件名,若查找不成功則依次查找index.js,index.node,index.json。
如果目錄分析的過程中沒有定位到任何文件,則自定義模塊會進入下一個模塊路徑繼續查找,直到所有的模塊路徑都遍歷完畢,依然沒找到則拋出查找失敗的異常。
參考源碼
在Module._load方法的內部調用了Module._findPath這個方法,這個方法是用來返回模塊的絕對路徑的,源碼如下:
Module._findPath = function(request, paths) {
// 列出所有可能的後綴名:.js,.json, .node
var exts = Object.keys(Module._extensions);
// 如果是絕對路徑,就不再搜索
if (request.charAt(0) === '/') {
paths = [''];
}
// 是否有後綴的目錄斜槓
var trailingSlash = (request.slice(-1) === '/');
// 第一步:如果當前路徑已在緩存中,就直接返回緩存
var cacheKey = JSON.stringify({request: request, paths: paths});
if (Module._pathCache[cacheKey]) {
return Module._pathCache[cacheKey];
}
// 第二步:依次遍歷所有路徑
for (var i = 0, PL = paths.length; i < PL; i++) {
var basePath = path.resolve(paths[i], request);
var filename;
if (!trailingSlash) {
// 第三步:是否存在該模塊文件
filename = tryFile(basePath);
if (!filename && !trailingSlash) {
// 第四步:該模塊文件加上後綴名,是否存在
filename = tryExtensions(basePath, exts);
}
}
// 第五步:目錄中是否存在 package.json
if (!filename) {
filename = tryPackage(basePath, exts);
}
if (!filename) {
// 第六步:是否存在目錄名 + index + 後綴名
filename = tryExtensions(path.resolve(basePath, 'index'), exts);
}
// 第七步:將找到的文件路徑存入返回緩存,然後返回
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
// 第八步:沒有找到文件,返回false
return false;
};
3、清除緩存
根據上述的模塊引入機制我們知道,當我們第一次引入一個模塊的時候,require的緩存機制會將我們引入的模塊加入到內存中,以提升二次加載的性能。但是,如果我們修改了被引入模塊的代碼之後,當再次引入該模塊的時候,就會發現那並不是我們最新的代碼,這是一個麻煩的事情。如何解決呢?
查看require對象
require(): 加載外部模塊
require.resolve():將模塊名解析到一個絕對路徑
require.main:指向主模塊
require.cache:指向所有緩存的模塊
require.extensions:根據文件的後綴名,調用不同的執行函數
解決方法
//刪除指定模塊的緩存
delete require.cache[require.resolve('/*被緩存的模塊名稱*/')]
// 刪除所有模塊的緩存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
然後我們再重新require進來需要的模塊就可以了。