基於源碼剖析nodejs模塊系統

nodejs模塊系統

簡介

爲了讓Node.js的文件能夠相互調用,Node.js提供了一個簡單的模塊系統。 html

模塊是Node.js 應用程序的基本組成部分,文件和模塊是一一對應的。換言之, node

一個 Node.js 文件就是一個模塊,這個文件多是JavaScript 代碼、JSON 或者編譯過的C/C++ 擴展。git

nodejs模塊分類

  • 原生模塊(核心模塊):fs、http、net等
    在Node進程啓動時,部分核心模塊就被直接加載進內存中,這部分核心 模塊引入時,文件定位和編譯執行個步驟能夠省略掉,而且在路徑分析 中優先判斷,因此它的加載速度是最快的。
  • 文件模塊:用戶編寫的模塊
    文件模塊是運行時動態加載,須要完整的路徑分析、文件定位、編譯執行 過程,速度比核心模塊慢。
  • 第三方模塊:art-template、經過npm下載的

模塊系統關鍵字

  • require
  • module.exports/exports

Node.js中沒有全局做用域,只有模塊做用域 github

​ ——外部訪問不到內部,內部訪問不到外部npm

node模塊require引入分析

模塊引⼊三部曲:json

  • 路徑分析
  • ⽂件定位
  • 編譯執⾏

引入規則

var 自定義變量名稱 = require 模塊

一、加載文件模塊,並執行裏面的代碼; bootstrap

二、拿到被加載的文件模塊導出的模塊對象。數組

系統模塊引入

var net = require(「net」);

var fs = require(「fs」);緩存

文件模塊引入

require('/文件名');//絕對路徑

require('./文件名');//相對路徑 app

require('../文件名')

若是直接引入會怎樣呢?var test = require(「test」);

image-20201225162128528

引入規則

  • 若是有「./」從當前目錄查找
  • 若是沒有「./」,先從系統模塊,再從node_modules下查找

路徑分析&文件定位

模塊標識符分析:對於不一樣的標識符,模塊的查找和定位不一樣。

  • 核心模塊, 如http、fs、path等
  • 「.」或「..」開始的相對路徑文件模塊
  • 以「/」開始的絕對路徑文件模塊
  • 非路徑形式的文件模塊,如che-ui模塊

require()方法會將路徑解析爲真 實路徑,並以真實路徑進行加 載編譯

文件定位:

  • 文件擴展名分析
  • 目錄分析和包

代碼追蹤棧:

Module.prototype.require --> Module.load --> Module.resolveFilename -->

Module.resolveLookupPaths --> Module._fifindPath --> fifileName(⽂件絕對路徑)

一、Module.prototype.require require入口

經過給定的path加載⼀個模塊,並返回該模塊的exports屬性。

const assert = require('assert').ok;
...
// Loads a module at the given file path. Returns that module's 'exports'
property
Module.prototype.require = function(path) {
  assert(path, "missing path");//path不能爲空
  assert(typeof path === "string", "path must be a string");//path必須是字
  符串類型
  return Module._load(path, this, false);//加載模塊並返回exports
}

assert

assert是Node.js中的斷⾔模塊: 提供簡單的斷⾔測試功能,主要⽤於內部使⽤,也能夠

require('assert') 後在外部進⾏使⽤。

模塊⽅法:

  • assert(value[,message]) == assert.ok(value[,message])
  • 若是value的值爲true,那麼什麼也不會發⽣;若是value爲false,將拋出⼀個信息爲message的錯誤。

實例:

image-20201225111727573

2、加載⽂件⽅法Module._load

調⽤Module._resolveFilename獲取⽂件絕對路徑,而且根據該絕對路徑添加緩存以及編譯模塊。

Module._load = function(request, parent, isMain) {
  //...
  var filename = Module.resolveFilename(request, parent); //路徑解析,絕對路徑
  //...
}

3、解析路徑⽅法 Module._resolveFilename

獲取⽂件絕對路徑。

