nodejs模块加载机制

in 编程
关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9

1.CommonJS规范的起因

1.JavaScript没有模块系统。没有原生的支持密闭作用域或依赖管理。
2.JavaScript没有标准库。除了一些核心库外,没有文件系统的API,没有IO流API等。
3.JavaScript没有标准接口。没有如Web Server或者数据库的统一接口。
4.JavaScript没有包管理系统。不能自动加载和安装依赖。

2.CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分。

1.模块引用
require('xxx')
2.模块定义
exports.xxx
module.exports = {}
3.模块标识
模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,
或者以.、..开头的相对路径,或者绝对路径。它可以没有文件名后缀.js。

3.Nodejs模块分类
Node.js的模块分为两类,一类为核心模块(node提供),一类为文件模块(用户编写)。核心模块在Node.js源代码编译的时候编译进了二进制执行文件,在nodejs启动过程中,部分核心模块直接加载进了内存中,所以这部分模块引入时可以省略文件定位和编译执行两个步骤,所以加载的速度最快。另一类文件模块是动态加载的,加载速度比核心模块慢。但是Node.js对核心模块和文件模块都进行了缓存,于是在第二次require时,是不会有重复开销的。其中原生模块都被定义在lib这个目录下面,文件模块则不定性。
ps:核心模块又分为两部分,C/C++编写的Javascript编写的,前者在源码的src目录下,后者则在lib目录下。(lib/*.js)(其中lib/internal部分不提供给文件模块)

注:通过process.moduleLoadList可以查看已经加载的核心模块。 核心模块 = 原生模块

clipboard.png

4.模块引入三步走

1.路径分析
2.文件定位
3.编译执行

1.路径分析
核心模块:如http、fs、path等,速度仅次于缓存。
路径形式的文件:以.或者..开始的相对路径,以/开始的绝对路径。
自定义模块:不属于核心模块也不属于路径形式的标识符。它是一种特殊的文件模块,可能是一个文件或者包的形式。这类模块的查找是最费时的,也是所有方式中最慢的一种。在定位时,会给出一个可能路径的数组
没有mac电脑,直接copy阮老师的图 http://www.ruanyifeng.com/blo...
clipboard.png

clipboard.png

关键源码
https://github.com/nodejs/nod...

clipboard.png

lib/module.js开头就使用了require,略蒙蔽?不要着急,这个js文件中的reuiqre与我们平常使用的是不一样的,此处的require其实是NativeModule.require。而NativeModule的定义在https://github.com/nodejs/nod...
node在启动的时候会去执行bootstrap_node.js这个模块,后续会对此进行分析,暂时只需明白module.js中的require与我们文件模块中使用的require不是同一个即可

路径分析代码追踪栈
Module.prototype.require --> Module._load --> Module._resolveFilename --> Module._resolveLookupPaths --> Module._findPath(文件定位) --> fileName(文件绝对路径)
几个方法的作用小结:
Module.prototype.require:直接调用Module._load并return
Module._load:调用Module._resolveFilename获取文件绝对路径,并且根据该绝对路径添加缓存以及编译模块
Module._resolveFilename:获取文件绝对路径
Module._resolveLookupPaths:获取文件可能路径
Module._findPath:根据文件可能路径定位文件绝对路径,包括后缀补全(.js, .json, .node)等都在此方法中执行,最终返回文件绝对路径

Module.prototype.require = function(path) {
  assert(path, 'missing path');
  assert(typeof path === 'string', 'path must be a string');
  return Module._load(path, this, /* isMain */ false);
};
Module._load = function(request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }

  var filename = Module._resolveFilename(request, parent, isMain);

  // 检测缓存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  // 检测是否是核心模块
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  // Don't call updateChildren(), Module constructor already does.
  var module = new Module(filename, parent);

  // 判断是否是入口模块
  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  // 添加缓存(小碎步,由于缓存在模块编译前就进行了设置,解决了循环依赖的问题!!!)
  Module._cache[filename] = module;

  // 编译模块
  tryModuleLoad(module, filename);

  return module.exports;
};
Module._resolveFilename = function(request, parent, isMain) {
  // 判断是否是核心模块
  if (NativeModule.nonInternalExists(request)) {
    return request;
  }

  // 计算所有可能的路径,对于核心模块,相对路径,绝对路径,自定义模块返回不同的数组
  var paths;
  paths = Module._resolveLookupPaths(request, parent, true);

  // look up the filename first, since that's the cache key.
  // 计算文件的绝对路径
  var filename = Module._findPath(request, paths, isMain);
  if (!filename) {
    var err = new Error(`Cannot find module '${request}'`);
    err.code = 'MODULE_NOT_FOUND';
    throw err;
  }
  return filename;
};

