node模块加载层级优化

模块加载痛点

人人也或多或少的相识node模块的加载机制,最为深刻的表述就是顺次从当前目次向上级查询node_modules目次,若发明依靠则加载。然则跟着运用范围的加大,目次层级越来越深,假如在某个模块中想要经由过程require体式格局以依靠称号或相对途径的体式格局援用其他模块就异常贫苦,影响开辟效力和雅观。

示例demo:

// 当前目次: /usr/local/test/index.js
// gulp模块地点途径为 /usr/lib/node_modules

var gulp = require('../../lib/gulp');
gulp.task('say',function(){
    console.log('hello wolrd');
});

现在的条件下,只要采纳上述中相对途径的体式格局援用依靠模块,能够看出上述援用的瑕玷:

  • 貌寝,非常冗杂

  • 轻易失足,难以保护

第二个瑕玷是最难以接收的,在屡次援用模块的情况下题目会被放大,因而急需寻觅某种计划处理多层目次依靠援用,本文将会议论笔者在开辟过程当中的一些尝试,并迎接人人一同议论其他可行性计划。

全局变量法

由于目的是处理毫无雅观又难以明白的相对目次层级,那末能够尝试运用变量完成目次层级的替换。这类计划最为直接,且node加载该依靠的速率最快,无需遍历其他各级目次。然则为了更加通用,笔者常采纳全局变量的体式格局绑定目次关联:

demo:

// 当前目次: /usr/local/test/index.js
// gulp模块地点途径为 /usr/lib/node_modules

global._root = '/usr/lib/node_modules';
var path = require('path');
var gulp = require(path.join(_root,'gulp'));
...

这类计划最为直接,然则可扩展性并不强,而且在多人保护的情况下尤甚,因而发起在单人开辟的小项目中采纳。

直接援用模块名

直接援用模块名,说到底就是直接援用node_modules目次中的依靠,相似援用node默许加载的那些模块,如http,event模块。

demo:

// 当前目次: /usr/local/test/index.js
// gulp模块地点途径为 /usr/lib/node_modules

var gulp = require('gulp');
...

在目次/usr/local/test、/usr/local、/usr、/四个目次下都没有“node_modules”目次或许“node_modules”目次下都没有gulp模块,那末运转这个文件,肯定会报错“MODULE_NOT_FOUND”,这就是我们接下来须要处理的题目,即怎样修正node加载依靠的层级关联

修正依靠加载层级

置信人人进修node也都读过一本书《深切浅出nodejs》,这本书的第二章第二节曾扼要引见node加载依靠所遍历的一些目次,书中让我们在某个测试文件中输出module.paths,效果是一个数组,相似于

['/usr/local/test/node_modules'、'/usr/local/node_modules'、'/usr/node_modules'、'/node_modules']

这给我们一个启示,即加载某个模块的递次就是依据上述数组项的递次顺次推断模块是不是存在,若存在则加载,事实上node也确切是如许做的(下文会针对源码剖析猜测的正确性)。那末,在猜测的基础上我们能够尝试修正该数组下能否影响本模块加载依靠的递次,假如胜利天然优美,如若不胜利需寻觅更加适当的处理计划。

尝试1:

// 当前目次: /usr/local/test/index.js
// gulp模块地点途径为 /usr/lib/node_modules

module.paths.push('/usr/lib/node_modules');
console.log(module.paths);
var gulp = require('gulp');

实行命令,一切正常,胜利了。经由过程输出信息可看出

['/usr/local/test/node_modules'、'/usr/local/node_modules'、'/usr/node_modules'、'/node_modules','/usr/lib/node_modules']

确切修正了依靠查找层级,不过能够看出设置的目次是在数组中的末了一位,这意味着node会在找到gulp依靠前遍历4层目次,末了才在第五层目次中找到它。假如项目中只援用了gulp也还好,然则跟着其他依靠的数目增加,运转时加载依靠/usr/lib/node_modules下的依靠将会消耗不少时候。因而发起人人在项目中评价好依靠的位置,假如适宜的话能够优先加载手动设置的依靠目次:

// 当前目次: /usr/local/test/index.js
// gulp模块地点途径为 /usr/lib/node_modules

module.paths.unshift('/usr/lib/node_modules');
console.log(module.paths);
var gulp = require('gulp');

如许,我们在不知道node底层怎样事变的前提下就完成了目的。哈哈,不过作为一位靠谱的前端(node)工程师,我们不会满足这类水平吧?哈哈!

深切源码探讨

