模块加载

2016-11-22  本文已影响0人  狐尼克朱迪

模块加载

基本知识

Node中的模块分为以下几类:

在模块加载时,Node会按照 .js .json .node的次序补足扩展名,依次尝试。对于第四种的自定义模块,Node在加载时会从当前文件目录下的node_modules文件开始,依次遍历父文件夹进行查找。 项目的目录为test,通过module.paths可以知道模块查找时可能遍历的路径:

模块查找路径
require源码解析

在Node中,需通过 var module = require("module") 这种形式调用模块,其内部实现逻辑如下:

// 模块加载入口
Module._load = function(request, parent, isMain) {
  // 返回文件名 会调用到__findpath
  var filename = Module._resolveFilename(request, parent);

  // 有缓存直接返回缓存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }
  ...
  // 加载文件
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  return module.exports;
};

// 根据参数 返回文件名, _findPath的逻辑是
// 1. 若模块的路径不以 / 结尾,则先检查该路径是否真实存在: 
// 2. 若存在且为一个文件,则直接返回文件路径作为结果。 
// 3. 若存在且为一个目录,则尝试读取该目录下的 package.json 中 main 属性所指向的文件路径。 
// 4. 判断该文件路径是否存在,若存在,则直接作为结果返回。 
// 5. 尝试在该路径后依次加上 .js , .json 和 .node 后缀,判断是否存在,若存在则返回加上后缀后的路径。 
// 6. 尝试在该路径后依次加上 index.js index.json 和 index.node,判断是否存在,若存在则返回拼接后的路径。 
// 7. 若仍未返回,则为指定的模块路径依次加上 .js , .json 和 .node 后缀,判断是否存在,若存在则返回加上后缀后的路径
Module._resolveFilename = function(request, parent) {
  ...
  var filename = Module._findPath(request, paths);
  ...
  return filename;
};


// 加载一个文件
Module.prototype.load = function(filename) {
  ...
  Module._extensions[extension](this, filename);
  ...
};

// 以.js结尾的文件为例  load函数 会执行到_compile方法中去
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};

Module.prototype._compile = function(content, filename) {
  // 包裹脚本
  var wrapper = Module.wrap(content);
  var compiledWrapper = runInThisContext(wrapper,{ filename: filename, lineOffset: 0 });
  ...

  // 执行逻辑
  const args = [this.exports, require, this, filename, dirname];
  const result = compiledWrapper.apply(this.exports, args);
  return result;
};

// Module.wrap的逻辑  把脚本前后包括起来,形成一个函数
NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
];

通过对模块加载流程的梳理,可知module是对象,而require是函数;且它俩实际上是一个函数的参数,并不是全局属性:

console.log(require) // Function  
console.log(module) // Object  
console.log(global.require) // undefined  
console.log(global.module) // undefined 

模块加载的问题

我们假设的场景是:test和test1是一个大工程的两个子工程,为了维护整个工程中模块的统一(版本一致、同步更新等),我们希望一个模块在工程中只有一份代码存在。两个子工程的文件夹名称和工程一样。如果在开发时,test工程想引用test1工程下的模块,那么我们可以采用如下方法:

  var moduleA = require("../test1/node_modules/moduleA")

这种方式有两个问题: 1. 写法比较丑 2. 难以维护。尤其是第二条,在真正的开发时,这是个比较令人头疼的地方,因此需要一个比较好的解决方法。

上述采用的是相对路径,我们可以通过全局的绝对路径进行实现:

global._rootTest1 = '/Users/ahu/test1/node_modules';
var path = require('path');
var moduleA = require(path.join(_rootTest1,'moduleA'));

这个和相对路径类似,有点换汤不换药的感觉,但也不失为一种方法。

模块加载优化

普通第三方模块加载只需要require进来就好,没有路径的问题;因此我们以此为目标考虑我们模块的加载优化。

var moduleA = require('moduleA');
修改module.paths

我们知道module.paths是模块查找时要遍历的文件夹路径,如果往其中添加模块所在的路径,那么就可以直接通过模块名加载到模块了:

module.paths.push('/Users/ahu/test1/node_modules');
console.log(module.paths);
var moduleA = require('moduleA');
Paste_Image.png
在考虑效率的情况下,需要依据路径下模块数决定其在module.paths的位置,数组位置越靠前,模块加载的优先级越高。

虽然基本目的达到了,但是对其原理不是很了解。我们从模块加载的源码进行探索:

// 初始化全局的依赖加载路径
Module._initPaths = function() {
  ...
  var paths = [path.resolve(process.execPath, '..', '..', 'lib', 'node')];

  ...
  // 我们需要着重关注此处,获取环境变量“NODE_PATH”
  var nodePath = process.env['NODE_PATH'];
  if (nodePath) {
    paths = nodePath.split(path.delimiter).concat(paths);
  }

  // modulePaths记录了全局加载依赖的根目录,在Module._resolveLookupPaths中有使用
  modulePaths = paths;
};

// @params: request为加载的模块名 
// @params: parent为当前模块(即加载依赖的模块)
Module._resolveLookupPaths = function(request, parent) {
  ...
 
  var start = request.substring(0, 2);
  // 若为引用模块名的方式,即require('moduleA')
  if (start !== './' && start !== '..') {
    // 此处的modulePaths即为Module._initPaths函数中赋值的变量
    var paths = modulePaths;
    if (parent) {
      if (!parent.paths) parent.paths = [];
      paths = parent.paths.concat(paths);
    }
    return [request, paths];
  } 
  ...
};

通过Node module加载的源码可知,影响模块加载的有以下几点:

基于这两点我们进行尝试。

NODE_PATH

可以修改系统环境变量中的NODE_PATH,需要保证开发、测试、发布环境同步进行修改,比较麻烦;而且由于影响范围较大,可能影响程序的正常运行:

export NODE_PATH=/Users/ahu/test1/node_modules

也可以在服务启动时修改NODE_PATH,如下方式:

NODE_PATH=/Users/ahu/test1/node_modules  node test.js

这个影响访问小,发布环境中借组启动脚本可以比较优雅的实现,但是开发时有可能比较麻烦。

process.env

除了上面两种,可以在程序中修改process.env中的NODE_PATH进行实现,但是由于_initPaths只执行一次而且已经执行完毕,因此需要重新执行一边:

process.env.NODE_PATH='/Users/ahu/test1/node_modules';
require('module').Module._initPaths();

var moduleA = require('moduleA');

总结

本文提出的优化方法都是对 NODE_PATH 进行修改,包括对系统环境变量和程序运行环境变量修改两方面。app-module-path这个模块也通过类似的方法进行实现。

参考文章

module源码
nativeModule源码
通过源码解析 Node.js 中一个文件被 require 后所发生的故事
node模块加载层级优化

上一篇下一篇

猜你喜欢

热点阅读