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

原文地址: https://medium.freecodecamp.o...

image

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

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

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

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

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

Resolving a local path

首先来介绍下module对象。读者能够经过REPL来看到module对象
imagenode

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

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

若是在当前文件夹下建立一个node_modules文件夹,并建立一个find-me.js文件,这时require('find-me')就可以找到find-me了。
imageapi

若是其余路径下也存在find-me.js 好比在用户的home目录下的node_modules文件夹下面存在另一个find-me.js:
image浏览器

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

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

Requiring a folder

模块不必定只是一个文件,读者也能够建立一个find-me文件夹,而且在文件夹中建立index.js,require('find-me')的时候会引用index.js:
image

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

require.resolve

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

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 对象:
image

在index.js也加入相似的代码,后面咱们经过node执行index.js。在index.js中引用lib/util.js:
image

在node中执行index.js:
image

注意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属性。
image

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

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

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

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

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

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

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

输出结果以下:
image

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

同时这也意味着咱们不能异步的修改exports对象。好比咱们不能像下面这么作:
image

Circular module dependency

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

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

咱们在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文件:
image

咱们能够直接使用它:
image

运行上面的代码会输出: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成功后,就能够像引用其余模块同样使用:
image

require.extensions能够看到目前只支持三种类型的扩展:
image

能够看到每种类型都有不一样的加载函数。对于.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的引用。
image

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

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

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

wrapper函数有5个参数:exports,require,module,__filename__dirname。这也是为何对于每一个模块来讲,这些变量都像是全局的缘由,实际上对每一个模块来讲,这些变量都是独立的。

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

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

能够看到报错的是wrapper函数的第一行。除此以外,因为每一个模块都被函数包裹了一遍,咱们能够经过arguments来访问wrapper函数全部的参数。
image

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

包裹函数的返回值是module.exporst。在包裹函数内部,咱们能够经过exports对象来修改module.exports的属性,可是不能对exports从新赋值,由于exports只是一个引用。

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

若是咱们修改了exports 对象,则exports对象再也不是module.exports的引用。这种引用的方式不只在这里能够正常工做,在javascript中都是能够正常工做的。

The require object

require对象并无什么特殊的。require是一个函数对象,接受模块名或者路径名,并返回module.exports对象。若是咱们想的话,能够随意的覆盖require对象。
好比为了测试,咱们但愿能够mock require函数的默认行为,返回一个模拟的对象,而不是引用模块返回module.exports对象。对require进行赋值能够实现这一目的:
image

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

这个函数接受一个数值类型的参数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函数,咱们能够这样调用:

image

这样会在5个*组成的frame 中打印Hey
咱们须要某种方式来区分当前模块是命令行单独调用仍是被其余模块引用的。这种状况,咱们能够经过require.main 来作判断:
image

这样咱们能够经过这个条件表达式来实现上述应用场景:
image

若是当前模块没有以模块的方式被其余模块引用,咱们能够根据命令行参数process.argv来调用printInFrame 函数。不然,咱们设置module.exports参数为printInFrame 函数。

All modules will be cached

理解模块缓存是十分重要的。咱们来经过一个简单的例子来说解下,好比说咱们有一个以下的字符画的js文件:
image

咱们但愿每次require文件的时候都能显示字符画。好比咱们引用两次字符画的js,但愿能够输出两次字符画:
image

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

相关文章
相关标签/搜索