笔者摘出了与模块(依靠)加载相干的代码:

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

  if (homeDir) {
    paths.unshift(path.resolve(homeDir, '.node_libraries'));
    paths.unshift(path.resolve(homeDir, '.node_modules'));
  }

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

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

  // clone as a read-only copy, for introspection.
  Module.globalPaths = modulePaths.slice(0);
};

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

  // 运用eval实行可实行字符串的情况下,parent.id 和parent.filename为空
  if (!parent || !parent.id || !parent.filename) {
    var mainPaths = ['.'].concat(modulePaths);
    mainPaths = Module._nodeModulePaths('.').concat(mainPaths);
    return [request, mainPaths];
  }
  
  ...
};

Module._initPaths函数在默许的生命周期内只实行一次,作用天然是设置全局加载依靠的相对途径。而当每次在文件中实行require加载其他依靠时,Module._resolveLookupPaths函数都邑实行,返回一个包含依靠名和可遍历的目次数组(该数组中的目次项能够加载到依靠,也能够没法加载依靠)。末了的事变就是依据Module._resolveLookupPaths函数返回的效果,遍历目次数组,加载依靠。假如遍历终了后仍没有找到依靠,则抛错。

在剖析完源码后,置信人人也都注重了几点信息:

  1. Module._initPaths函数内部搜检了NODE_PATH环境变量

  2. Module._initPaths函数只实行一次

  3. Module._initPaths函数初始化的全局依靠加载途径与module.paths有关联

那末,我们能够从另一个角度处理依靠加载的题目。

环境变量法

经由过程上一节的源码剖析,我们知道了NODE_PATH的作用,那末怎样运用或许文雅的运用NODE_PATH来处理依靠加载题目呢?

尝试一

最为直接的是,修正体系的环境变量。在linux下,实行

export NODE_PATH=/usr/lib/node_modules

即可处理。

然则,这类计划毕竟不文雅,由于我们的一个项目就修正了体系的环境变量,假如其他项目也采纳这类计划,那末置信体系的NODE_PATH将会变得很长,而且会由于NODE_PATH的子途径递次题目涌现意想不到的争执,因而作为这类处理计划不发起运用。

尝试二

我们愿望只针对当前运转的顺序设置环境变量,不影响其他顺序;而且一旦当前顺序退出,设置的环境变量也被恢复。满足这类需求的完成,最为直观的就是命令行设置。经由过程查阅node手册能够如许运转:

NODE_PATH=/usr/lib/node_modules  node /usr/local/test/index.js

如许,仍能够胜利加载gulp依靠,而不影响体系的环境变量。

然则,命令行的体式格局不言而喻,就是貌寝,贫苦。每次运转顺序都须要提早输入一系列的途径,这类体式格局将代码的可保护性变为了顺序的可保护性,在担任的项目中不适合运用。

尝试三

node运转时给我们供应了一个变量,对,就是process。process是node默许加载的Process模块的一个属性,经由过程process可猎取运用历程的相干信息,同时包含设置的环境变量。

我们能够在运用的进口文件设置环境变量:

// 当前目次: /usr/local/test/index.js
// gulp模块地点途径为 /usr/lib/node_modules
process.env.NODE_PATH='/usr/lib/node_modules';
var gulp = require('gulp');

如许我们在实行文件,意想不到的事变发生了,仍报出“MODULE_NOT_FOUND”毛病。

这是为何呢?缘由仍要追溯到源码。在源码剖析小节中总结了三点,个中第二点提到了Module._initPaths函数只实行一次,这意味着当我们在代码中设置了process.env.NODE_PATH=’/usr/lib/node_modules’;,但是由于此时Module._initPaths已实行终了,因而设置的环境变量并没有被运用。处理这个题目也比较简单,即从新挪用Module._initPaths即可。

// 当前目次: /usr/local/test/index.js
// gulp模块地点途径为 /usr/lib/node_modules
process.env.NODE_PATH='/usr/lib/node_modules';
require('module').Module._initPaths();
// 或许 module.constructor._initPaths()
var gulp = require('gulp');

如许,平安无公害的处理了多基目次下依靠挪用的题目。

总结

本文从现实开辟中碰到的题目动身,提出了几种处理多基目次下依靠的几种计划:

  • 全局变量法

  • 修正module.paths要领

  • 环境变量法(三种完成)

固然,社区另有一些协助处理这类题目的模块,如“app-module-path”,但头脑也迥然不同。在这里和人人一同分享进修收成,愿望对列位有些启示和感悟,不胜感激!

    原文作者:RoyalRover
    原文地址: https://segmentfault.com/a/1190000007514377
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