【模塊化系列】Nodejs模塊化的原理

1、前言

node的應用是模塊組成的,Node遵循commonjs的模塊規範,用來隔離每一個模塊的做用域,使每個模塊在自身的命名空間中執行。node

commonjs的主要內容:json

模塊必須經過module.exports導出對外的變量或接口,經過require()來導入其餘模塊的輸出到當前模塊做用域中。數組

commonjs模塊特色:瀏覽器

一、全部代碼運行在當前模塊做用域中,不會污染全局做用域。緩存

二、模塊同步加載,根據代碼的順序加載。函數

三、模塊能夠屢次加載,只會在第一次加載時運行一次,而後運行結果會被緩存,之後再加載,就直接從緩存中讀取結果,如若想要模塊再次運行,必須清除緩存。性能

咱們先來 看一下簡單的例子ui

編寫一個demo-exports.jsspa

let name = 'saucxs';
let getName = function (name) {
    console.log(name)
};

module.exports = {
    name: name,
    getName: getName
}

咱們再來編寫一個demo-require.js3d

let person = require('./demo-export')

console.log(person, '-------------')       // { name: 'saucxs', getName: [Function: getName] } 

console.log(person.name, '===========')   // saucxs
person.getName('gmw');                    // gmw

person.name = 'updateName'
console.log(person, '22222222')           //  { name: 'updateName', getName: [Function: getName] }

console.log(person.name, '3333333')       // updateName
person.getName('gmw')                     // gmw

node demp-require.js,結果如上所示。

2、module對象

commonjs規範,每個文件就是一個模塊,每個模塊中都有一個module對象,這個對象就指向當前模塊。module對象具備以下屬性:

(1)id:當前模塊id。

(2)exports:表示當前模塊暴露給外部的值。

(3)parent:是一個對象,表示調用當前模塊的模塊。

(4)children:是一個對象,表示當前模塊調用的模塊。

(5)filename:模塊的絕對路徑

(6)paths:從當前模塊開始查找node_modules目錄,而後依次進入到父目錄,查找父目錄下的node_modules目錄;依次迭代,直到根目錄下的node_modules目錄。

(7)loaded:一個布爾值,表示當前模塊是否被徹底加載。

咱們來看一下栗子

module.js

module.exports = {
    name: 'saucxs',
    getName: function (name) {
        console.log(name)
    }
}

console.log(module)

node module.js

image

一、module.exports

咱們知道了module對象有一個exports屬性,該屬性用來對外暴露變量,方法或者整個模塊。當其餘文件須要require該模塊的時候,實際上讀取的是module對象中的exports屬性。

二、exports對象

既然都有了module.exports就能知足全部的需求,爲啥還有一個exports對象呢?

咱們如今來看一下二者的關係:

(1)exports對象和module.exports都是引用類型變量,指向同一個內存地址,在node中,二者一開始都是指向一個空對象的。

exports = module.exports = {}

(2)其次,exports對象是經過形參的方式傳入,直接賦值給形參的引用,可是並不能改變做用域外的值。

var module = {
    exports: {}
};

var exports = module.exports;

function change(exports) {
    /*爲形參exports添加屬性name,會同步到外部的module.exports對象*/
    exports.name = 'saucxs'
    /*在這裏修改wxports的引用,並不會影響到module.exports*/
    exports = {
        age: 18
    }
    console.log(exports)  // {age: 18}
}

change(exports);
console.log(module.exports);   // {exports: {name: 'saucxs'}}

分析上述代碼:

直接給exports賦值,會改變當前模塊內部的形參exports的對象應用。說明當前的exports對象已經跟外部的module.exports對象沒有任何關係,因此改變exports對象不會影響到module.exports。

注意:module.exports就是爲了解決上述exports直接賦值的問題,會致使拋出不成功的問題而產生的。

//這些操做都是合法的
exports.name = 'saucxs';
exports.getName = function(){
    console.log('saucxs')
};


//至關於下面的方式
module.exports = {
    name: 'saucxs',
    getName: function(){
        console.log('saucxs')
    }
}

//或者更常規的寫法
let name = 'saucxs';
let getName = function(){
    console.log('saucxs')
}
module.exports = {
    name: name,
    getName: getName
}

這樣就能夠不用每次都把要拋出對象或者方法直接賦值給exports屬性,直接採用對象字面量的方式更加方便。

3、require方法

require是模塊的引入規則,經過exports或者module.exports拋出一個模塊,經過require方法傳入模塊標識符,而後node根據必定的規則引入該模塊,咱們就可使用模塊中定義的方法和屬性。

(一)node中引入模塊的機制

一、在node中引入模塊,須要經歷3個步驟:

(1)路徑分析

(2)文件定位

(3)編譯執行

二、在node中,模塊分爲兩種:

(1)node提供的模塊,例如http模塊,fs模塊等,稱爲核心模塊。核心模塊在node源代碼編譯過程當中就有編譯了二進制文件,在node進程啓動的時候,部分核心模塊就直接加載進內存中,所以這部分模塊是不用經歷上述的(2),(3)步驟,並且在路徑分析中優先判斷,所以加載速度是最快的。

(2)用戶本身編寫的模塊,稱爲文件模塊。文件模塊是須要按需加載的,須要經歷上述的三個步驟,速度較慢。

三、優先從緩存中加載

瀏覽器會緩存靜態腳本文件以提升頁面性能同樣,Node對引入過的模塊也會進行緩存。與瀏覽器不一樣的是:Node緩存的是編譯執行以後的對象而不是靜態文件。咱們舉個例子看一下

requireA.js

