咱們都知道Nodejs遵循的是CommonJS
規範,當咱們require('moduleA')
時,模塊是怎麼經過名字或者路徑獲取到模塊的呢?首先要聊一下模塊引用、模塊定義、模塊標識三個概念。javascript
CommonJS
規範模塊上下文提供require()
方法來引入外部模塊,看似簡單的require函數, 其實內部作了大量工做。示例代碼以下:java
//test.js
//引入一個模塊到當前上下文中
const math = require('math');
math.add(1, 2);
複製代碼
模塊上下文提供了exports
對象用於導入導出當前模塊的方法或者變量,而且它是惟一的導出出口。模塊中存在一個module
對象,它表明模塊自身,exports
是module的屬性。一個文件就是一個模塊,將方法做爲屬性掛載在exports上就能夠定義導出的方式:node
//math.js
exports.add = function () {
let sum = 0, i = 0, args = arguments, l = args.length;
while(i < l) {
sum += args[i++];
}
return sum;
}
複製代碼
這樣就可像test.js
裏那樣在require()以後調用模塊的屬性或者方法了。json
模塊標識就是傳遞給require()
方法的參數,它必須是符合小駝峯命名的字符串,或者以.
、..
開頭的相對路徑或者絕對路徑,能夠沒有文件後綴名.js
.數組
在Node中引入模塊,須要經歷以下四個步驟:瀏覽器
Node.js中模塊能夠經過文件路徑或名字獲取模塊的引用。模塊的引用會映射到一個js文件路徑。 在Node中模塊分爲兩類:緩存
const mod = require('module_name')
const { methodA } = require('module_name')
複製代碼
執行後,Node內部會載入內置模塊或經過NPM安裝的模塊。require函數會返回一個對象,該對象公開的API多是函數、對象或者屬性如函數、數組甚至任意類型的JS對象。bash
核心模塊是Node源碼在編譯過程當中編譯進了二進制執行文件。在Node啓動時這些模塊就被加載進內存中,因此核心模塊引入時省去了文件定位和編譯執行兩個步驟,而且在路徑分析中優先判斷,所以核心模塊的加載速度是最快的。文件模塊則是在運行時動態加載,速度比核心模塊慢。服務器
這裏列下node模塊的載入及緩存機制:閉包
一、載入內置模塊(A Core Module)
二、載入文件模塊(A File Module)
三、載入文件目錄模塊(A Folder Module)
四、載入node_modules裏的模塊
五、自動緩存已載入模塊
一、載入內置模塊
Node的內置模塊被編譯爲二進制形式,引用時直接使用名字而非文件路徑。當第三方的模塊和內置模塊同名時,內置模塊將覆蓋第三方同名模塊。所以命名時須要注意不要和內置模塊同名。如獲取一個http模塊
const http = require('http')
複製代碼
返回的http便是實現了HTTP功能Node的內置模塊。
二、載入文件模塊
絕對路徑的
const myMod = require('/home/base/my_mod')
複製代碼
或相對路徑的
const myMod = require('./my_mod')
複製代碼
注意,這裏忽略了擴展名.js
,如下是對等的
const myMod = require('./my_mod')
const myMod = require('./my_mod.js')
複製代碼
三、載入文件目錄模塊
能夠直接require一個目錄,假設有一個目錄名爲folder,如
const myMod = require('./folder')
複製代碼
此時,Node將搜索整個folder目錄,Node會假設folder爲一個包並試圖找到包定義文件package.json。若是folder目錄裏沒有包含package.json
文件,Node會假設默認主文件爲index.js
,即會加載index.js
。若是index.js
也不存在, 那麼加載將失敗。
四、載入node_modules裏的模塊
若是模塊名不是路徑,也不是內置模塊,Node將試圖去當前目錄的node_modules
文件夾裏搜索。若是當前目錄的node_modules
裏沒有找到,Node會從父目錄的node_modules
裏搜索,這樣遞歸下去直到根目錄。
五、自動緩存已載入模塊
對於已加載的模塊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開始加載...
模塊modA加載完畢
true
複製代碼
能夠看到雖然require了兩次,但modA.js仍然只執行了一次。mod1和mod2是相同的,即兩個引用都指向了同一個模塊對象。
優先從緩存加載
和瀏覽器會緩存靜態js文件同樣,Node也會對引入的模塊進行緩存,不一樣的是,瀏覽器僅僅緩存文件,而nodejs緩存的是編譯和執行後的對象(緩存內存) require()
對相同模塊的二次加載一概採用緩存優先的方式,這是第一優先級的,核心模塊緩存檢查先於文件模塊的緩存檢查。
基於這點:咱們能夠編寫一個模塊,用來記錄長期存在的變量。例如:我能夠編寫一個記錄接口訪問數的模塊:
let count = {}; // 因模塊是封閉的,這裏實際上借用了js閉包的概念
exports.count = function(name){
if(count[name]){
count[name]++;
}else{
count[name] = 1;
}
console.log(name + '被訪問了' + count[name] + '次。');
};
複製代碼
咱們在路由的 action
或 controller
裏這樣引用:
let count = require('count');
export.index = function(req, res){
count('index');
};
複製代碼
以上便完成了對接口調用數的統計,但這只是個demo,由於數據存儲在內存,服務器重啓後便會清空。真正的計數器必定是要結合持久化存儲器的。
在進入路徑查找以前有必要描述一下module path
這個Node.js中的概念。對於每個被加載的文件模塊,建立這個模塊對象的時候,這個模塊便會有一個paths屬性,其值根據當前文件的路徑 計算獲得。咱們建立modulepath.js
這樣一個文件,其內容爲:
// modulepath.js
console.log(module.paths);
複製代碼
咱們將其放到任意一個目錄中執行node modulepath.js命令,將獲得如下的輸出結果。
[ '/home/ikeepstudying/research/node_modules',
'/home/ikeepstudying/node_modules',
'/home/node_modules',
'/node_modules' ]
複製代碼
1.文件擴展名分析
調用require()
方法時若參數沒有文件擴展名,Node會按.js
、.json
、.node
的順尋補足擴展名,依次嘗試。
在嘗試過程當中,須要調用fs模塊阻塞式地判斷文件是否存在。由於Node的執行是單線程的,這是一個會引發性能問題的地方。若是是.node
或者·.json·文件能夠加上擴展名加快一點速度。另外一個訣竅是:同步配合緩存。
2.目錄分析和包
require()
分析文件擴展名後,可能沒有查到對應文件,而是找到了一個目錄,此時Node會將目錄看成一個包來處理。
首先, Node在擋牆目錄下查找package.json
,經過JSON.parse()
解析出包描述對象,從中取出main屬性指定的文件名進行定位。若main屬性指定文件名錯誤,或者沒有pachage.json
文件,Node會將index看成默認文件名。
簡而言之,若是require
絕對路徑的文件,查找時不會去遍歷每個node_modules
目錄,其速度最快。其他流程以下:
1.從module path
數組中取出第一個目錄做爲查找基準。
2.直接從目錄中查找該文件,若是存在,則結束查找。若是不存在,則進行下一條查找。
3.嘗試添加.js
、.json
、.node
後綴後查找,若是存在文件,則結束查找。若是不存在,則進行下一條。
4.嘗試將require
的參數做爲一個包來進行查找,讀取目錄下的package.json
文件,取得main
參數指定的文件。
5.嘗試查找該文件,若是存在,則結束查找。若是不存在,則進行第3條查找。
6.若是繼續失敗,則取出module path
數組中的下一個目錄做爲基準查找,循環第1至5個步驟。
7.若是繼續失敗,循環第1至6個步驟,直到module path中的最後一個值。
8.若是仍然失敗,則拋出異常。
整個查找過程十分相似原型鏈的查找和做用域的查找。所幸Node.js對路徑查找實現了緩存機制,不然因爲每次判斷路徑都是同步阻塞式進行,會致使嚴重的性能消耗。
一旦加載成功就以模塊的路徑進行緩存
每一個模塊文件模塊都是一個對象,它的定義以下:
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 = [];
}
複製代碼
對於不一樣擴展名,其載入方法也有所不一樣:
.js
經過fs模塊同步讀取文件後編譯執行。.node
這是C/C++編寫的擴展文件,經過dlopen()
方法加載最後編譯生成的文件.json
同過fs模塊同步讀取文件後,用JSON.pares()
解析返回結果其餘看成.js
每個編譯成功的模塊都會將其文件路徑做爲索引緩存在Module._cache
對象上。
json
文件的編譯
.json
文件調用的方法以下:其實就是調用JSON.parse
//Native extension for .json
Module._extensions['.json'] = function(module, filename) {
var content = NativeModule.require('fs').readFileSync(filename, 'utf-8');
try {
module.exports = JSON.parse(stripBOM(content));
} catch(err) {
err.message = filename + ':' + err.message;
throw err;
}
}
複製代碼
Module._extensions
會被賦值給require()
的extensions
屬性,因此能夠用:console.log(require.extensions)
;輸出系統中已有的擴展加載方式。 固然也能夠本身增長一些特殊的加載:
require.extensions['.txt'] = function(){
//code
};。
複製代碼
可是官方不鼓勵經過這種方式自定義擴展名加載,而是指望先將其餘語言或文件編譯成JavaScript文件後再加載,這樣的好處在於不講煩瑣的編譯加載等過程引入Node的執行過程。
js
模塊的編譯 在編譯的過程當中,Node對獲取的javascript文件內容進行了頭尾包裝,將文件內容包裝在一個function中:
(function (exports, require, module, __filename, __dirname) {
var math = require(‘math‘);
exports.area = function(radius) {
return Math.PI * radius * radius;
}
})
複製代碼
包裝以後的代碼會經過vm原生模塊的runInThisContext()
方法執行(具備明確上下文,不污染全局),返回一個具體的function對象,最後傳參執行,執行後返回module.exports
.
核心模塊編譯
核心模塊分爲C/C++
編寫和JavaScript編寫的兩個部分,其中C/C++
文件放在Node項目的src目錄下,JavaScript文件放在lib目錄下。
1.轉存爲C/C++代碼
Node採用了V8附帶的js2c.py工具,將全部內置的JavaScript代碼轉換成C++裏的數組,生成node_natives.h頭文件:
namespace node {
const char node_native[] = { 47, 47, ..};
const char dgram_native[] = { 47, 47, ..};
const char console_native = { 47, 47, ..};
const char buffer_native = { 47, 47, ..};
const char querystring_native = { 47, 47, ..};
const char punycode_native = { 47, 47, ..};
...
struct _native {
const char* name;
const char* source;
size_t source_len;
}
static const struct _native natives[] = {
{ "node", node_native, sizeof(node_native)-1},
{ "dgram", dgram_native, sizeof(dgram_native)-1},
...
};
}
複製代碼
在這個過程當中,JavaScript代碼以字符串形式存儲在node命名空間中,是不可直接執行的。在啓動Node進程時,js代碼直接加載到內存中。在加載的過程當中,js核心模塊經歷標識符分析後直接定位到內存中。
2.編譯js核心模塊
lib目錄下的模塊文件也在引入過程當中經歷了頭尾包裝的過程,而後才執行和導出了exports對象。與文件模塊的區別在於:獲取源代碼的方式(核心模塊從內存加載)和緩存執行結果的位置。
js核心模塊源文件經過process.binding('natives')
取出,編譯成功的模塊緩存到NativeModule._cache
上。代碼以下:
function NativeModule() {
this.filename = id + '.js';
this.id = id;
this.exports = {};
this.loaded = fales;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
複製代碼
import
和require
簡單的說一下import
和require
的本質區別
import
是ES6的模塊規範,require
是commonjs的模塊規範,詳細的用法我不介紹,我只想說一下他們最基本的區別,import是靜態加載模塊,require是動態加載,那麼靜態加載和動態加載的區別是什麼呢?
靜態加載時代碼在編譯的時候已經執行了,動態加載是編譯後在代碼運行的時候再執行,那麼具體點是什麼呢? 先說說import,以下代碼
import { name } from 'name.js'
// name.js文件
export let name = 'jinux'
export let age = 20
複製代碼
上面的代碼表示main.js
文件裏引入了name.js
文件導出的變量,在代碼編譯階段執行後的代碼以下:
let name = 'jinux'
複製代碼
這個是我本身理解的,其實就是直接把name.js
裏的代碼放到了main.js
文件裏,比如是在main.js
文件中聲明同樣。 再來看看require
var obj = require('obj.js');
// obj.js文件
var obj = {
name: 'jinux',
age: 20
}
module.export obj;
複製代碼
require是在運行階段,須要把obj對象整個加載進內存,以後用到哪一個變量就用哪一個,這裏再對比一下import
,import
是靜態加載,若是隻引入了name,age是不會引入的,因此是按需引入,性能更好一點。
開發nodejs應用時會面臨一個麻煩的事情,就是修改了配置數據以後,必須重啓服務器才能看到修改後的結果。
因而問題來了,挖掘機哪家強?噢,no! no! no!怎麼作到修改文件以後,自動重啓服務器。
server.js
中的片斷:
const port = process.env.port || 1337;
app.listen(port);
console.log("server start in " + port);
exports.app = app;
複製代碼
假定咱們如今是這樣的, app.js的片斷:
const app = require('./server.js');
複製代碼
若是咱們在server.js中啓動了服務器,咱們中止服務器能夠在app.js中調用
app.app.close()
複製代碼
可是當咱們從新引入server.js
app = require('./server.js')
複製代碼
的時候會發現並非用的最新的server.js文件,緣由是require的緩存機制,在第一次調用require('./server.js')
的時候緩存下來了。
這個時候怎麼辦?
下面的代碼解決了這個問題:
delete require.cache[require.resolve('./server.js')];
app = require('./server.js');
複製代碼