淺析node.js的模塊加載

module

在node.js中,模塊使用CommonJS規範,一個文件是一個模塊javascript

node.js中的模塊可分爲三類java

  • 內部模塊 - node.js提供的模塊如 fs,http,path等
  • 自定模塊 - 咱們本身寫的模塊
  • 第三方模塊 - 經過npm安裝的模塊

node.js提供了大量的模塊供咱們使用,好比 想解析一個文件的路徑,可使用path模塊下的相應方法實現:node

const path = require('path');
//返回目標文件的絕對路徑
console.log(path.resolve('./1.txt'));
複製代碼

運行結果:npm

/Users/cuiyue/workspace/test/1.txt
複製代碼

使用require引入相應的模塊,便可使用。json

__dirname和__filename

node.js的每一個模塊都有這兩個參數,它們都是一個絕對路徑的地址,區別是__filename存放了從根目錄到當前文件名的路徑,__dirname只存放從根目錄到模塊的所在目錄:緩存

console.log(__dirname);
console.log(__filename);
複製代碼

運行結果:安全

/Users/cuiyue/workspace/test
/Users/cuiyue/workspace/test/module.js
複製代碼

vm模塊

vm模塊是node.js提供在V8虛擬機中編譯和運行的工具,node.js中的模塊內部實現就是經過此模塊完成。閉包

說說vm的基本用法。app

在js環境中有一個eval函數,它能夠運行js的代碼字符串,好比:函數

eval('console.log("Hello javascript.")'); //輸出Hello javascript.
複製代碼

能夠看到,eval函數的參數是一段字符串,它能夠運行字符串形式的js代碼,但它可使用上下文環境中的變量:

var num=100;
eval('console.log(num)'); //輸出100
複製代碼

以上是能夠正確訪問num的值。

vm模塊提供了方法建立一個安全的沙箱,在指定的上下文環境中運行代碼,不受外界干擾。

const vm = require('vm');
var num = 100;
vm.runInThisContext('console.log(num)');
複製代碼

運行結果:

console.log(num)
            ^
ReferenceError: num is not defined
複製代碼

能夠看到代碼報錯了,說明在vm建立了指定的上下文環境中,拿不到外界的參量。

CommonJS規範

在之前,因爲javascript的歷史緣由致使它的模塊機制不好,因爲這些缺點使得javascript不太善於開發大型應用,因而提出了CommonJS規範以彌補javascript的不足。

CommonJS規範主要分爲三塊內容:模塊導入導出、模塊定義、模塊標識。

模塊導入導出

CommonJS中使用require()函數進行模塊的引入。

const mymodule = require('mymodule');
複製代碼

使用exports導出模塊

module.exports = {
    name: 'Tom'
};
複製代碼

引用的名稱能夠不帶路徑,若不帶路徑表示引入的是node提供的模塊或是npm安裝的第三方模塊(node_modules)

模塊定義

module對象:在每個模塊中,module對象表明該模塊自身。

export屬性:module對象的一個屬性,它向外提供接口。

模塊標識

模塊標識指的是傳遞給require方法的參數,必須是符合小駝峯命名的字符串,或者以 .、..、開頭的相對路徑,或者絕對路徑。

node中模塊解析流程

  1. 首先接收參數,把傳入的模塊名稱解析成絕對路徑
  2. 若沒有後綴名稱,依次拼接.js .json .node嘗試加載,仍到不到模塊則報錯
  3. 取得正確的路徑後判斷緩存中是否存在此模塊,如有則取出
  4. 若緩存中不存在則加載此文件,在外包裹一層閉包並執行它

以上爲大體流程,下面嘗試着寫一下模塊。

代碼的基本結構:

/** * Module類,用於處理模塊加載 */
function Module() {}

//模塊的緩存
Module._cacheModule = {};

//不一樣擴展名的加載策略
Module._extensions = {};

//根據moduleId解析絕對路徑,
Module._resolveFileName = function(moduleId) {};

//入口函數
function req(moduleId) {}
複製代碼

