第二章 模塊機制

之前 ECMAScript 的問題:

沒有模塊系統,標準庫較少(如文件系統等缺失API),沒有標準接口,無包管理系統

CommonJS

CommonJS 規范涵蓋:模塊、二進制、Buffer、字符編碼、I/O流、進程環境、文件系統、套接字、單元測試、Web 服務器網關接口、包管理。

Node借鑒CommonJS的Modules規范實現了一套模塊系統。

Node 與瀏覽器以及W3C組織、CommonJS組織、ECMAScript之間的關系

CommonJS 的模塊規范

包括:模塊引用、模塊定義、模塊標識3部分。

  • 模塊引用

示例:

var math = require('math');
  • 模塊定義

被引用模塊的上下文提供 exports 對象用于導出當前模塊的方法與變量,這是該模塊唯一的導出出口。在模塊中還包括一個標識模塊本身的 module 對象。exports 正是 module 對象的一個屬性的引用。

示例:

// math.js
let count = 0;
exports.incr = function () {
  count += 1;
  return count;
};

// program.js
var incr = require('match');
console.log(incr()); // 1
console.log(incr()); // 2
  • 模塊標識

其實就是傳遞給 require() 方法的參數,它必須是符合小駝峰命名的字符串,或者一個相對路徑,或者一個絕對路徑。

模塊加載的具體實現

Node 引入一個模塊包括如下步驟:

  1. 路徑分析
  2. 文件定位
  3. 編譯執行

Node 中的模塊分為:核心模塊(Node本身提供的模塊)、文件模塊(用戶編寫的模塊)。

  • 核心模塊在Node源碼編譯過程中被編譯成二進制執行文件。在Node進程啟動過程中部分核心模塊會直接被加載進內存,故這部分核心模塊在引入時,不需文件定位和編譯執行,且在路徑分析中優先判斷,所以它們的加載速度是最快的。

  • 文件模塊是在運行時動態加載,需要完整經歷模塊引入的流程,加載速度較慢。

模塊加載步驟:

優先從緩存加載

Node對引入過的模塊都會進行緩存(緩存的是編譯和執行后的對象),以避免二次引入時的開銷。所以對于二次加載的模塊,Node會優先從緩存中引入。另外核心模塊的緩存檢測優先于文件模塊。

路徑分析

即對模塊標識符的分析。

  • 核心模塊:如 httpfs 等,優先加載,不可以加載與核心模塊標識符相同的自定義模塊。
  • 路徑形式的文件模塊:相對路徑或絕對路徑,分析文件模塊時,require() 方法會將該路徑轉為真實路徑,并以此為真實路徑作為該模塊的索引來緩存被編譯執行后的模塊對象。
  • 自定義模塊:不是核心模塊也不以路徑作為標識符的模塊,可以是一個包或者文件,這類模塊加載最慢。

Node 自定義模塊的查找策略(類似于JavaScript的原型鏈或者作用域的查找方式):

  • 當前文件目錄下的 node_modules 目錄
  • 父目錄下的 node_modules 目錄
  • 逐級向上遞歸查找,直至根目錄下的 node_modules 目錄

文件定位

  • 文件拓展名分析:模塊的標識符可不包含文件拓展名。Node 會按照 .js、.json、.node 的次序不足拓展名一次嘗試。這里需注意的是:在嘗試過程中,會調用 fs 模塊同步阻塞式的判斷文件是否存在,故在調用 .json .node 文件是最好帶上拓展名,已提升文件定位的速度。
  • 目錄分析:在分析模塊標識符的過程中,發現是一個目錄時會以包的形式來處理。 Node 會查找并解析 package.json 文件,從其 main 屬性來定位該模塊入口文件。如果無法通過 main 屬性獲取入口文件,則Node 默認以 index 作為入口文件名,依次查找 index.js、index.json、index.node;若未找到則進入下一個查找路徑繼續上述步驟,直至路徑遍歷完畢仍未找到拋出異常。

模塊編譯(文件模塊)

