基于源码剖析nodejs模块系统

nodejs模块系统

简介

为了让Node.js的文件可以相互调用,Node.js提供了一个简单的模块系统。

模块是Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,

一个 Node.js 文件就是一个模块,这个文件可能是JavaScript 代码、JSON 或者编译过的C/C++ 扩展。

nodejs模块分类

  • 原生模块(核心模块):fs、http、net等
    在Node进程启动时,部分核心模块就被直接加载进内存中,这部分核心 模块引入时,文件定位和编译执行个步骤可以省略掉,并且在路径分析 中优先判断,所以它的加载速度是最快的。
  • 文件模块:用户编写的模块
    文件模块是运行时动态加载,需要完整的路径分析、文件定位、编译执行 过程,速度比核心模块慢。
  • 第三方模块:art-template、通过npm下载的

模块系统关键字

  • require
  • module.exports/exports

Node.js中没有全局作用域,只有模块作用域

​ ——外部访问不到内部,内部访问不到外部

node模块require引入分析

模块引⼊三部曲:

  • 路径分析
  • ⽂件定位
  • 编译执⾏

引入规则

var 自定义变量名称 = require模块

1、加载文件模块,并执行里面的代码;

2、拿到被加载的文件模块导出的模块对象。

系统模块引入

var net = require(“net”);

var fs = require(“fs”);

文件模块引入

require(‘/文件名’);//绝对路径

require(‘./文件名’);//相对路径

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(⽂件绝对路径)

1、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){
  //1、判断是否是⾃身
  if(id == 'native_module'){
    return NativeModule
  }

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

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

  //4、存放在模块加载列表⾥
  process.moduleLoadList.push('NativeModule ' + id);

  //5、载⼊该原⽣模块、缓存、编译、返回
  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”
  • 尾部:“})”

2、包装后的代码会通过vm原生模块的runInThisContext()方法,返回一个具体的 function对象。

3、将当前模块对象的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-8、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://juejin.im/post/5ab4d3d151882521d6578298

https://segmentfault.com/a/1190000012086435

https://www.jianshu.com/p/ed3435661583

https://my.oschina.net/luyongfugx/blog/395455?utm_source=tuicoolhttps://segmentfault.com/a/1190000012373889

https://blog.csdn.net/w_q_1025/article/details/54896346

https://www.jianshu.com/p/609489e8c929

https://www.jianshu.com/p/99d78efeae25

源码:

https://github.com/nodejs/node/blob/v5.x/lib/module.js

https://github.com/nodejs/node/blob/v5.x/lib/internal/module.js

https://github.com/nodejs/node/blob/v5.x/lib/internal/bootstrap_node.js


   转载规则


《基于源码剖析nodejs模块系统》 浅夏晴空 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
GMTC大前端技术演变 GMTC大前端技术演变
GMTCGMTC全球大前端技术大会是由极客邦科技旗下InfoQ中国主办的技术盛会,关注前端、移动、AI应用等多个技术领域,促进全球技术交流,推动国内技术升级。GMTC为期4天,包括两天的会议和两天的培训课,主要面向各行业前端、移动开发
2020-12-30
下一篇 
React中使用Vditor详解 React中使用Vditor详解
安装 npm install vditor -s 引用导入依赖包 import Vditor from “vditor”; 导入样式 import “vditor/src/assets/scss/index.scss”; 使用示例
2020-12-24
  目录