模块加载痛点

大家也或多或少的了解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”,但思想也大同小异。在这里和大家一起分享学习收获,希望对各位有些启发和感悟,不胜感激!

node模块加载层级优化的更多相关文章

  1. Node - 模块加载与 lerna 提升

    从node_modules 加载模块的过程 如果要加载的模块非核心模块,并且路径不是'/'. '../'和'./'开头,这个模块就会从当前文件夹递归向上在node_modules文件夹中寻找这个模块. ...

  2. node模块加载机制。

  3. node 学习笔记 - Modules 模块加载系统 (1)

    本文同步自我的个人博客:http://www.52cik.com/2015/12/11/learn-node-modules-path.html 用了这么久的 require,但却没有系统的学习过 n ...

  4. Node.js模块 加载笔记

    //核心模块就是Node.js标准API种提供的模块,如fs,http,net.vm等.官方提供,编译成二进制代码//核心模块拥有最高的加载优先级 //文件模块则是存储为单独的文件(或文件夹)的模块, ...

  5. 【 js 模块加载 】深入学习模块化加载(node.js 模块源码)

    一.模块规范 说到模块化加载,就不得先说一说模块规范.模块规范是用来约束每个模块,让其必须按照一定的格式编写.AMD,CMD,CommonJS 是目前最常用的三种模块化书写规范.  1.AMD(Asy ...

  6. 【 js 模块加载 】【源码学习】深入学习模块化加载(node.js 模块源码)

    文章提纲: 第一部分:介绍模块规范及之间区别 第二部分:以 node.js 实现模块化规范 源码,深入学习. 一.模块规范 说到模块化加载,就不得先说一说模块规范.模块规范是用来约束每个模块,让其必须 ...

  7. Node.js require 模块加载原理 All In One

    Node.js require 模块加载原理 All In One require 加载模块,搜索路径 "use strict"; /** * * @author xgqfrms ...

  8. 构建服务端的AMD/CMD模块加载器

    本文原文地址:http://trock.lofter.com/post/117023_1208040 . 引言:  在前端开发领域,相信大家对AMD/CMD规范一定不会陌生,尤其对requireJS. ...

  9. 小矮人Javascript模块加载器

    https://github.com/miniflycn/webkit-dwarf 短小精悍的webkit浏览器Javascript模块加载器 Why 我们有许多仅基于webkit浏览器开发的应用 无 ...

随机推荐

  1. 通过ProGet搭建一个内部的Nuget服务器

    .NET Core项目完全使用Nuget 管理组件之间的依赖关系,Nuget已经成为.NET 生态系统中不可或缺的一个组件,从项目角度,将项目中各种组件的引用统统交给NuGet,添加组件/删除组件/以 ...

  2. x:bind不支持样式文件 或 此Xaml文件必须又代码隐藏类才能使用{x:Bind} 解决办法

    这两天学习UWP开发,发现一个很有趣的问题,就是我题目中的描述的. 我习惯了在ResourceDictionary中写样式文件,但是发现用x:Bind时会有问题 如果是写在Style里,则提示 “x: ...

  3. 《Django By Example》第二章 中文 翻译 (个人学习,渣翻)

    书籍出处:https://www.packtpub.com/web-development/django-example 原作者:Antonio Melé (译者注:翻译完第一章后,发现翻译第二章的速 ...

  4. Xamarin+Prism小试牛刀:定制跨平台Outlook邮箱应用(后续)

    在[Xamarin+Prism小试牛刀:定制跨平台Outlook邮箱应用]里面提到了Microsoft 身份认证,其实这也是一大块需要注意的地方,特作为后续补充这些知识点.上章是使用了Microsof ...

  5. spring源码分析之<context:property-placeholder/>和<property-override/>

    在一个spring xml配置文件中,NamespaceHandler是DefaultBeanDefinitionDocumentReader用来处理自定义命名空间的基础接口.其层次结构如下: < ...

  6. Oracle 数据库语句大全

    Oracle数据库语句大全 ORACLE支持五种类型的完整性约束 NOT NULL (非空)--防止NULL值进入指定的列,在单列基础上定义,默认情况下,ORACLE允许在任何列中有NULL值. CH ...

  7. bzoj3208--记忆化搜索

    题目大意: 花花山峰峦起伏,峰顶常年被雪,Memphis打算帮花花山风景区的人员开发一个滑雪项目.    我们可以把风景区看作一个n*n的地图,每个点有它的初始高度,滑雪只能从高处往低处滑[严格大于] ...

  8. scala练习题1 基础知识

    1, 在scala REPL中输入3. 然后按下tab键,有哪些方法可以被调用? 24个方法可以被调用, 8个基本类型: 基本的操作符, 等:     2,在scala REPL中,计算3的平方根,然 ...

  9. 如何使用swing创建一个BeatBox

    首先,我们需要回顾一些内容(2017-01-04 14:32:14): 1.Swing组件 Swing的组件(component,或者称之为元件),是较widget更为正确的术语,它们就是会放在GUI ...

  10. webpack学习总结

    前言 在还未接触webpack,就有几个疑问: 1. webpack本质上是什么? 2. 跟异步模块加载有关系吗? 3. 可否生成多个文件,一定是一个? 4. 被引用的文件有其他异步加载模块怎么办? ...