深刻了解Nodejs模塊機制

咱們都知道Nodejs遵循的是CommonJS規範,當咱們require('moduleA')時,模塊是怎麼經過名字或者路徑獲取到模塊的呢?首先要聊一下模塊引用、模塊定義、模塊標識三個概念。javascript

1 CommonJS規範

1.1 模塊引用

模塊上下文提供require()方法來引入外部模塊,看似簡單的require函數, 其實內部作了大量工做。示例代碼以下:java

//test.js
//引入一個模塊到當前上下文中
const math = require('math');
math.add(1, 2);
複製代碼

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

1.3 模塊標識

模塊標識就是傳遞給require()方法的參數,它必須是符合小駝峯命名的字符串,或者以...開頭的相對路徑或者絕對路徑,能夠沒有文件後綴名.js.數組

2. Node的模塊實現

在Node中引入模塊,須要經歷以下四個步驟:瀏覽器

  • 路徑分析
  • 文件定位
  • 編譯執行
  • 加入內存

2.1 路徑分析

Node.js中模塊能夠經過文件路徑或名字獲取模塊的引用。模塊的引用會映射到一個js文件路徑。 在Node中模塊分爲兩類:緩存

  • 一是Node提供的模塊,稱爲核心模塊(內置模塊),內置模塊公開了一些經常使用的API給開發者,而且它們在Node進程開始的時候就預加載了。
  • 另外一類是用戶編寫的模塊,稱爲文件模塊。如經過NPM安裝的第三方模塊(third-party modules)或本地模塊(local modules),每一個模塊都會暴露一個公開的API。以便開發者能夠導入。如
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] + '次。');
};
複製代碼

咱們在路由的 actioncontroller裏這樣引用:

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' ]
複製代碼

2.2 文件定位

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對路徑查找實現了緩存機制,不然因爲每次判斷路徑都是同步阻塞式進行,會致使嚴重的性能消耗。

一旦加載成功就以模塊的路徑進行緩存

2.3 模塊編譯

每一個模塊文件模塊都是一個對象,它的定義以下:

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 = {};
複製代碼

3 importrequire

簡單的說一下importrequire的本質區別

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對象整個加載進內存,以後用到哪一個變量就用哪一個,這裏再對比一下importimport是靜態加載,若是隻引入了name,age是不會引入的,因此是按需引入,性能更好一點。

4 nodejs清除require緩存

開發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');
複製代碼
相關文章
相關標籤/搜索