附上所有代碼:

const path = require('path');
const fs = require('fs');
const vm = require('vm');

/** * Module類,用於處理模塊加載 */
function Module(file) {
  this.id = file; //當前模塊的id,它使用完整的絕對路徑標識,所以是惟一的
  this.exports = {}; //導出
  this.loaded = false; //模塊是否已加載完畢
}

//模塊的緩存
Module._cacheModule = {};

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

//不一樣擴展名的加載策略
Module._extensions = {
  '.js': function(currentModule) {
    let js = fs.readFileSync(currentModule.id, 'utf8'); //讀取出js文件內容
    let fn = Module._wrapper[0] + js + Module._wrapper[1];
    vm.runInThisContext(fn).call(
      currentModule.exports,
      currentModule.exports,
      req,
      currentModule,
      path.dirname(currentModule.id),
      currentModule.id);
    return currentModule.exports;
  },
  '.json': function(currentModule) {
    let json = fs.readFileSync(currentModule.id, 'utf8');
    return JSON.parse(json); //轉換爲JSON對象返回
  },
  '.node': ''
};

//加載模塊(實例方法)
Module.prototype.load = function(file) {
  let extname = path.extname(file); //獲取後綴名
  return Module._extensions[extname](this);
};

//根據moduleId解析絕對路徑,
Module._resolveFileName = function(moduleId) {
  let p = path.resolve(moduleId);

  if (!path.extname(moduleId)) { //傳入的模塊沒有後綴
    let arr = Object.keys(Module._extensions);

    //循環讀取不一樣擴展名的文件
    for (var i = 0; i < arr.length; i++) {
      let file = p + arr[i]; //拼接上後綴名成爲一個完整的路徑
      try {
        fs.accessSync(file);
        return file; //若此文件存在返回它
      } catch (e) {
        console.log(e);
      }
    }
  } else {
    return p;
  }
};

function req(moduleId) {
  let file = Module._resolveFileName(moduleId);

  if (Module._cacheModule[file]) { //若緩存中存在此模塊
    return Module._cacheModule[file];
  } else {
    let module = new Module(file);
    module.exports = module.load(file);
    return module.exports;
  }
}

console.log(req('./a.js')());
複製代碼

a.js的文件內容:

module.exports = function() {
  console.log('This message from a.js');
  console.log(__dirname);
  console.log(__filename);
}
複製代碼

最終運行結果:

This message from a.js
/Users/cuiyue/workspace/test
/Users/cuiyue/workspace/test/a.js
複製代碼

重要代碼說明

_resolveFileName

_resolveFileName方法的主要做用是把傳入的模塊解析成絕對路徑,這樣才能夠進行下一步,根據完整的路徑加載模塊。

所以要進行判斷,若是傳入的模塊不存在,則要報錯;若是傳入的模塊已經有擴展名了,就不要拼接了;若沒有擴展名,依次以.js .json .node的順序拼接成完成的模塊進行加載。

_extensions

此對象中封裝了加載不一樣類型模塊的處理方法,其中如果.json類型則使用fs讀取文件直接轉換成JSON對象並返回。

如果.js文件則讀取後,拼接閉包,將exports,require,module,__dirname,__filename五大參數拼接好,使用vm模塊的沙箱機制運行,獲得的結果放入module.exports返回。

總結

以上就是node.js的模塊加載的簡單邏輯,實際上node.js的源碼遠遠比上面的代碼複雜,光是處理模塊路徑、判斷合法等操做就寫了N行。並且我這裏沒有寫緩存以及其它的複雜邏輯,但核心差很少就是這些,核心的核心就是用fs.readFileSync讀取js文件,把內容拼接到一個大大的閉包中,這也解釋了爲何咱們本身寫的全部node模塊中都會有require方法,exports導出,以及__dirname和__filename參數。

瞭解了node.js的模塊加載邏輯,在之後寫node.js就更可避免一些誤解,寫出精細的代碼。

相關文章
相關標籤/搜索