当前位置: 首页 > 后端技术 > Node.js

开发者需要了解nodejs中require的机制

时间:2023-04-03 18:35:06 Node.js

原文地址:https://medium.freecodecamp.o...node中使用了两个核心模块来管理模块依赖:require模块:全局可见,无需额外使用require('require')modulemodule:全局可见,不需要使用require('module'),可以认为requiremodule是一个命令,modulemodule是所需模块的组织者。在Node中引用模块并不是一件复杂的事情:constconfig=require('/path/to/file');要求模块公开一个函数(如上所示)。当require()函数传入一个path参数时,node会依次执行以下步骤:解析:找到path的绝对路径。加载:确定文件的内容。包装:构造私有范围。包装可以确保每次需要文件时require和exports都是私有的。评估:评估链接是虚拟机处理加载文件的最后一个链接。缓存:为了避免引用同一个文件,不要重复上面的步骤。在本文中,笔者将通过案例来说明上述不同的阶段,以及这些阶段对开发者开发节点模块的影响。首先,通过终端创建文件夹mkdir~/learn-node&&cd~/learn-node。下面这篇文章中的所有命令都在~/learn-node中执行。解析本地路径首先引入模块对象。读者可以通过REPL看到模块对象。每个模块对象都有一个id属性来区分不同的模块。id属性一般是模块对应的绝对路径,在REPL中会简单的设置为。Node模块和系统盘上的文件是一一对应的。引用模块实际上会将文件的内容加载到内存中。Node支持多种引用文件的方式(例如,通过相对路径或预配置路径)。在将文件内容加载到内存之前,需要找到文件的绝对路径。不指定路径直接引用find-me模块时:require('find-me');node为了找到find-me模块会遍历module.paths指定的路径:上面的路径是从当前路径到根目录。目录下node_modules文件夹的路径,以及一些遗留但已弃用的路径。如果node在上述路径中找不到find-me.js,它会抛出“找不到模块错误”。例外。如果在当前文件夹下创建一个node_modules文件夹,并创建一个find-me.js文件,那么require('find-me')就可以找到find-me了。如果其他路径下也有find-me.js,比如在用户家目录下的node_modules文件夹下还有一个find-me.js:在learn-code目录下执行require('find-me')时,因为在learn-code下的node_modules目录下有一个find-me.js。此时用户家目录下的find-me.js不会被加载执行。如果我们删除~/learn-code目录下的node_modules文件夹,然后执行referencefind-me,就会用到用户家目录下node_modules下的fine-me:需要一个文件夹module不一定只是一个文件,读者也可以创建一个find-me文件夹,并在文件夹中创建index.js,require('find-me')时会引用index.js:注意,既然当前目录下有find-me,那么在这次引用find-me会忽略用户主目录中的node_modules。当引用目录时,默认会查找index.js,但我们可以通过package.json中的main属性指定使用哪个文件。例如,为了让require('find-me')解析到find-me文件夹中的其他文件,我们需要在find-me目录中添加一个package.json并指定要解析的文件:require。resolve如果你只想解析一个模块而不执行它,你可以使用require.resolve函数。resolve和require函数除了不执行文件外,性能是一致的。找不到文件时仍然会抛出异常,找到文件则返回文件的绝对路径。resolve函数可以用来检测某个模块是否安装,如果是则使用安装的模块。相对路径和绝对路径除了从node_modules中解析模块外,我们还可以将模块放在任意位置,通过相对路径(./或../)或绝对路径(/)来引用模块。比如find-me.js在lib目录下,而不是在node_modules目录下,我们可以这样引用find-me:require('./lib/find-me');文件之间的父子关系创建一个lib/util.js并添加一行console.log来区分,同时输出module对象:在index.js中添加类似的代码,然后我们通过node执行index.js。index.js中引用lib/util.js:在node中执行index.js:注意index模块(id:'.')是lib/util模块的父模块。但是lib/util模块没有显示在输出中索引模块的子模块中。相反,使用[Circular]的值,因为这是一个循环引用。此时如果node打印出lib/util为index的子模块,就会进入死循环。这也可以解释为什么需要简单地将lib/util替换为[Circular]。那么如果在lib/util模块中引用index模块会发生什么。这就是节点中允许的循环引用。为了更好地理解循环依赖,首先需要了解一些关于模块对象的概念。exports、module.exports和同步加载模块exports在任何模块中都是一个特殊的对象。注意在上面的结果中,每次打印模块对象时,都会有一个空对象的exports属性。我们可以向这个特定的导出对象添加一些属性。比如为index.js和lib/index.js暴露id属性。现在再次执行index.js,可以看到每个文件的module对象上新增的属性:这里为了简洁,笔者在输出结果中删除了一些属性,但是可以看到现在的exports对象具有我们之前定义的属性。您可以向exports对象添加任意数量的属性,并且可以用其他东西替换整个exports对象。比如你想把exports对象替换成一个函数,可以这样执行:执行index.js可以看到exports对象变成了一个函数:这里,把exports对象替换成一个函数并没有完成通过exports=function(){}的。其实我们不能这样做,因为模块中的exports对象只是对module.exports的引用,而module.exports负责暴露的属性。当我们重新分配exports对象时,对module.exports的引用将被破坏。在这种情况下,只引入了一个新变量,而不是修改module.exports属性。当我们导入一个模块时,require函数实际上返回了module.exports对象。例如修改index.js中的require('./lib/util')为:上述代码将lib/util中暴露的属性赋值给UTIL常量。当我们执行index.js时,最后一行会返回如下结果:UTIL:{id:'lib/util'}下面说说每个模块对象上的loaded属性。到目前为止,每次我们打印模块对象时,加载的属性都是假的。模块对象使用loaded属性记录哪些模块已经加载(loaded为true),哪些模块还没有加载(loaded为false)。您可以使用setImmediate方法在下一个事件循环中查看模块已加载的信息。输出如下:在延迟的console.log中我们可以看到lib/util.js和index.js已经完全加载。当节点加载模块完成时,导出对象也将完成。请求和加载的过程是同步的。这就是为什么我们可以在一个循环后看到模块加载完成消息的原因。这也意味着我们不能异步修改导出对象。比如我们不能这样:循环模块依赖我们来回答上面提到的循环依赖问题:如果模块1依赖模块2,模块2依赖模块1,这时候会发生什么?为了找到答案,我们在lib目录下创建两个文件,module1.js和module2.js,让它们相互引用:执行module1.js时,会看到如下结果:我们还没有完全加载成功在module1中引用module2的情况下,因为module1是在module1还没有完全加载成功的时候在module1中被引用的,所以此时在module2中能够获取到的exports对象是循环依赖之前的所有属性(也就是before要求('module2'))。此时只能访问a属性,因为b和c属性在require('module2')之后。Node对循环依赖的处理非常简单。可以引用未完全加载的模块,但只能获取部分属性。JSON和C/C++插件我们可以通过require函数原生加载JSON和C++扩展。您甚至不需要在使用时指定扩展名。在没有指定文件扩展名的情况下,node会先尝试加载.js文件。如果找不到.js文件,它将尝试加载.json文件,如果找到.json文件,它将解析.json文件。如果也找不到.json文件,它将尝试加载.node文件。但是为了避免语义歧义,开发者应该在文件的扩展名不是.js时指定。加载.json文件对于管理静态配置或定期从外部文件读取配置很有用。比如我们有如下的json文件:我们可以直接使用:运行上面的代码会输出:Serverwillrunathttp://localhost:8080如果node找不到.js和.json,就会去寻找.nodefiles,使用解析node扩展名的方法解析.node文件。Node官方文档中有一个用c++写的扩展案例。本例暴露了一个hello()函数,执行hello()函数会输出world。您可以使用node-gyp将.cc文件编译并构建为.node文件。开发人员需要配置binding.gyp来告诉node-gyp要做什么。成功构建addon.node后,就可以像引用其他模块一样使用了:从require.extensions可以看出目前只支持三种扩展:可以看到每种都有不同的加载功能。.js文件使用module._compile方法,.json文件使用JSON.parse方法,.node文件使用process.dlopen方法。您在Node中编写的所有代码都将包装在函数中节点中模块的包装经常被误解。在了解按节点包装模块之前,我们先回顾一下exports/module.exports之间的关系。我们可以使用exports来暴露属性,但是我们不能直接替换exports对象,因为exports对象只是对module.exorst的引用。准确地说,导出对象对每个模块都是全局的,定义为对模块对象上属性的引用。在讲解节点打包过程之前,我们再问一个问题。在浏览器中,当我们在全局环境中声明一个变量时:varanswer=42;在定义answer变量后的脚本中,answer变量是一个全局变量。在节点中不是这种情况。当我们在一个模块中定义了一个变量,其他模块无法直接访问该模块中的变量,那么这个变量在node中是如何本地化的呢?答案很简单。在编译模块之前,node将模块代码包装在一个函数中。我们可以通过模块对象上的wrapper属性看到这个功能:node不会直接执行你在文件中写的代码。而是执行被包装函数的代码,被包装函数将你写在函数体中的代码包装起来。这确保了任何模块中的顶级变量对于其他模块都是本地的。包装函数有5个参数:exports、require、module、__filename和__dirname。这就是为什么对于每个模块来说,这些变量看似是全局的,但实际上对于每个模块来说都是独立的。当节点执行包装函数时,这些变量已经被正确赋值。exports定义为对module.exports的引用,require和module都指向要执行的函数,__filename和__dirname代表被包装模块的文件名和目录路径。如果运行失败的模块,您可以立即看到包装的函数。可以看到错误是包装函数的第一行。此外,由于每个模块都被一个函数包装,我们可以通过参数访问包装函数的所有参数。第一个参数是exports对象,开头是一个空对象,后面是require/module对象。这两个对象不是全局变量,而是与index.js相关的实例化对象。最后两个参数代表文件路径和文件夹路径。包装函数的返回值是module.exorst。在包装函数内部,我们可以通过exports对象修改module.exports的属性,但是我们不能重新赋值exports,因为exports只是一个引用。上面的描述相当于下面的代码:如果我们修改exports对象,exports对象就不再是对module.exports的引用了。这种引用方式不仅在这里有效,在javascript中也有效。require对象require对象没有什么特别之处。require是一个函数对象,它接受一个模块名或路径名并返回一个module.exports对象。如果需要,我们可以自由覆盖require对象。例如,为了测试,我们希望模拟require函数的默认行为并返回一个模拟对象,而不是从引用的模块返回module.exports对象。对require的赋值实现了这一点:在重新分配给require之后,每次调用require('something')都会返回模拟对象。require对象也有自己的属性。之前我们已经看到用于解析模块路径的resolve属性和require.extensions属性。另外还有一个require.main属性来区分当前模块是被引用还是直接运行。比如我们在print-in-frame.js文件中有一个printInFrame函数:这个函数接受一个数值类型的参数numberic和一个字符串类型的参数header,该函数首先根据大小参数,并在框架中打印标题。我们可以通过两种方式使用这个函数:命令行直接调用:~/learn-node$nodeprint-in-frame8Hello,将8和Hello传递给命令行上的函数,打印一个由8组成的帧*,并在框架中输出hello。require方式调用:假设print-in-frame.js暴露了一个printInFrame函数,我们可以这样调用:这会在一个由5*组成的frame中打印Hey。我们需要一些方法来区分当前模块是通过命令行自己调用的还是被其他模块引用的。这种情况下,我们可以使用require.main来判断:所以我们可以使用这个条件表达式来实现上面的应用场景:如果当前模块没有被其他模块以模块的形式引用,我们可以使用命令行参数process.argv调用printInFrame函数。否则,我们将module.exports参数设置为printInFrame函数。所有模块都将被缓存了解模块缓存非常重要。让我们通过一个简单的例子来解释它。比如我们有一个人物画的js文件如下:我们希望每次需要这个文件的时候都能显示人物画。比如我们两次引用人物画的js,我们希望输出两次人物画:第二次引用不会输出人物画,因为此时模块已经缓存了。第一次引用后,我们可以通过require.cache查看模块缓存。缓存对象是一个简单的键值对,每个引用的模块都会缓存在这个对象上。缓存上的值是每个模块对应的模块对象。我们可以通过从require.cache中删除模块对象来使缓存无效。如果我们从缓存中移除模块对象并重新请求,node仍然会重新加载模块并重新缓存模块。但是,对于这种情况,上述修改缓存的方式并不是最好的方式。最简单的方法是将ascii-art.js包装在一个函数中并公开它。这样,我们在引用ascii-art.js的时候,就会得到一个每次执行都会输出字符画的函数。