console.log('模塊requireA開始加載...')
exports = function() {
    console.log('Hi')
}
console.log('模塊requireA加載完畢')
init.js

var mod1 = require('./requireA')
var mod2 = require('./requireA')
console.log(mod1 === mod2)

執行node init.js

image

雖然咱們兩次引入requireA這個模塊,可是模塊中的代碼其實只執行了一遍。而且mod1和mod2指向了同一個模塊。

四、module._load源碼

Module._load = function(request, parent, isMain) {

  //  計算絕對路徑
  var filename = Module._resolveFilename(request, parent);

  //  第一步:若是有緩存,取出緩存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;

  // 第二步:是否爲內置模塊
  if (NativeModule.exists(filename)) {
    return NativeModule.require(filename);
  }

  // 第三步:生成模塊實例,存入緩存
  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  // 第四步:加載模塊
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  // 第五步:輸出模塊的exports屬性
  return module.exports;
};

對應的流程以下:

image

(二)路徑分析和文件定位

一、路徑分析

模塊標識符分析:

(1)核心模塊,如http,fs模塊。

(2)以 . 或者 ../ 開始的相對路徑文件模塊。

(3)以 / 開始的絕對路徑模塊。

(4)非路徑形式的文件模塊。

分析:

(1)核心模塊:優先級僅次於緩存,加載速度最快。若是自定義模塊和核心模塊名稱相同,加載會失敗。若想成功,必須修改自定義模塊的名稱或者換個路徑。

(2)路徑形式的文件模塊:以 . 或者 .. 或者 / 開始的標識符,都會被當作文件模塊來處理。加載過程當中,require方法將路徑轉換爲真實的路徑,加載速度僅次於核心模塊。

(3)非路徑形式的自定義模塊:這是一種特殊的文件模塊,多是一個文件或者包的形式。查找這類模塊的策略相似於js做用域鏈,node會逐個嘗試模塊路徑中的路徑,知道找到目標文件爲止。

注意:這是node定位文件模塊的具體文件的時候的查找策略,具體表現爲一個路徑的組成的數組。

能夠在REPL環境中輸出Module對象,查看其path屬性的方式查看上述數組,文章開始的paths數組:

image

二、文件定位

(1)文件拓展名分析

require()分析的標識符能夠不包含擴展名,node會按.js、.node、.json的次序補足擴展名,依次嘗試

(2)目標分析和包

若是在擴展名分析的步驟中,查找不到文件而是查找到相應目錄,此時node會將目錄當作包來處理,進行下一步分析查找當前目錄下package.json中的main屬性指定的文件名,若查找不成功則依次查找index.js,index.node,index.json。

若是目錄分析的過程當中沒有定位到任何文件,則自定義模塊會進入下一個模塊路徑繼續查找,直到全部的模塊路徑都遍歷完畢,依然沒找到則拋出查找失敗的異常。

(3)參考源碼

在Module._load方法的內部調用了Module._findPath這個方法,這個方法是用來返回模塊的絕對路徑的,源碼以下:

Module._findPath = function(request, paths) {

  // 列出全部可能的後綴名:.js,.json, .node
  var exts = Object.keys(Module._extensions);

  // 若是是絕對路徑,就再也不搜索
  if (request.charAt(0) === '/') {
    paths = [''];
  }

  // 是否有後綴的目錄斜槓
  var trailingSlash = (request.slice(-1) === '/');

  // 第一步:若是當前路徑已在緩存中,就直接返回緩存
  var cacheKey = JSON.stringify({request: request, paths: paths});
  if (Module._pathCache[cacheKey]) {
    return Module._pathCache[cacheKey];
  }

  // 第二步:依次遍歷全部路徑
  for (var i = 0, PL = paths.length; i < PL; i++) {
    var basePath = path.resolve(paths[i], request);
    var filename;

    if (!trailingSlash) {
      // 第三步:是否存在該模塊文件
      filename = tryFile(basePath);

      if (!filename && !trailingSlash) {
        // 第四步:該模塊文件加上後綴名,是否存在
        filename = tryExtensions(basePath, exts);
      }
    }

    // 第五步:目錄中是否存在 package.json 
    if (!filename) {
      filename = tryPackage(basePath, exts);
    }

    if (!filename) {
      // 第六步:是否存在目錄名 + index + 後綴名 
      filename = tryExtensions(path.resolve(basePath, 'index'), exts);
    }

    // 第七步:將找到的文件路徑存入返回緩存,而後返回
    if (filename) {
      Module._pathCache[cacheKey] = filename;
      return filename;
    }
 }
    
  // 第八步:沒有找到文件,返回false 
  return false;
};

(三)清除緩存

根據上述的模塊引入機制咱們知道,當咱們第一次引入一個模塊的時候,require的緩存機制會將咱們引入的模塊加入到內存中,以提高二次加載的性能。可是,若是咱們修改了被引入模塊的代碼以後,當再次引入該模塊的時候,就會發現那並非咱們最新的代碼,這是一個麻煩的事情。如何解決呢?

require(): 加載外部模塊

require.resolve():將模塊名解析到一個絕對路徑

require.main:指向主模塊

require.cache:指向全部緩存的模塊

require.extensions:根據文件的後綴名,調用不一樣的執行函數

解決辦法:

//刪除指定模塊的緩存
delete require.cache[require.resolve('/*被緩存的模塊名稱*/')]

// 刪除全部模塊的緩存
Object.keys(require.cache).forEach(function(key) {
     delete require.cache[key];
})

而後咱們再從新require進來須要的模塊就能夠了。

相關文章
相關標籤/搜索