摘要:环境变量法通过上一节的源码分析,我们知道了的作用,那么如何使用或者优雅的使用来解决依赖加载问题呢尝试一最为直接的是,修改系统的环境变量。
模块加载痛点
大家也或多或少的了解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函数返回的结果,遍历目录数组,加载依赖。如果遍历结束后仍没有找到依赖,则抛错。
在分析完源码后,相信大家也都注意了几点信息:
Module._initPaths函数内部检查了NODE_PATH环境变量
Module._initPaths函数只执行一次
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”,但思想也大同小异。在这里和大家一起分享学习收获,希望对各位有些启发和感悟,不胜感激!
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/80932.html
摘要:例如指定一些依赖到模块中实现规范的模块化,感兴趣的可以查看的文档。 CommonJS 定义了 module、exports 和 require 模块规范,Node.js 为了实现这个简单的标准,从底层 C/C++ 内建模块到 JavaScript 核心模块,从路径分析、文件定位到编译执行,经历了一系列复杂的过程。简单的了解 Node 模块的原理,有利于我们重新认识基于 Node 搭建的...
摘要:可以通过配置属性进行修改,默认将会自动创建个库文件仅含有依然会创建个库文件仅含有假设所有的体积都大于将会创建一个库文件和一个通用组件文件仅含有当这些体积小于是,会故意将该模块复制到三个文件中。 该文章内容大致翻译自 webpack 4: Code Splitting, chunk graph and the splitChunks optimization 原有的问题 webpack...
摘要:我们可以为元素添加属性然后在回调函数中接受该元素在树中的句柄,该值会作为回调函数的第一个参数返回。使用最常见的用法就是传入一个对象。单向数据流,比较有序,有便于管理,它随着视图库的开发而被概念化。 面试中问框架,经常会问到一些原理性的东西,明明一直在用,也知道怎么用, 但面试时却答不上来,也是挺尴尬的,就干脆把react相关的问题查了下资料,再按自己的理解整理了下这些答案。 reac...
摘要:近期在阅读最新几版的官方文档过程中发现不少术语不清之处特发此文总结以下的术语大量在官方文档中直接出现且直接如基本词语一样使用不理解它们会严重影响阅读自适应自旋锁自适应自旋锁是一个允许线程在特定点自旋等待特定事件发生而不是直接进行并等待该事件 近期在阅读JAVA最新几版的官方文档过程中发现不少术语不清之处,特发此文总结.以下的术语大量在官方文档中直接出现,且直接如基本词语一样使用,不理解...
摘要:为了防止某些文档或脚本加载别的域下的未知内容,防止造成泄露隐私,破坏系统等行为发生。模式构建函数响应式前端架构过程中学到的经验模式的不同之处在于,它主要专注于恰当地实现应用程序状态突变。严重情况下,会造成恶意的流量劫持等问题。 今天是编辑周刊的日子。所以文章很多和周刊一样。微信不能发链接,点了也木有用,所以请记得阅读原文~ 发个动图娱乐下: 使用 SVG 动画制作游戏 使用 GASP ...
阅读 3190·2021-11-19 09:40
阅读 2989·2021-09-09 09:32
阅读 770·2021-09-02 09:55
阅读 1373·2019-08-26 13:23
阅读 2383·2019-08-26 11:46
阅读 1214·2019-08-26 10:19
阅读 2035·2019-08-23 16:53
阅读 1050·2019-08-23 12:44