NodeJs模塊機制

一、CommonJs規範
講到nodejs的模塊化就不得不講CommonJs規範了,在以前的文章裏也有講過CommonJs相關使用,具體使用可以到JavaScript類別下查看,這裏就不放傳送門了。在這裏就不多做贅述了,下面就說一下基本的用法。

導出模塊 module.exports:

// DateUtil.js

class DateUtil {

    static getDate() {
        return new Date();
    }

}
module.exports = DateUtil;
1
2
3
4
5
6
7
8
9
10
引入模塊 require:

// main.js

const DateUtil = require('./DateUtil');

console.log('當前時間', DateUtil.getDate());
1
2
3
4
5
二、Node的模塊實現
在Node中引入模塊,需要經歷如下三個步驟:

路徑分析
文件定位
編譯執行
在Node中,模塊分爲兩類:一類是Node自身提供的模塊,稱爲核心模塊:fs、http等,就像java中的jdk提供的核心類一樣。第二類是用戶編寫的模塊,稱爲文件模塊。

核心模塊部分在Node源代碼的編譯過程中,編譯進了二進制執行文件。在Node進程啓動時,核心模塊就被直接加載進內存,所以這部分的模塊引入時,文件定位和編譯執行這兩個步驟可以省略掉,並且在路徑分析中優先判斷,所以它的加載速度是最快的。
文件模塊在運行時動態加載,需要完整的路徑分析,文件定位,編譯執行過程,加載速度比核心模塊慢。
以上介紹了模塊加載過程和模塊的類別劃分,下面我們來看看Node加載模塊的具體過程,如下圖:

Node爲了優化加載模塊的速度,也像瀏覽器一樣引入了緩存,對加載過的模塊會保存到緩存內,下次再次加載時就會命中緩存,節省了對相同模塊的多次重複加載。模塊加載前會將需要加載的模塊名轉爲完整路徑名,查找到模塊後將完整路徑名保存到緩存,下次再次加載該路徑模塊時就可以直接從緩存中取得。

上圖還說明了緩存模塊的加載是在覈心模塊之前,也就是先查詢緩存,緩存沒找到後再查Node自帶的核心模塊,如果核心模塊也沒有查詢到,最後再去用戶自定義模塊內查找。

模塊加載的優先級是:緩存模塊 > 核心模塊 > 用戶自定義模塊。

在require加載模塊時,require參數的標識符可以以文件類型結尾require("./test.js"),也可以省略文件類型require("./test")。對於省略類型的第二種寫法,Node首先會認爲它是一個.js文件,如果沒有查找到該js文件,然後會去查找.json文件,如果還沒有查找到該json文件,最後會去查找.node文件,如果連.node文件都沒有查找到,就該拋異常了。Node在執行require加載模塊時是線程阻塞的,大家都知道Node是單線程執行的,如果長期阻塞的話系統其它任務就得不到執行了,所以爲了加快require模塊的加載,如果不是.js文件的話,在require的時候就把文件類型加上,這樣Node就不會再去一一嘗試了。

require加載無文件類型的優先級:.js > .json > .node

三、模塊編譯
在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 = [];
}
1
2
3
4
5
6
7
8
9
10
11
12
編譯和執行是引入文件模塊的最後一個階段。定位到具體的文件後,Node會新建一個模塊對象,然後根據路徑載入並編譯。對於不同的文件擴展名,其載入方法也有所不同,具體如下:

.js文件:通過fs模塊同步讀取文件後編譯執行。
.json文件:通過fs模塊同步讀取文件後,用JSON.parse()解析返回結果。
.node文件:這是用C/C++編寫的擴展文件,通過dlopen()方法加載最後編譯生成的文件。
其餘擴展名的文件:它們都被當作.js文件載入。
每一個編譯成功的模塊都會將其文件路徑做爲索引緩存在Module._cache對象上,以提高二次引入的性能。

我們都知道,在瀏覽器中編寫的js文件如果變量定義不是在函數或對象內就會存在污染全局變量的情況,例如下面這種方式定義的變量:

<script>
    var a = 'test';
</script>
1
2
3
等同於window.a = 'test';。但是我們在Node中的每個.js模塊內並沒有做任何其它處理,定義的變量怎麼就不會污染全局環境了呢?還有在Node的模塊內怎麼就可以直接使用module、require、exports、__filename、__ dirname等對象呢?事實上,在編譯的過程中,Node對獲取的JavaScript文件內容進行了頭尾包裝。例如我們一開始寫的DateUtil.js

// DateUtil.js

class DateUtil {

    static getDate() {
        return new Date();
    }

}
module.exports = DateUtil;
1
2
3
4
5
6
7
8
9
10
編譯包裝後的文件是下面這樣的:

// Node編譯時包裝後的DateUtil.js
(function(exports, require, module, __filename, __dirname) {
    class DateUtil {

        static getDate() {
            return new Date();
        }

    }
    module.exports = DateUtil;
});
1
2
3
4
5
6
7
8
9
10
11
這樣每個模塊之間就進行了作用域隔離。

四、模塊引入
我們自己編寫的.js,.node文件被稱爲文件模塊,在文件模塊內可能會用到Node提供的核心模塊javascript,如buffer,crypto,evals,fs,os,而這些核心模塊又可能會調用底層C/C++ 編寫的內建模塊。它們的依賴關係如下圖所示:

文件模塊也可以直接調用內建模塊,但是不推薦這種直接調用,因爲核心模塊中基本都封裝了內建模塊,內建模塊的內部變量和方法已經導出到核心模塊了。

我們在編寫文件模塊時如果需要依賴核心模塊,可以通過require("os")這種方式,但是在require的背後都執行了哪些邏輯?接下來我們通過下圖瞭解一下:

有時候核心模塊並不能夠滿足我們的需求,這時我們就需要根據自身的業務開發自己的sdk,在這裏就需要用到擴張模塊了。

擴張模塊由C/C++ 編寫,屬於文件模塊的一種。(windows系統)C/C++ 模塊通過預先編譯爲.dll文件,通過.dll文件生成.node文件,然後調用process.dlopen()方法導出JavaScript文件。如下圖所示:

五、模塊調用棧
上面我們說了文件模塊、核心模塊、內建模塊、C/C++ 擴張模塊,到這裏該說一下以上幾個模塊的調用關係了。如下圖:

C/C++ 內建模塊屬於最底層的模塊,它屬於核心模塊,主要提供API給JavaScript核心模塊和第三方JavaScript文件模塊調用。不過這裏不推薦文件模塊直接調用C/C++ 內建模塊,除非你對它非常的瞭解。

JavaScript核心模塊主要扮演的角色有兩種:一類是做爲 C/C++ 內建模塊的封裝層和橋接層,供文件模塊調用;一類是純碎的功能模塊,它不需要根底層打交道,但是又十分重要。

文件模塊通常由第三方編寫,包括普通JavaScript模塊和C/C++ 擴張模塊,主要調用方向爲普通JavaScript模塊調用擴張模塊。
--------------------- 
原文:https://blog.csdn.net/qbian/article/details/79367500 
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章