Module._resolveFilename = function(request, parent){
  //是原⽣模塊而且不是原⽣內部模塊則直接返回
  if(NativeModule.nonInternalExists(request)){
    return request;
  }
  //計算全部可能的路徑
  var resolvedModule = Module._resolveLookupPaths(request, parent);
  var id = resolvedModule[0];
  var paths = resolvedModule[1];
  //計算⽂件的絕對路徑
  var filename = Module._findPath(request, paths);
  if(!filename){
    var err = new Error(`Cannot find module '${request}'`);
    err.code = "MODULE_NOT_FOUND";
    throw err;
  }
  //返回⽂件絕對路徑
  return filename; 
}

NativeModule.nonInternalExists

nonInternalExists是Node.js原⽣模塊提供的⽅法,⽤於判斷:是原⽣模塊而且不是原⽣內部模塊。

實現⽅法⾃⾏欣賞:

NativeModule.nonInternalExists = function(id){
    return NativeModule.exists(id) && !NativeModule.isInternal(id);
}

NativeModule.isInternal = function(id){
    return id.startsWith('internal/');
}
node/lib/module.js ⽂件開頭引⼊的兩個原⽣內部模塊 const internalModule =require('internal/module'); //internal/module 便是路徑名也是id const internalUtil =require('internal/util');

