前言
模組對於node來說是不可或缺的一部分,是服務端程式設計的基礎。趁著整理模組之際,先將node部分的模組的封裝等做一個總結。希望能夠切實的幫助到你。本篇將對CommenJS規範,node的檔案模組和核心模組等做一個綜合的整理。如果你喜歡我的文章,歡迎評論,歡迎Star~。歡迎關注我的github部落格
正文
我覺得模組的出現是js進步最大的地方。因為有了模組,才使得很多優秀的東西可以真正被共享出來,而不用去擔心變數汙染、名稱空間的問題。
node作為一門服務端的javascript,它借鑑了CommonJS的規範,形成了一套易用的模組規範。
首先,看看下面這個最常見的例子:
//circle.js
const { PI } = Math;
exports.area = x => PI*x**2;
exports.circle = x => 2*PI*x;複製程式碼
//main.js
const circle = require('./circle');
console.log(circle.area(4)); //50.26548245743669複製程式碼
其實,我們可以清晰地看到兩個檔案中,模組規範部分可以分成三部分:
- require(模組引用) => 這是整個模組系統最核心的部分,能夠引入其他模組,充分的運用。
- exports(模組定義) => 另一個出彩的地方,可以將自身模組中的內容匯出,供其他模組使用。
- 標識(模組標識) => 供別人認清楚模組的東西。
這三塊內容可以使用一張圖片概括:
如圖:
從這幅圖中,我們可以看到,模組之間可以通過exports將介面暴露出來,然後通過require來對另一個模組內的內容進行引入。
這樣我們就大概懂得了模組的定義。它主要分為三部分:模組的引用、模組定義和模組標識。
然而,整個模組部分我們最需要去了解的是require機制。node對於require實現,有很多的東西可以去欣賞。
首先,需要明白的是整個模組引入的步驟。從上面的例子中,可以看出這三部分:
- 路徑分析 => 拿例子來進行分析的話,'./circle'就是路徑。(./此類的是相對路徑,當然還有絕對路徑)
- 檔案定位 => 通過分析出來的位置,去進行檔案的獲取。
- 編譯執行 => 只有通過編譯過的檔案,才能夠放入其他模組中進行使用 (之後也會分析如何進行編譯的)。
有的時候,情況是特殊的。模組本身就分成核心模組和檔案模組。而核心模組在node原始碼編譯的時候。就被編譯成二進位制檔案。並且部分核心模組會在node程式啟動時,直接載入到記憶體中,因此這一部分的核心模組引入是不需要經過檔案定位和編譯執行的步驟的。
還有特殊就是在快取部分。每個模組首次載入之後,node會快取其編譯執行後的物件,方便二次載入。所以,二次載入時,是以快取優先的,從快取中載入的模組也是不需要檔案定位和編譯的。
單從路徑分析說起,可以分成三種不同的方式:
- 核心模組,如http、fs、path等,載入優先順序僅次於快取載入,且會直接編譯成二進位制檔案
- 路徑形式的模組,如上面例子中的‘./circle’。路徑明確,查詢速度相對較快,載入速度慢於核心模組
- 自定義模組,大多如npm包形式的檔案,儲存在node_modules,並沒有相應的路徑。這種查詢比較繁瑣。
查詢方式: 1. 從當前目錄下面的node_modules中查詢是否具備相應的模組 2. 若具備,則直接載入使用,否則,會去查詢父目錄下的node_modules目錄,直至查詢到根目錄下的node_modules中。這種方式是最慢的。
再來分析檔案定位:
第一個例子中的識別符號是'./circle'。可以發現,這個檔案識別符號是沒有字尾名的。那麼,node是如何來進行定位的呢?其實,node有一個預設的定位順序:js、node、json。這裡會最先識別js,之後一次對json和node的檔案進行識別。因此,這裡有個小技巧:在識別.node和.json的檔案的時候,帶上檔案字尾名會快一點, 為什麼呢?是因為,node是使用fs同步阻塞的方式,逐一去嘗試,該檔案是否存在,存在著直接載入;不存在的話,嘗試下一個字尾名。
還有對於那些自定義的模組,如npm包。node的定位方式也是不同的。通常來說,npm包中都會具備package.json檔案,這個檔案中有個main屬性,這指向的就是整個包的入口檔案;如果沒有這些條件,node會去預設載入index.js、index.json和index.node
最後就是模組編譯部分的分析了:
首先,編譯執行也可以通過上面的三類字尾檔名來進行分析:
- js檔案 => 通過fs模組同步讀取之後,編譯執行
- node檔案 => .node檔案是c/c++檔案模組的編譯檔案,使用dlopen的方法對檔案進行載入引入。
- json檔案 => json檔案首先是通過fs讀取檔案,然後通過JSON.parse方法進行編譯執行。
其他字尾名的檔案,都會被當成是js檔案進行處理。同時,我們需要的細緻地分析一下javascript檔案編譯的一些具體過程。
javascript檔案編譯
在使用fs讀取檔案之後,讀取出來的內容node會如何去處理呢?會造成變數汙染嗎?很顯然是不會的。node是根據CommonJS的規範,對讀取進入的內容,在頭部和尾部進行包裝,包裝成function(exports, require, module, dirname,filename){ ...讀取內容 }。這樣子,就起到了一個作用域隔絕的作用,不會對現有模組中的內容汙染。而dirname、filename是node中存在的。
然後將這個函式程式碼使用vm原生的模組runInThisContext()方法執行(類似eval => 將字串轉化成可執行的js的程式碼)。然後返回一個具體的物件,供現有模組中的內容進行使用。
C/C++模組的編譯
這個編譯主要是依靠node的process.dlopen()方法進行執行,同時node使用libuv對windows和*nix平臺做了相容性的處理。這種模組的效能相對於普通檔案模組來說較高,但是編寫成本也會相應地提高。
json檔案編譯
json檔案編譯會比較簡單,就是通過fs讀取檔案,然後通過JSON.parse方法進行編譯,最終將內容給予現有模組中命名的那個變數。
總結
至此我們對node的模組整體的機制,大致已經整理清楚了,從模組的匯出,到引入,以及識別符號的分析。均可從CommonJS中找到影子,但是node對其進行的加工又相對比較完美。
如果你對我寫的有疑問,可以評論,如我寫的有錯誤,歡迎指正。你喜歡我的部落格,請給我關注Star~呦。大家一起總結一起進步。歡迎關注我的github部落格