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

栏目: C++ · 发布时间: 6年前

内容简介:原文地址:node中采用了两个核心模块来管理模块依赖:可以认为

原文地址: medium.freecodecamp.org/requiring-m…

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

node中采用了两个核心模块来管理模块依赖:

  • require 模块:全局可见,不需要额外使用 require('require')
  • module 模块:全局可见,不需要额外使用 require('module')

可以认为 require 模块是一个command, module 模块是所需模块的organizer。 在Node中引用模块并不是一件复杂的事情: const config = require('/path/to/file'); require 模块暴露出一个函数(就像上面看到的那样)。当 require() 函数传入一个path参数的时候,node会依次执行如下步骤:

  • Resolving : 找到path的绝对路径。
  • Loading : 确定文件的内容。
  • Wrapping :构造私有的作用域。Wrapping可以确保每次require文件的时候,require和exports都是私有的。
  • Evaluating :evaluating环节是VM处理已加载文件的最后一个环节。
  • Caching :为了避免引用相同的文件情况下,不重复执行上面的步骤。

本文中笔者将通过案例讲解上面提到的不同阶段以及这些阶段对于开发者开发node模块的影响。 首先通过终端创建一个文件夹 mkdir ~/learn-node && cd ~/learn-node 下面本文所有的命令都是在 ~/learn-node 中执行。

Resolving a local path

首先来介绍下 module 对象。读者可以通过REPL来看到 module 对象

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

每个module对象都有id属性用来区分不同的模块。id属性一般都是模块对应的绝对路径,在REPL中会简单的设置为 <repl> 。Node模块和系统磁盘上的文件是一一对应的。引用模块实际上会将文件中的内容加载到内存中。node支持通过多种方式来引用文件(比如说通过相对路径或者预配置路径),在把文件中内容加载到内存之前,需要先找到文件的绝对路径。 当不指定路径直接引用 find-me 模块的时候: require('find-me'); node会依次遍历 module.paths 指定的路径来寻找 find-me 模块:

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

上面的路径是从当前路径到根目录所有目录下的 node_modules 文件夹的路径,除此之外也包括一些遗留的但是已经不推荐使用的路径。如果node在上述路径中都找不到 find-me.js 就会抛出一个“cannot find module error.”的异常。

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

如果在当前文件夹下创建一个node_modules文件夹,并创建一个find-me.js文件,这时 require('find-me') 就能够找到find-me了。

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

如果其他路径下也存在find-me.js 比如在用户的home目录下的node_modules文件夹下面存在另外一个find-me.js:

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

当在learn-code目录下执行 require('find-me') ,由于在learn-code下的node_modules目录下有一个find-me.js,此时用户的home目录下的find-me.js并不会加载执行。

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

如果我们从~/learn-code目录下删除node_modules文件夹,再执行引用find-me,则会使用用户home目录下的node_modules下的fine-me:

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

Requiring a folder

模块不一定只是一个文件,读者也可以创建一个find-me文件夹,并且在文件夹中创建index.js, require('find-me') 的时候会引用index.js:

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

注意此时由于当前目录下有了find-me, 则此时引用find-me会忽略用户home目录下的node_modules。当引用目录的时候,默认情况下会寻找index.js,但是我们可以通过package.json中的main属性来指示用那个文件。举个例子,为了让 require('find-me') 能够解析到find-me文件夹下的其他文件,我们需要在find-me目录下加一个package.json,并指定应该解析到哪个文件:

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

require.resolve

如果只想解析模块但不执行模块,可以使用 require.resolve 函数。 resolverequire 函数的表现除了不执行文件之外,其他方面表现是一致的。当文件找不到的时候仍然会抛出一个异常,在找到文件的情况下会返回文件的绝对路径。

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

resolve 函数可以用来检测是否安装了某个模块,并在检查到模块的情况下使用已安装的模块。

Relative and absolute paths

除了从node_modules中解析出模块,我们也可以把模块放在任何地方,通过相对路径( ./ 或者 ../ 打头)或者绝对路径( / 打头)的方式来引用该模块。 比如,如果find-me.js在lib目录下而不是在node_modules目录下,我们可以通过这种方式来引用find-me: require('./lib/find-me');

Parent-child relation between files

创建一个 lib/util.js 并加入一行console.log来做区分,同时输出 module 对象:

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

在index.js也加入类似的代码,后面我们通过node执行index.js。在index.js中引用 lib/util.js :

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

在node中执行index.js:

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

注意index模块 (id: '.')lib/util 模块的父模块。但是输出结果中 lib/util 模块并没有显示在 index 模块的子模块中。取而代之的是一个 [Circular] 的值,因为这儿是一个循环引用。此时如果node打印 lib/utilindex 的子模块的话,则会进入到死循环。这也可以解释了为什么需要简单的用 [Circular] 来代替 lib/util 。 那么如果在 lib/util 模块中引用 index 模块会发生什么。这就是node中所允许的的循环引用。

为了能够更好的理解循环依赖,首先需要了解一些关于module对象上的一些概念。

exports, module.exports, and synchronous loading of modules