定位到模塊后便會編譯執行該模塊。每個文件模塊都是一個對象,其定義如下:

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  if (parent && parent.chidren) {
     parent.children.push(this);
  }

  this.filename = null;
  this.loaded = false;
  this.children = [];
}

Node 對于不同類型的文件模塊執行不同的加載方法。

  • .js 文件:
    fs 模塊同步讀取文件內容,wrap 模塊內容,vm 編譯模塊內容返回包含上下文的function,傳入之前 wrap 的參數,執行該函數。

wrap 內容:

(function (exports, require, module, __filename, __dirname) {
  /* 實際文件內容 */
})
  • .node 文件
    Node 調用 process.dlopen() 方法來加載和執行 .node 文件。dlopen() 方法通過 libuv 兼容層封裝了 Windows 和 *nix 平臺下的不同實現。實際上,.node模塊并不需要編譯,它已經是C/C++模塊編譯生成好的二進制文件了,執行的過程中,會將模塊的 exports 對象與.node 模塊產生聯系,返回給調用者。

  • .json 文件
    調用 fs 模塊通過讀取文件內容,調用 JSON.parse() 解析,將其賦給模塊對象的exports。

Module._extensions['.json'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(internalModule.stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};

注:文件模塊加載的具體代碼實現可以參考這里

核心模塊包括:c/c++ 編寫的模塊、js 編寫的模塊

JavaScript 核心模塊的編譯過程

  1. 通過 v8 附帶的 js2c.py 工具將 js 代碼以字符串的形式存儲到 node 的命名空間中;
  2. 通過 process.binding('natives') 取出代碼,存放到 NativeModule._cache 對象中;
  3. 當 require() 方法調用時,從 NativeModule._cache 中取出對應 id(模塊標識符) 的代碼,通過 NativeModule.compile() 方法 wrap、執行相應的代碼。

C/C++ 核心模塊的編譯過程

這里分為:純 C/C++ 編寫的模塊、核心部分由 C/C++ 編寫,對外封裝由 JS 完成的模塊。其中純 C/C++ 編寫的部分稱為內建模塊

內建模塊

Node 內建模塊的結構體定義:

struct node_module {
  int nm_version;
  unsigned int nm_flags;
  void* nm_dso_handle;
  const char* nm_filename;
  node::addon_register_func nm_register_func;
  node::addon_context_register_func nm_context_register_func;
  const char* nm_modname;
  void* nm_priv;
  struct node_module* nm_link;
};

可通過 get_builtin_module() 方法取出該模塊。內建模塊在編譯 Node 源代碼時會被編譯成二進制文件,在 Node 進程啟動時,直接加載進內存中,可直接被外部(核心模塊、C/C++拓展模塊-但不建議直接調用)調用。這里同 JS 核心文件加載一樣通過 process.binding() 方法加載,但它將 exports 對象緩存到 binding_cache_object 中。

os 原生模塊引入流程

C/C++ 拓展模塊

C/C++ 拓展模塊的編寫基本同內建模塊一致,可借助 node-gyp 進行編譯,只是不需要注冊到 node builtin 模塊中,而是通過 process.dlopen() 動態加載進來。由于 .node 文件已是編譯后的二進制文件,所以被加載進來后不需編譯直接執行,相較于 JavaScript 模塊會略快一點。

.node 文件引入流程

包與 NPM

包實際被打包成一個存檔文件(zip 或 tar.gz 格式)。CommonJS 規范的包結構:

  • package.json: 包描述文件
  • bin: 可執行文件
  • lib: JavaScript 文件
  • doc: 項目文檔
  • test: 單元測試

NPM

依賴安裝:

  • 全局安裝:只是將包描述文件中 bin 字段下的可執行腳本以軟連接的方式鏈接到 node 執行目錄下的 ../../lib/node_module 中。path.resolve(process.execPath, '..', '..', 'lib', 'node_modules')
  • 本地安裝:npm install <package.json 文件所在目錄 or url>

一些鉤子: package.json 文件的scripts 中定義。

scripts: {
  "preinstall":  "install 該包之前執行的腳本",
  install: "install 該包時執行的腳本",
  uninstall: "卸載該包時執行的腳本",
  test: "單元測試腳本",
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容