美文网首页
模块加载

模块加载

作者: 狐尼克朱迪 | 来源:发表于2016-11-22 12:26 被阅读0次

    模块加载

    基本知识

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

    • 核心模块, 如http fs path等
    • 以 . 或者 .. 开始的相对路径文件
    • 以 / 开始的绝对路径文件
    • 非路径形式的文件模块, 如自定义的connect模块

    在模块加载时,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这个环境变量
    • Module的_initPaths方法,只执行一次

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

    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模块加载层级优化

    相关文章

      网友评论

          本文标题:模块加载

          本文链接:https://www.haomeiwen.com/subject/yoyppttx.html