任何模块中exports都是一个特别的对象。注意到上面的结果中,每次打印module对象,都会有一个为空对象的exports属性。我们可以在这个特别的exports对象上加入一些属性。比如为 index.jslib/index.js 暴露id属性。

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

现在再执行index.js, 就能看到每个文件的module对象上新增的属性:

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

这里为了简洁,笔者删除了输出结果中的一些属性,但是可以看到 exports 对象现在就有了我们之前定义的属性。你可以在exports对象上增加任意数量的属性,也可以把整个exports对象替换成其他东西。比如说想要把exports对象换成一个函数可以如下:

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

再执行index.js就可以看到exports对象变成了一个函数:

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

这里把exports对象替换成函数并不是通过 exports = function(){} 来完成的。实际上我们也不能这么做,因为模块中的exports对象只是 module.exports 的引用,而 module.exports 才是负责暴露出来的属性。当我们给exports对象重新赋值的时候,会断开对 module.exports 的引用,这种情况下只是引入了一个新的变量而不是修改 module.exports 属性。

当我们引入某个模块,require函数返回的实际上是 module.exports 对象。举个例子,把index.js中 require('./lib/util') 修改为:

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

上面的代码把 lib/util 中暴露出来的属性赋值给UTIL常量。当我们执行 index.js 时,最后一行会返回如下结果: UTIL: { id: 'lib/util' }

下面来谈谈每个module对象上的loaded属性。到目前为止,每次我们打印module对象的时候, loaded 属性都是为 false 。module对象通过 loaded 属性来记录哪些模块已经加载(loaded为true),哪些模块还未加载(loaded为false)。可以通过 setImmediate 方法来再下一个event loop中看到模块已经加载完成的信息。

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

输出结果如下:

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

再延迟的 console.log 中我们可以看到 lib/util.jsindex.js 已经被完全加载。 当node加载模块完成后,exports对象也会变成已完成状态。 requiring和loading 的过程是同步的。这也是为什么我们能够在一个循环之后能够看到模块加载完成信息的原因。

同时这也意味着我们不能异步的修改exports对象。比如我们不能像下面这么做:

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

Circular module dependency

下面来回答前面提到的循环依赖的问题:如果模块1依赖模块2,同时模块2又依赖模块1,这时候会发生什么呢? 为了找到答案,我们在 lib 目录下创建两个文件, module1.jsmodule2.js ,并让他们互相引用:

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

当执行 module1.js 的时候,会看到如下结果:

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

我们在 module1 还没有完全加载成功的情况下引用 module2 ,由于 module2 中在 module1 还没有完全加载成功的情况就引用 module1 ,此时在 module2 中能够得到的 exports 对象是循环依赖之前的全部属性(也就是 require('module2') 之前)。此时只能访问到 a 属性,因为 bc 属性在 require('module2') 之后。

node在循环依赖这块的处理十分简单。你可以引用哪些还没有完全加载的模块,但是只能拿到一部分属性。

JSON and C/C++ addons

通过 require 函数我们可以原生的加载 JSONC++ 扩展。使用的时候甚至不需要指定扩展名。在文件扩展名没有指定的情况下,node首先会尝试加载 .js 的文件。如果 .js 的文件没有找到,则会尝试加载 .json 文件,如果找到 .json 文件则会解析 .json 文件。如果 .json 文件也没有找到,则会尝试加载 .node 文件。但是为了避免语义模糊,开发者应该在非 .js 的情况下指定文件的扩展名。

加载 .json 文件对于管理静态配置、或者周期性的从外部文件中读取配置的场景是十分有用的。比如我们有如下json文件:

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

我们可以直接使用它:

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

运行上面的代码会输出: Server will run at http://localhost:8080 如果node找不到 .js.json 的情况下,会寻找 .node 文件,并采用解析node扩展的方式来解析 .node 文件。

Node 官方文档中有一个c++写的扩展案例。该案例暴露了一个 hello() 函数,执行 hello() 函数会输出 world 。你可以使用 node-gyp.cc 文件编译、构建成 .node 文件。开发者需要配置 binding.gyp 来告诉 node-gyp 该做什么。在构建 addon.node 成功后,就可以像引用其他模块一样使用:

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

require.extensions 可以看到目前只支持三种类型的扩展:

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

可以看到每种类型都有不同的加载函数。对于 .js 文件使用 module._compile 方法,对于 .json 文件使用 JSON.parse 方法,对于 .node 文件使用 process.dlopen 方法。

All code you write in Node will be wrapped in functions

node中对模块的包裹常常被误解,在理解node对模块的包裹之前,先来回顾下 exports/module.exports 的关系。 我们可以用 exports 来暴露属性,但是不能直接替换 exports 对象,因为 exports 对象只是对 module.exporst 的引用。

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

准确的来说, exports 对象对于每个模块来说是全局的,定义为 module 对象上属性的引用。 在解释node包装过程前,我们再来问一个问题。 在浏览器中,当我们在全局环境中申明一个变量: var answer = 42; 在定义 answer 变量之后的脚本中, answer 变量就属于全局变量。 在node中并不是这样的。当我们在一个模块中定义了一个变量,另外的模块并不能直接访问该模块中的变量,那么在node中变量是如何被局部化的呢?

