[NodeJs系列]NodeJs模塊機制

注: 1. 本文涉及的nodejs源碼如無特別說明則全部基於v10.14.1

歡迎關注公衆號:前端情報局

Nodejs 中對模塊的實現

本節主要基於NodeJs源碼,對其模塊的實現做一個簡要的概述,如有錯漏,望諸君不吝指正。

當我們使用require引入一個模塊的時候,概況起來經歷了兩個步驟:路徑分析和模塊載入

路徑分析

路徑分析其實就是模塊查找的過程,由_resolveFilename函數實現。

我們通過一個例子,展開說明:

const http = require('http');
const moduleA = requie('./parent/moduleA');

這個例子中,我們引入兩種不同類型的模塊:核心模塊-http和自定義模塊moduleA

對於核心模塊而言,_resolveFilename會跳過查找步驟,直接返回,交給下一步處理

if (NativeModule.nonInternalExists(request)) {
    // 這裏的request 就是模塊名稱 'http'
    return request;
}

而對於自定義模塊而言,存在以下幾種情況(_findPath

  1. 文件模塊
  2. 目錄模塊
  3. 從node_modules目錄加載
  4. 全局目錄加載

這些在官方文檔中已經闡述的很清楚了,這裏就不再贅述。

如果模塊存在,那麼_resolveFilename會返回該模塊的絕對路徑,比如/Users/xxx/Desktop/practice/node/module/parent/moduleA.js

載入模塊

獲取到模塊地址後,Node就開始着手載入模塊。

首先,Node會查看模塊是否存在緩存中:

// filename 即模塊絕對路徑
var cachedModule = Module._cache[filename];
if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
}

存在則返回對應緩存內容,不存在則進一步判斷該模塊是否是核心模塊:

if (NativeModule.nonInternalExists(filename)) {
    return NativeModule.require(filename);
}

如果模塊既不存在於緩存中也非核心模塊,那麼Node會實例化一個全新的模塊對象


function Module(id, parent){
  // 通常是模塊絕對路徑
  this.id = id;
  // 要導出的內容
  this.exports = {};
  // 父級模塊
  this.parent = parent;
  this.filename = null;
  // 是否已經加載成功
  this.loaded = false;
  // 子模塊
  this.children = [];
}

var module = new Module(filename, parent);

而後Node會根據路徑嘗試載入。

function tryModuleLoad(module, filename) {
  var threw = true;
  try {
    module.load(filename);
    threw = false;
  } finally {
    if (threw) {
      delete Module._cache[filename];
    }
  }
}

對於不同的文件擴展名,其載入方法也有所不同。

通過fs同步讀取文件內容後將其包裹在指定函數中:

Module.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

調用執行此函數:

compiledWrapper.call(this.exports, this.exports, require, this,
                                  filename, dirname);
  • .json文件

通過fs同步讀取文件內容後,用JSON.parse解析並返回內容

var content = fs.readFileSync(filename, 'utf8');
try {
    module.exports = JSON.parse(stripBOM(content));
} catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
}
  • .node

這是用C/C++編寫的擴展文件,通過dlopen()方法加載最後編譯生成的文件。

return process.dlopen(module, path.toNamespacedPath(filename));
  • .mjs

這是用於處理ES6模塊的擴展文件,是NodeJs在v8.5.0後新增的特性。對於這類擴展名的文件,只能使用ES6模塊語法import引入,否則將會報錯(啓用 --experimental-modules的情況下)

throw new ERR_REQUIRE_ESM(filename);

如果一切順利,就會返回附加在exports對象上的內容

return module.exports;

模塊循環依賴

接下來我們來探究一下模塊循環依賴的問題:模塊1依賴模塊2,模塊2依賴模塊1,會發生什麼?

這裏只探究commonjs的情況

爲此,我們創建了兩個文件,module-a.js和module-b.js,並讓他們相互引用:

module-a.js

console.log(' 開始加載 A 模塊');
exports.a = 2;
require('./module-b.js');
exports.b = 3;
console.log('A 模塊加載完畢');

module-b.js

console.log(' 開始加載 B 模塊');
let moduleA = require('./module-a.js');
console.log(moduleA.a,moduleA.b)
console.log('B 模塊加載完畢');

運行module-a.js,可以看到控制檯輸出:

開始加載 A 模塊
開始加載 B 模塊
2 undefined
B 模塊加載完畢
A 模塊加載完畢

這時因爲每個require都是同步執行的,在module-a完全加載前需要先加載./module-b,此時對於module-a而言,其exports對象上只附加了屬性a,屬性b是在./module-b加載完成後才賦值的。

QA

  1. 如何刪除模塊緩存?

可以通過delete require.cache(moduleId)來刪除對應模塊的緩存,其中moduleId表示的是模塊的絕對路徑,一般的,如果我們需要對某些模塊進行熱更新,可以使用此特性,舉個例子:

// hot-reload.js
console.log('this is hot reload module');

// index.js
const path = require('path');
const fs = require('fs');
const hotReloadId = path.join(__dirname,'./hot-reload.js');
const watcher = fs.watch(hotReloadId);
watcher.on('change',(eventType,filename)=>{
    if(eventType === 'change'){
        delete require.cache[hotReloadId];
        require(hotReloadId);
    }
});
  1. Node中可以使用ES6 模塊嗎?

從8.5.0版本開始,NodeJs開始支持原生ES6模塊,啓用該功能需要兩個條件:

  1. 所有使用ES6模塊的文件擴展名都必須是.mjs
  2. 命令行選項--experimental-modules

node --experimental-modules index.mjs

node --experimental-modules index.mjs

但是截止到NodeJs v10.15.0,ES6模塊的支持依舊是實驗性的,筆者並不推薦在公司項目中使用

參考

  1. nodejs-loader.js
  2. 樸靈. 深入淺出Node.js

image

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