// 暴露给文件模块的文件定位方法
require.resolve = function(request) {
  return Module._resolveFilename(request, self);
};

// 用法
require.resolve('a.js')
// 返回 /home/ruanyf/tmp/a.js
Module._resolveLookupPaths代码相对复杂,这里简单起见只展示一些其执行结果
tt.js文件目录d/wedoctor
node tt.js
console.log(module.constructor._resolveLookupPaths('fs', module, true))
console.log(module.constructor._resolveLookupPaths('/hello', module, true))
console.log(module.constructor._resolveLookupPaths('../../hello', module, true))
console.log(module.constructor._resolveLookupPaths('hello', module, true))

1.加载核心模块的时候,返回 null
2.加载绝对路径的时候,返回
[ 'D:\\wedoctor\\node_modules',
  'D:\\node_modules',
  'C:\\Users\\小韩\\.node_modules',
  'C:\\Users\\小韩\\.node_libraries',
  'D:\\tools\\nodejs\\lib\\node' ]
由于是绝对路径,所以在_findPath方法中会被清空
3.加载相对路径的时候,返回
[ 'D:\\wedoctor' ]
4.加载自定义模块的时候,返回
[ 'D:\\wedoctor\\node_modules',
  'D:\\node_modules',
  'C:\\Users\\小韩\\.node_modules',
  'C:\\Users\\小韩\\.node_libraries',
  'D:\\tools\\nodejs\\lib\\node' ]

上面的数组,就是模块所有可能的路径。基本上是,从当前路径开始一级级向上寻找 node_modules 子目录。
最后那三个路径,主要是为了历史原因保持兼容,实际上已经很少用了。
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;
};

至此在看_load方法中,以及获取到的文件的绝对地址fileName,以此判断缓存,是否是核心模块,如果两者都不是,则进行模块编译tryModuleLoad。继续看tryModuleLoad方法

function tryModuleLoad(module, filename) {
  var threw = true;
  // 调用了module.load方法
  try {
    module.load(filename);
    threw = false;
  } finally {
    if (threw) {
      delete Module._cache[filename];
    }
  }
}
Module.prototype.load = function(filename) {
  debug('load %j for module %j', filename, this.id);

  assert(!this.loaded);
  this.filename = filename;
  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;
};
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};


// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(internalModule.stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};

以js为例最终调用module._compile方法

Module.prototype._compile = function (content, filename) {
  content = internalModule.stripShebang(content);
  // 添加函数包裹
  var wrapper = Module.wrap(content);
  // 把字符串转换成可用函数
  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });
  // 通过call方法,使模块内部this指向module.exports对象,同时将一些方法作为参数传入模块中
  result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
  return result;
};

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

接下来还需要分析,NativeModule是如何注入到module.js中的,NativeModule又是如何加载核心模块的?

NativeModule代码在源码目录lib/internal/bootstrap_node.js中
bootstrap_node.js在node启动中被调用,并且注入c++对象process,bootstrap_node.js 中会对process 进行部分信息初始化,其实这只是很少的一部分,大部分都在 c++ 部分初始化的。
在node.cc文件中我们可以发现process对象初始化代码

clipboard.png

之前我们讲到,nodejs源码中Javascript编写的核心模块都会通过V8附带的js2c.py工具转换成C++里面的数组,生成node_natives.h头文件。
当调用process.binding('natives')时候,该方法将通过js2c.py工具转换出的字符串数组取出,然后重新转换为普通字符串,以对Javascript核心模块进行编译和执行。

clipboard.png

clipboard.png

clipboard.png

继续在该文件中我们可以看到

clipboard.png

clipboard.png

在lib/module.js也是实现被v8工具转换成c++数组,所以通过NativeModule.require('module')对module模块进行编译执行,并且获取module模块导出的Module对象,在执行该对象的runMain函数,回过头来我们看module.js

clipboard.png

原来当我们在命令行执行 node app.js时候 app.js就是process.argv[1],而Module._load这个方法之前就介绍过了。回头来,在Node真正执行app.js之前,做了许多前置工作,包括process对象注入,核心模块的加载等。

参考:
https://github.com/nodejs/nod...
https://github.com/nodejs/nod...
http://www.ruanyifeng.com/blo...
http://f2e.souche.com/blog/a-...

关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9
扫一扫关注公众号添加购物返利助手,领红包
Comments are closed.

推荐使用阿里云服务器

超多优惠券

服务器最低一折,一年不到100!

朕已阅去看看