文档章节

node模块加载层级优化

oschina_00
 oschina_00
发布于 2017/04/25 17:03
字数 2399
阅读 4
收藏 0
点赞 0
评论 0

模块加载痛点

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

 

作者:伯乐在线专栏作者 - 欲休

本文转载自:伯乐在线专栏作者 - 欲休

共有 人打赏支持
oschina_00
粉丝 5
博文 101
码字总数 0
作品 0
廊坊
Webpack原理-输出文件分析

虽然在前面的章节中你学会了如何使用 Webpack ,也大致知道其工作原理,可是你想过 Webpack 输出的 是什么样子的吗? 为什么原来一个个的模块文件被合并成了一个单独的文件?为什么 能直接运...

⋅ 01/03 ⋅ 0

再见,CommonsChunkPlugin!

编者按:本文由为之漫笔翻译在众成翻译,已授权奇舞周刊转载。 webpack 4用两个新的配置选项(optimization.splitChunks and optimization.runtimeChunk)替代了CommonsChunkPlugin。本文介绍这...

奇舞周刊 ⋅ 04/15 ⋅ 0

SeaJS 2.0 Beta 发布,JS 模块加载框架

如果你是第一次接触 SeaJS,可以直接访问官方文档:seajs.org 按照顺序依次尝试和阅读就好。 如果你是老用户,请随着下文一起来看看这 3 个多月来,SeaJS 都发生了哪些变化。 更小 看下图,左...

oschina ⋅ 2013/02/18 ⋅ 13

GoodERP 18.22 发布,开源企业管理软件

GoodERP 18.22 已发布。 2018/05/21 ~ 2018/05/27 core 模块 [添加] 供应商上增加资质到期日期和天数。 home_page 模块 [优化] 首页显示优化:关键指标账户余额、应收余额等去掉圆圈显示,主...

GoodERP ⋅ 05/29 ⋅ 0

【Cocos2d-x】开发基础-Node与Node层级架构

本篇博客讲解: 1.Node与Node层级架构 2.Node中重要的操作 3.Node中重要的属性 4.游戏循环与调度 Node与Node层级架构 首先来看一张图 这个图反应了Node与Node的层级架构 所谓层级架构其实就是...

qq_26525215 ⋅ 2017/06/30 ⋅ 0

Android性能优化:布局优化 详细解析(含、、讲解 )

前言 在 开发中,性能优化策略十分重要 本文主要讲解性能优化中的布局优化,希望你们会喜欢。 目录 1. 影响的性能 布局性能的好坏 主要影响 :应用中的页面显示速度 2. 如何影响性能 布局影响...

carson_ho ⋅ 03/20 ⋅ 0

面向工程的移动Web前端模版--Qing

什么是Qing? Qing是一套基础开发模版,来源于我们在手机与PC端上的大量工程实践。Qing所提供不是冷冰冰的文件, 而是一套Web前端解决方案,所以Qing不只是关注项目的初始状态,而是整体的工...

叶秀兰 ⋅ 2014/09/17 ⋅ 0

TypeScript手册翻译系列4-模块

模块 在TypeScript中利用模块(module)来组织代码。这里将讨论内部和外部模块,以及在何时使用哪种方式更合适,以及怎么使用。当然也会讨论一些高级话题,例如如何使用外部模块,以及在Typ...

一配 ⋅ 2015/08/23 ⋅ 1

4.5 查看模块

在实际项目中,我们可能需要查询全局/本地都安装了哪些模块,来做下一步的操作。本节介绍如何查询已经安装的模块。 1. 查看全局模块 与安装模块一样,-g 参数在 npm 中就代表着全局的含义。 ...

嘘别吵_4d1c ⋅ 2017/12/09 ⋅ 0

RequireJS实战-优化oschina前端JS(完)

本人文笔很烂请见谅,欢迎吐槽和各种拍砖!分享快乐! 关于RequireJS入门与好处,这里就直接略过了。(网上有很多), 我们就以OSchina.net为例,看看应用RequireJS是如何模块与优化JS的。 ...

lee5hx ⋅ 2014/03/29 ⋅ 10

没有更多内容

加载失败,请刷新页面

加载更多

下一页

容器之重命名镜像

使用docker tag命令来重命名镜像名称,先执行help,查看如何使用如下 mjduan@mjduandeMacBook-Pro:~/Docker % docker tag --helpUsage:docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TA...

汉斯-冯-拉特 ⋅ 10分钟前 ⋅ 0

with 的高级用法

那么 上下文管理器 又是什么呢? 上下文管理器协议包含 __enter__ 和 __exit__ 两个方法。with 语句开始运行时,会在上下文管理器对象上调用 __enter__ 方法。with 语句运行结束后,会在上下...

阿豪boy ⋅ 29分钟前 ⋅ 0

使用 jsoup 模拟登录 urp 教务系统

需要的 jsoup 相关 jar包:https://www.lanzous.com/i1abckj 1、首先打开教务系统的登录页面,F12 开启浏览器调试,注意一下 Request Headers 一栏的 Cookie 选项,我们一会需要拿这个 Cook...

大灰狼时间 ⋅ 29分钟前 ⋅ 0

关于线程的创建

转自自己的笔记: http://note.youdao.com/noteshare?id=87584d4874acdeaf4aa027bdc9cb7324&sub=B49E8956E145476191C3FD1E4AB40DFA 1.创建线程的方法 Java使用Thread类代表线程,所有的线程对......

MarinJ_Shao ⋅ 41分钟前 ⋅ 0

工厂模式学习

1. 参考资料 工厂模式-伯乐在线 三种工厂-思否 深入理解工厂模式 2. 知识点理解 2.1 java三种工厂 简单工厂 工厂模式 抽象工厂 2.2 异同点 逐级复杂 简单工厂通过构造时传入的标识来生产产品...

liuyan_lc ⋅ 53分钟前 ⋅ 0

Java NIO

1.目录 Java IO的历史 Java NIO之Channel Java NIO之Buffer Java NIO之Selector Java NIO之文件处理 Java NIO之Charset Java 可扩展IO 2.简介 “IO的历史”讲述了Java IO API从开始到现在的发...

士别三日 ⋅ 57分钟前 ⋅ 0

[Err] ORA-24344: success with compilation error

从txt文本复制出创建function的脚本,直接执行,然后报错:[Err] ORA-24344: success with compilation error。 突然发现脚本的关键字,居然不是高亮显示。 然后我把脚本前面的空格去掉,执行...

wenzhizhon ⋅ 今天 ⋅ 0

Spring Security授权过程

前言 本文是接上一章Spring Security认证过程进一步分析Spring Security用户名密码登录授权是如何实现得; 类图 调试过程 使用debug方式启动https://github.com/longfeizheng/logback该项目,...

hutaishi ⋅ 今天 ⋅ 0

HAProxy基于KeepAlived实现Web高可用及动静分离

前言 软件负载均衡一般通过两种方式来实现: 基于操作系统的软负载实现 基于第三方应用的软负载实现 LVS是基于Linux操作系统实现的一种软负载,而HAProxy则是基于第三方应用实现的软负载。 ...

寰宇01 ⋅ 今天 ⋅ 0

微软自研处理器的小动作:已经开始移植其他平台的工具链

微软将 Windows 10 、Linux 以及工具链如 C/C++ 和 .NET Core 运行时库、Visual C++ 2017 命令行工具、RyuJIT 编辑器等移植到其自主研发的处理器架构 E2。微软还移植了广泛使用的 LLVM C/C++...

linux-tao ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部