也就是說在咱們⾃⼰的代碼⾥⾯是請求不到Node.js源碼⾥⾯ lib/internal/*.js 這些⽂件的,⽐如 require("internal/module") 運⾏時會報錯 Error: Cannot find module'internal/module'

特例 require("internal/repl")能夠執⾏,具體什麼應⽤場景,請⾃⾏查找。

寫個測試⽂件,在⾥⾯打印 process.moduleLoadList ,能夠查看已經加載的原⽣模塊。

4Module._resolveLookupPaths

計算全部可能的路徑,對於核⼼模塊、相對路徑、絕對路徑、⾃定義模塊返回不一樣的數組。實現代碼相對較複雜不作分析,只看執⾏結果

image-20201225113943994

5Module._fifindPath

根據⽂件可能路徑定位⽂件絕對路徑,包括後綴的補全(.js , .json, .node)

Module._findPath = function(request, paths){
    //絕對路徑,將 paths 清空
    if(path.isAbsolute(request)){
        paths = [''];
    }
    //第⼀步:若是當前路徑已在緩存中,直接返回緩存
    var cacheKey = JSON.stringify({request: request, paths: paths});
    if (Module._pathCache[cacheKey]) {
        return Module._pathCache[cacheKey];
    }
    //獲取後綴名:.js, .json, .node
    const exts = Object.keys(Module._extensions);
    //模塊路徑是否以/結尾,若是路徑以/結尾,那麼就是⽂件夾
    const trailingSlash = request.slice(-1) === '/';
  
    // 第⼆步,依次遍歷全部路徑
    for (var i = 0, PL = paths.length; i < PL; i++) {
        // Don't search further if path doesn't exist
        if (paths[i] && stat(paths[i]) < 1) continue;var basePath = path.resolve(paths[i], request);
        var filename;
        if (!trailingSlash) { // 模塊路徑⾮「/」結尾,那麼多是⽂件,也多是⽂件夾
          const rc = stat(basePath); // 判斷⽂件類型,是⼀個⽂件仍是⽬錄
          if (rc === 0) { 
            //a. 若是是⼀個⽂件,則轉換爲真實路徑
            filename = toRealPath(basePath);
          } else if (rc === 1) { 
            //b. 若是是⼀個⽬錄,則調⽤tryPackage⽅法讀取該⽬錄下的
            package.json⽂件,把⾥⾯的 main屬性設置爲filename
            filename = tryPackage(basePath, exts);
          }
          //c. 若是沒有讀到路徑上的⽂件,則經過tryExtensions嘗試在該路徑後依次加上.js,.json 和.node後            綴,判斷是否存在,若存在則返回加上後綴後的路徑
          if (!filename) {
            filename = tryExtensions(basePath, exts);
          } 
        }

      //第三步:若是依然不存在,則一樣調⽤tryPackage⽅法讀取該⽬錄下的package.json⽂件,把⾥⾯的           main屬性設置爲filename
      if (!filename) {
        filename = tryPackage(basePath, exts);
      }

      //第四步: 若是依然不存在,則嘗試在該路徑後依次加上index.js,index.json和index.node,判斷是 否 存在,若存在則返回拼接後的路徑。
      if (!filename) {
        // try it with each of the extensions at "index"
        filename = tryExtensions(path.resolve(basePath, 'index'), exts);
      }

      //第五步:若解析成功,則把解析獲得的⽂件名cache起來,下次require就不⽤再次解析了
      if (filename) {
        // Warn once if '.' resolved outside the module dir
        if (request === '.' && i > 0) {
          warned = internalUtil.printDeprecationMessage(
          'warning: require(\'.\') resolved outside the package ' +
          'directory. This functionality is deprecated and will be
          removed ' +'soon.', warned);
        }
        Module._pathCache[cacheKey] = filename;
        return filename;
      } 
    }
    //第六步: 若解析失敗,則返回false
    return false; 
}

//tryPackage
function tryPackage(requestPath, exts, isMain) {
  var pkg = readPackage(requestPath);
  if (!pkg) return false;
  var filename = path.resolve(requestPath, pkg);
  return tryFile(filename, isMain) || //直接判斷這個⽂件是否存在並返回
  tryExtensions(filename, exts, isMain) || //判斷分別以js,json,node等後綴結尾的⽂件是否存在
  tryExtensions(path.resolve(filename, 'index'), exts, isMain); //判斷分別以${filename}/index.(js|json|node)等後綴結尾的⽂件是否存在
}

//tryExtensions
function tryExtensions(p, exts, isMain) {
  for (var i = 0; i < exts.length; i++) {
    const filename = tryFile(p + exts[i], isMain);
    if (filename) {
      return filename;
    }
  }
  return false; 
}

//tryFile
function tryFile(requestPath) {
    const rc = stat(requestPath);
    return rc === 0 && toRealPath(requestPath);
}

//toRealPath
function toRealPath(requestPath) {
    return fs.realpathSync(requestPath, Module._realpathCache);
}

查找策略

  1. require()傳入的字符串最後一個字符不是/時:

    1. 若是是個文件,直接返回這個文件的路徑
    2. 若是是個文件夾,則查找該文件夾下是否有package.json文件,以及這個文件 當中的main字段對應的路徑(對應源碼當中的方法爲tryPackage):

      1. 若是main字段對應的路徑是一個文件且存在,直接返回這個路徑
      2. 在main字段對應的路徑後依次加上 .js , .json 和 .node 後綴,判斷是否 存在,若存在則返回加上後綴後的路徑。
      3. 在main字段對應的路徑後依次加上 index.js ,index.json 和 index.node, 判斷是否存在,若存在則返回拼接後的路徑。
    3. 對文件路徑後分別添加.js,.json,.node後綴,判斷是否存在,若存在則返回 加上後綴後的路徑。
  2. require()傳入的字符串最後一個字符是/時,即require的是一個文件夾時:

    1. 查詢該文件夾下的package.json文件中的main字段對應的路徑,步驟如1.2
    2. 該路徑後依次加上 index.js ,index.json 和 index.node,判斷是否存在,若 存在則返回拼接後的路徑。

6、路徑解析完畢,再次返回Module._load

Module._load = function(request, parent, isMain) {
  //解析⽂件絕對路徑
  //第⼀步: 先檢查是否在⽂件模塊緩存中,若是有緩存,直接取緩存,Module._cache存放⽂件模塊
  var filename = Module.resolveFilename(request, parent);
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports; 
  }

  //第⼆步: 檢測是不是原⽣模塊,若是是,使⽤原⽣模塊的加載⽅法
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  //第三步: 判斷⽆緩存且⾮原⽣模塊後,新建模塊實例
  var module = new Module(filename, parent);
  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  //加載模塊前,就將模塊緩存
  Module._cache[filename] = module;
  var hadException = true;

  //第四步: 加載模塊
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename]; //加載失敗,刪除緩存
    }
  }
  return module.exports; 
}

NativeModule.require

主要⽤來加載Node.js的⼀些原⽣模塊。

源碼:

NativeModule.require = function(id){
  //一、判斷是不是⾃身
  if(id == 'native_module'){
    return NativeModule
  }

  //二、是否有緩存,原⽣模塊存放在NativeModule._cache中
  var cached = NativeModule.getCached(id);
  if(cached){
    return cached.exports;
  }

  //三、是不是原⽣模塊
  if(!NativeModule.exists(id)){
    throw new Error('No such native module ' + id);
  }

  //四、存放在模塊加載列表⾥
  process.moduleLoadList.push('NativeModule ' + id);

  //五、載⼊該原⽣模塊、緩存、編譯、返回
  var nativeModule = new NativeModule(id);
  nativeModule.cache();
  nativeModule.compile();
  return nativeModule.exports; 
}

NativeModule.prototype.compile = function() {
  var source = NativeModule.getSource(this.id);
  source = NativeModule.wrap(source);
  var fn = runInThisContext(source, { filename: this.filename });
  fn(this.exports, NativeModule.require, this, this.filename);
  this.loaded = true;
};

NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

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

NativeModule.prototype.cache = function() {
    NativeModule._cache[this.id] = this;
};

編譯執⾏

經過步驟5找到對應的文件後Node會新建一個模塊對象,定義以下:

function Module (id, parent) { 
  this.id = id; 
  this.exports = {}; 
  this.parent = parent; 
  if (parent && parent.children) { 
    parent.children.push(this); 
  }
  this.filename = null; 
  this.loaded = false; 
  this.children = []; 
}

根據路徑載入並編譯。對於不一樣的文件擴展名,其載入方法不一樣:

  • .js文件,經過fs模塊同步讀取文件後編譯執行。
  • .node文件。
  • .json文件,經過fs模塊同步讀取文件後,用JSON.parse()解析返回結果。
  • 其他擴展名文件,它們都被看成.js文件載入。

JS模塊編譯

Node對獲取的JavaScript文件內容進行頭尾包裝

  • 頭部: 「(function (exports, require, module, __filename, _dirname {\n」
  • 尾部:「})」

二、包裝後的代碼會經過vm原生模塊的runInThisContext()方法,返回一個具體的 function對象。

三、將當前模塊對象的exports屬性、require()方法、module(模塊對象自身)以及 在文件定位中獲得的完整文件路徑和文件目錄做爲參數傳遞給這個function()執行。執 行後,模塊的exports屬性被返回給調用方。

7、加載模塊 Module.prototype.load

Module.prototype.load = function(filename){

    assert(!this.loaded);

    this.filename = filename;

    //獲取這個module路徑上全部可能的node_modules路徑

    this.paths = Module._nodeModulePaths(path.dirname(filename);

    var extension = path.extname(filename) || ".js";

    if(!Module._extensions[extension]) extension = ".js";

    Module._extensions[extension](this, filename);

    this.loaded = true; 

}

調⽤Module._extension⽅法加載不一樣格式的⽂件

如下爲js⽂件:

Module._extensions[".js"] = function(module, filename){

  var content = fs.readFilSync(filename, 'utf8'); //同步讀取⽂件的⽂本內容

  module._compile(internalModule.stripBOM(content), filename); //編譯

}

stripBOM內部原⽣模塊的⽅法

function stripBOM(content){

  //檢測第⼀額字符是否爲BOM;

  //BOM:它常被⽤來當作標示⽂件是以UTF-八、UTF-16或UTF-32編碼的記號。

  if(content.charCodeAt(0) === 0xFEFF){

    content = content.slice(1);

  }

  return content; 

}

8、編譯⽅法Module.prototype._compile

Module.prototype._compile = function(content, filename){
  /**
   *⽂件頭部
   *Module.wrapper = NativeModule.wrapper;
   *Module.wrap = NativeModule.wrap; 
   */
  var wrapper = Module.wrap(content);
  // vm.runInThisContext在⼀個v8的虛擬機內部執⾏wrapper後的代碼,相似於eval
  var compiledWrapper = runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0
  })

  //...
  const dirname = path.dirname(filename);
  /**
   *這個require並⾮是Module.prototype.require⽅法,
   *⽽是經過internalModule.makeRequireFunction從新構造出來的,
   *這個⽅法內部仍是依賴Module.prototype.require⽅法去加載模塊的,
   *同時還對這個require⽅法作了⼀些拓展。
   */
  const require = internalModule.makeRequireFunction.call(this);
  const args = [this.exports, require, this, filename, dirname];
  const result = compiledWrapper.apply(this.exports, args);
  return result;
}

