【Node】詳解模塊的實現過程

CommonJS 定義了 module、exports 和 require 模塊規範,Node.js 爲了實現這個簡單的標準,從底層 C/C++ 內建模塊到 JavaScript 核心模塊,從路徑分析、文件定位到編譯執行,經歷了一系列複雜的過程。簡單的瞭解 Node 模塊的原理,有利於咱們從新認識基於 Node 搭建的框架。前端

1、CommonJS 模塊規範

CommonJS 規範或標準簡單來講是一種理論,它指望 JavaScript 能夠具有跨宿主環境執行的能力,不只能夠開發客戶端應用,還能夠開發服務端應用、命令行工具、桌面圖形界面應用等。node

CommonJS 規範對模塊的定義分爲三個部分:json

  • 模塊定義後端

    在模塊中存在module對象表明模塊自己,模塊上下文提供exports屬性 ,將方法掛載在exports對象上便可以定義導出方式,例如:數組

    // math.js
      exports.add = function(){ //...}
    複製代碼
  • 模塊引用瀏覽器

    module提供require()方法引入外部模塊的 API 到當前的上下文中:緩存

    var math = require('math')
    複製代碼
  • 模塊標識bash

    模塊標識實際就是傳遞給require()方法中的參數,能夠是按小駝峯(camelCase)命名的字符串,也能夠是文件路徑。服務器

Node.js 借鑑了 CommonJS 規範的設計,特別是 CommonJS 的 Modules 規範,實現了一套模塊系統,同時 NPM 實現了 CommonJS 的 Packages 規範,模塊和包組成了 Node 應用開發的基礎。網絡

2、Node 模塊加載原理

上述模塊規範看起來十分簡單,只有moduleexportsrequire,但 Node 是如何實現的呢?

須要經歷路徑分析(模塊的完整路徑)、文件定位(文件擴展名或目錄)、編譯執行三個步驟。

2.1 路徑分析

回顧require()接收 模塊標識 做爲參數來引入模塊,Node 就是基於這個標識符進行路徑分析。不一樣的標識符采用的分析方式是不一樣的,主要分爲一下幾類:

  • Node 提供的核心模塊,如 http、fs、path

    核心模塊在 Node 源碼編譯時存爲二進制執行文件,在 Node 啓動時直接加載到內存中,路徑分析中優先判斷,因此加載速度很快,並且也不用後續的文件定位和編譯執行。

    若是想加載與核心模塊同名的自定義模塊,如自定義 http 模塊,那必須選用不一樣標誌符或改用路徑方式。

  • 路徑形式的文件模塊.、..相對路徑模塊和/絕對路徑模塊

    .、..或/開始的標識符都會當成文件模塊處理,Node 會將require()中的路徑轉爲真實路徑做爲索引,而後編譯執行。

    因爲文件模塊明確了文件位置,因此縮短了路徑分析時間,加載速度僅慢與核心模塊。

  • 自定義模塊,即非路徑形式的文件模塊

    即不是核心模塊,也不是路徑形式的文件模塊,自定義文件是特殊的文件模塊,在路徑查找時 Node 會逐級查找該模塊路徑中的路徑。
    模塊路徑查找策略示例以下:

    // paths.js
    console.log(module.paths)
    
    // Terminal
    $ node paths.js
    [ '/Users/tong/WebstormProjects/testNode/node_modules',
    '/Users/tong/WebstormProjects/node_modules',
    '/Users/tong/node_modules',
    '/Users/node_modules',
    '/node_modules' ]
    
    複製代碼

從上述示例輸出的模塊路徑數組能夠看出,模塊的查找時沿當前路徑向上逐級查找node_modules目錄,直到目標路徑爲止,相似 JS 原型鏈或做用域鏈。路徑越深速度越慢,因此自定義模塊加載速度最慢。

緩存優先機制:Node 會對引入過的模塊進行緩存以提升性能,不一樣於瀏覽器緩存的是文件,Node 緩存的是編譯和執行後的對象,因此require()對相同模塊的二次加載採用緩存優先的方式。這個緩存優先是第一優先級的,比核心模塊的優先級要高!

2.2 文件定位

模塊路徑分析完成後是文件定位,主要包括文件擴展名的分析、目錄和包的處理。爲了表達的更清晰,將文件定位分爲四個步驟:

  • step1: 補充擴展名

    一般require()中的標識符是不包含文件擴展名的,這種狀況下,Node會按照 .js、.json、.node 的順序嘗試補充擴展名

    在嘗試補充擴展名時,須要調用 fs 模塊同步阻塞式判斷文件是否存在,因此這裏提高性能的小技巧,就是 .json 和 .node 文件傳遞給require()時帶上擴展名會加快一些速度。

  • step2: 目錄處理查找 pakage.json

    若是補充擴展名後沒有找到對應文件,可是獲得了一個目錄,此時 Node會將目錄當作一個包處理。依據 CommonJS 包規範的實現,Node 會在目錄下查找pakage.json(包描述文件),經過JSON.parse()解析成包描述對象,從中main屬性指定的文件名定位

  • step3: 繼續默認查找 index 文件

    若是沒有pakage.json或者main屬性指定的文件名錯誤,那 Node 會將 index 當作默認文件名,依次查找 index.js、index.json、index.node

  • step4: 進入下一個模塊路徑

    在上述目錄分析過程當中沒有成功定位時,自定義模塊按路徑查找策略進入上一層node_modules目錄,當整個模塊路徑數組遍歷完畢後沒有定位到文件,則會拋出查找失敗異常。

緩存加載的優化策略使得二次引入不須要路徑分析、文件定位、編譯執行這些過程,並且核心模塊也不須要文件定位的過程,這大大提升了再次加載模塊時的效率

2.3 編譯執行

Node 中每一個模塊都是一個對象,在具體定位到文件後,Node 會新建該模塊對象,而後根據路徑載入並編譯。不一樣的文件擴展名載入方法爲:

  • .js 文件: 經過 fs 模塊同步讀取後編譯執行
  • .json 文件: 經過 fs 模塊同步讀取後,用JSON.parse()解析並返回結果
  • .node 文件: 這是用 C/C++ 寫的擴展文件,經過process.dlopen()方法加載最後編譯生成的
  • 其餘擴展名: 都被當作 js 文件載入

載入成功後 Node 會調用具體的編譯方式將文件執行後返回給調用者。對於 .json 文件的編譯最簡單,JSON.parse()解析獲得對象後直接賦值給模塊對象的exports,而 .node 文件是C/C++編譯生成的,Node 直接調用process.dlopen()載入執行就能夠,下面重點介紹 .js 文件的編譯


在 CommonJS 模塊規範中有moduleexportsrequire 這3個變量,在 Node API 文檔中每一個模塊還有 __filename__dirname這兩個變量,可是在模塊中沒有定義這些變量,那它們是怎麼產生的呢?

事實上在編譯過程當中,Node 對每一個 JS 文件都被進行了封裝,例如一個 JS 文件會被封裝成以下:

(function (exports, require, module, __filename, __dirname) {
	var math = require('math')
	export.add = function(){ //... }
})
複製代碼

首先每一個模塊文件之間都進行了做用域隔離,經過vm原生模塊的runInThisContext()方法(相似 eval)返回一個具體的 function 對象,最後將當前模塊對象的exports屬性、require()方法、模塊對象自己module、文件定位時獲得的完整路徑__filename文件目錄__dirname做爲參數傳遞給這個 function 執行。模塊的exports屬性上的任何方法和屬性均可以被外部調用,其他的則不可被調用。

至此,moduleexportsrequire的流程就介紹完了。


曾經困惑過,每一個模塊均可以使用exports的狀況下,爲何還必須用module.exports

由於exports只是module.exports的一個地址引用,如module.exports 已經具有一些屬性和方法,Node 會忽略exports只導出 module.exports。因此直接賦值給module.exports會更準確。

編譯成功的模塊會將文件路徑做爲索引緩存在 Module._cache 對象上,路徑分析時優先查找緩存,提升二次引入的性能。

3、Node 核心模塊

總結來講 Node 模塊分爲Node提供的核心模塊和用戶編寫的文件模塊。文件模塊是在運行時動態加載,包括了上述完整的路徑分析、文件定位、編譯執行這些過程,核心模塊在Node源碼編譯成可執行文件時存爲二進制文件,直接加載在內存中,因此不用文件定位和編譯執行。

核心模塊分爲 C/C++ 編寫的和 JavaScript 編寫的兩部分,在編譯全部 C/C++ 文件以前,編譯程序須要將全部的 JavaScript 核心模塊編譯爲 C/C++ 可執行代碼,編譯成功的則放在 NativeModule._cache對象上,顯然和文件模塊 Module._cache的緩存位置不一樣。

在覈心模塊中,有些模塊由純 C/C++ 編寫的內建模塊,主要提供 API 給 JavaScript 核心模塊,一般不能被用戶直接調用,而有些模塊由 C/C++ 完成核心部分,而 JavaScript 實現封裝和向外導出,如 buffer、fs、os 等。

因此在Node的模塊類型中存在依賴層級關係:內建模塊(C/C++)—> 核心模塊(JavaScript)—> 文件模塊。

使用require()十分的方便,但從 JavaScript 到 C/C++ 的過程十分複雜,總結來講須要經歷 C/C++ 層面內建模塊的定義、(JavaScript)核心模塊的定義和引入以及(JavaScript)文件模塊的引入。

4、前端模塊規範

對比先後端的 JavaScript,瀏覽器端的 JavaScript 須要經歷從同一個服務器端分發到多個客戶端執行,經過網絡加載代碼,瓶頸在於寬帶;而服務器端 JavaScript 相同代碼須要屢次執行,經過磁盤加載,瓶頸在於 CPU 和內存,因此先後端的 JavaScript 在 Http 兩端的職責徹底不用。

Node 模塊的引入幾乎是同步的,而前端模塊若是同步引入,那腳本加載須要太長的時間,因此 CommonJS 爲後端 JavaScript 制定的規範不適合前端。然後出現 AMD 和 CMD 用於前端應用場景。

4.1 AMD 規範

AMD 即異步模塊定義(Asynchronous Module Definition),模塊定義爲:

define(id?, dependencies?, factory);
複製代碼

AMD 模塊須要用define明肯定義一個模塊,其中模塊id與依賴dependencies是可選的,factory的內容就是實際代碼的內容。例如指定一些依賴到模塊中:

define(['dep1', 'dep2'], function(){
	// module code
});
複製代碼

require.js 實現 AMD 規範的模塊化,感興趣的能夠查看 require.js 的文檔。

4.2 CMD 規範

CMD 模塊的定義更加簡單:

define(factory);
複製代碼

定義的模塊同 Node 模塊同樣是隱式包裝,在依賴部分支持動態引入,例如:

define(function(require, exports, module){
 	// module code
 });
複製代碼

requireexportsmodule經過形參傳遞給模塊,須要依賴模塊時直接使用require()引入。

sea.js 實現 AMD 規範的模塊化,感興趣的能夠查看 sea.js 的文檔。


推薦兩本 Node 的書籍:《Node.js 實戰》主要是使用示例,《深刻淺出 Node.js》偏實現原理。

固然個人博客也會繼續總結更新,下一篇內容會是關於 CommonJS 包規範和 NPM 包管理的內容。
相關文章
相關標籤/搜索