答案很简单。在编译模块之前,node把模块代码包装在一个函数中,我们可以通过 module 对象上的 wrapper 属性来看到这个函数:

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

node并不会直接执行你写在文件中的代码。而是执行包裹函数的代码,包裹函数会把你写的代码包装在函数体中。这就保证了任何模块中的顶级变量对于别的模块来说是局部的。

wrapper 函数有5个参数: exports , require , module , __filename__dirname 。这也是为什么对于每个模块来说,这些变量都像是全局的原因,实际上对每个模块来说,这些变量都是独立的。

当node执行包装函数的时候,这些变量都已经被正确赋值。 exports 被定义为 module.exports 的引用, requiremodule 都指向待执行的函数, __filename__dirname 表示了被包裹模块的文件名和目录的路径。

如果你运行了一个出错的模块,立马就能看到包裹函数。

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

可以看到报错的是wrapper函数的第一行。除此之外,由于每个模块都被函数包裹了一遍,我们可以通过 arguments 来访问wrapper函数所有的参数。

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

第一个参数是 exports 对象,一开始是一个空对象,接着是 require/module 对象,这两个对象不是全局变量,都是与 index.js 相关的实例化对象。最后两个参数表示文件路径和文件夹路径。

包裹函数的返回值是 module.exporst 。在包裹函数内部,我们可以通过 exports 对象来修改 module.exports 的属性,但是不能对 exports 重新赋值,因为 exports 只是一个引用。

上面描述的等价于下面的代码:

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

如果我们修改了 exports 对象,则 exports 对象不再是 module.exports 的引用。这种引用的方式不仅在这里可以正常工作,在javascript中都是可以正常工作的。

The require object

require 对象并没有什么特殊的。 require 是一个函数对象,接受模块名或者路径名,并返回 module.exports 对象。如果我们想的话,可以随意的覆盖 require 对象。 比如为了测试,我们希望可以mock require 函数的默认行为,返回一个模拟的对象,而不是引用模块返回 module.exports 对象。对 require 进行赋值可以实现这一目的:

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

在对 require 进行重新赋值之后,每次调用 require('something') 都会返回mock对象。 require 对象也有自身的属性。前面我们已经看到过了用于解析模块路径的 resolve 属性以及 require.extensions 属性。 除此之外,还有 require.main 属性用来区别当前模块是被引用还是直接运行的。比如说我们在 print-in-frame.js 文件中有一个 printInFrame 函数:

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

这个函数接受一个数值类型的参数 numberic 和一个字符串类型的参数 header ,函数中首先根据 size 参数打印指定个数 * 的frame,并在frame中打印 header 。 我们可以有两种方式来使用这个函数:

  1. 命令行直接调用: ~/learn-node $ node print-in-frame 8 Hello ,命令行中给函数传入8和Hello,打印一个8个 * 组成的frame,并在frame中输出 hello
  2. require 方式调用:假设 print-in-frame.js 暴露出一个 printInFrame 函数,我们可以这样调用:
    开发者需要了解的nodejs中require的机制

这样会在5个*组成的frame 中打印 Hey 。 我们需要某种方式来区分当前模块是命令行单独调用还是被其他模块引用的。这种情况,我们可以通过 require.main 来做判断:

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

这样我们可以通过这个条件表达式来实现上述应用场景:

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

如果当前模块没有以模块的方式被其他模块引用,我们可以根据命令行参数 process.argv 来调用 printInFrame 函数。否则,我们设置 module.exports 参数为 printInFrame 函数。

All modules will be cached

理解模块缓存是十分重要的。我们来通过一个简单的例子来讲解下,比如说我们有一个如下的字符画的js文件:

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

我们希望每次 require 文件的时候都能显示字符画。比如我们引用两次字符画的js,希望可以输出两次字符画:

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

第二次引用并不会输出字符画,因为此时模块已经被缓存了。在第一次引用后,我们可以通过 require.cache 来查看模块缓存情况。 cache 对象是一个简单的键值对,每次引用的模块都会被缓存在这个对象上。 cache 上的值就是每个模块对应的 module 对象。我们可以从 require.cache 上移除 module 对象来让缓存失效。如果我们从缓存中缓存中移除 module 对象,重新require的时候,node依然会重新加载该模块,并重新缓存该模块。 但是,对于这种情况,上面的修改缓存的方式并不是最好的方法。最简单的方法是把 ascii-art.js 包装在函数中然后暴露出去,这样的话,当我们引用 ascii-art.js 的时候,会得到一个函数,每次执行的时候都会输出字符画。

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

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Development Recipes

Web Development Recipes

Brian P. Hogan、Chris Warren、Mike Weber、Chris Johnson、Aaron Godin / Pragmatic Bookshelf / 2012-1-22 / USD 35.00

You'll see a full spectrum of cutting-edge web development techniques, from UI and eye candy recipes to solutions for data analysis, testing, and web hosting. Make buttons and content stand out with s......一起来看看 《Web Development Recipes》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

html转js在线工具
html转js在线工具

html转js在线工具