function makeRequireFunction() {

  const Module = this.constructor;
  const self = this;

  function require(path) {
    try {
      exports.requireDepth += 1;
      return self.require(path);
    } finally {
      exports.requireDepth -= 1;
    }
  }

  function resolve(request) {
    return Module._resolveFilename(request, self);
  }

  require.resolve = resolve;
  require.main = process.mainModule;

  // Enable support to add extra extension types.
  require.extensions = Module._extensions;require.cache = Module._cache;
  return require; 

}
  • require(): 加載外部模塊
  • require.resolve():將模塊名解析到⼀個絕對路徑
  • require.main:指向主模塊
  • require.cache:指向全部緩存的模塊
  • require.extensions:根據⽂件的後綴名,調⽤不一樣的執⾏函數

9、擴展

以node index.js的形式啓動,模塊如何加載?

其實node啓動的原理跟require是⼀樣的,src/node.cc中的node::LoadEnvironment函數會被調⽤,

在該函數內則會接着調⽤lib/internal/bootstrap_node.js中的代碼,並執⾏startup函數,startup函

數會執⾏Module.runMain⽅法,⽽Module.runMain⽅法會執⾏Module._load⽅法,參數就是命令

⾏的第⼀個參數(⽐如: node index.js),如此,跟前⾯介紹的require就⾛到⼀起了。

// bootstrap main module.

Module.runMain = function() {

  // Load the main module--the command line argument.
  Module._load(process.argv[1], null, true);
  // Handle any nextTicks added in the first tick of the program
  process._tickCallback();

};

10、流程圖

image-20201225154755972

image-20201225154847678

Node模塊導出

  • Node.js中是模塊做用域 ,默認文件中的全部成員只在當前文件中有效(關閉原則)
  • 對於但願能夠訪問的模塊成員,需將其掛載到module.exports 或 exports

在 NodeJS 中想要導出模塊中的變量或者函數有三種方式

  • 經過exports.xxx = xxx 導出

a.js

let name = "it6666.top";

function sum(a, b) {
    return a + b;
}

exports.str = name;
exports.fn = sum;

b.js

let aModule = require("./07-a");

console.log(aModule);
console.log(aModule.str);
console.log(aModule.fn(10, 20));

運行結果以下所示:

img

  • 經過 module.exports.xxx = xxx 導出

a.js

let name = "it6666.top";

function sum(a, b) {
    return a + b;
}

module.exports.str = name;
module.exports.fn = sum;

b.js 其實能夠不動的,我把返回值單獨的接收了一下而後在輸出打印。

let aModule = require("./07-a");

console.log(aModule);
console.log(aModule.str);

let res = aModule.fn(10, 20);

console.log(res);

運行結果以下所示:

img

  • 經過 global.xxx = xxx 導出

a.js

let name = "it6666.top";

function sum(a, b) {
    return a + b;
}

global.str = name;
global.fn = sum;

b.js

let aModule = require("./07-a");

console.log(str);
let res = fn(10, 20);
console.log(res);

運行結果以下所示:

img

源碼:

https://github.com/nodejs/nod...

https://github.com/nodejs/nod...

https://github.com/nodejs/nod...

相關文章
相關標籤/搜索