原文地址: https://medium.freecodecamp.o...
node中采用了两个核心模块来管理模块依赖:javascript
require
模块:全局可见,不须要额外使用require('require')
module
模块:全局可见,不须要额外使用require('module')
能够认为require
模块是一个command,module
模块是所需模块的organizer。
在Node中引用模块并非一件复杂的事情:const config = require('/path/to/file');
require
模块暴露出一个函数(就像上面看到的那样)。当require()
函数传入一个path参数的时候,node会依次执行以下步骤:html
本文中笔者将经过案例讲解上面提到的不一样阶段以及这些阶段对于开发者开发node模块的影响。
首先经过终端建立一个文件夹mkdir ~/learn-node && cd ~/learn-node
下面本文全部的命令都是在~/learn-node
中执行。java
首先来介绍下module
对象。读者能够经过REPL来看到module
对象node
每一个module对象都有id属性用来区分不一样的模块。id属性通常都是模块对应的绝对路径,在REPL中会简单的设置为<repl>
。Node模块和系统磁盘上的文件是一一对应的。引用模块实际上会将文件中的内容加载到内存中。node支持经过多种方式来引用文件(好比说经过相对路径或者预配置路径),在把文件中内容加载到内存以前,须要先找到文件的绝对路径。
当不指定路径直接引用find-me
模块的时候:require('find-me');
node会依次遍历module.paths
指定的路径来寻找find-me
模块:c++
上面的路径是从当前路径到根目录全部目录下的node_modules
文件夹的路径,除此以外也包括一些遗留的可是已经不推荐使用的路径。若是node在上述路径中都找不到find-me.js
就会抛出一个“cannot find module error.”的异常。json
若是在当前文件夹下建立一个node_modules文件夹,并建立一个find-me.js文件,这时require('find-me')
就可以找到find-me了。api
若是其余路径下也存在find-me.js 好比在用户的home目录下的node_modules文件夹下面存在另一个find-me.js:浏览器
当在learn-code目录下执行require('find-me')
,因为在learn-code下的node_modules目录下有一个find-me.js,此时用户的home目录下的find-me.js并不会加载执行。缓存
若是咱们从~/learn-code目录下删除node_modules文件夹,再执行引用find-me,则会使用用户home目录下的node_modules下的fine-me:app
模块不必定只是一个文件,读者也能够建立一个find-me文件夹,而且在文件夹中建立index.js,require('find-me')
的时候会引用index.js:
注意此时因为当前目录下有了find-me, 则此时引用find-me会忽略用户home目录下的node_modules。当引用目录的时候,默认状况下会寻找index.js,可是咱们能够经过package.json中的main属性来指示用那个文件。举个例子,为了让require('find-me')
可以解析到find-me文件夹下的其余文件,咱们须要在find-me目录下加一个package.json,并指定应该解析到哪一个文件:
若是只想解析模块但不执行模块,可使用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
模块并无显示在index
模块的子模块中。取而代之的是一个[Circular]
的值,由于这儿是一个循环引用。此时若是node打印lib/util
为index
的子模块的话,则会进入到死循环。这也能够解释了为何须要简单的用[Circular]
来代替lib/util
。
那么若是在lib/util
模块中引用index
模块会发生什么。这就是node中所容许的的循环引用。
为了可以更好的理解循环依赖,首先须要了解一些关于module对象上的一些概念。
任何模块中exports都是一个特别的对象。注意到上面的结果中,每次打印module对象,都会有一个为空对象的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' }
下面来谈谈每一个module对象上的loaded属性。到目前为止,每次咱们打印module对象的时候,loaded
属性都是为false
。module对象经过loaded
属性来记录哪些模块已经加载(loaded为true),哪些模块还未加载(loaded为false)。能够经过setImmediate
方法来再下一个event loop中看到模块已经加载完成的信息。
输出结果以下:
再延迟的console.log
中咱们能够看到lib/util.js
和index.js
已经被彻底加载。
当node加载模块完成后,exports对象也会变成已完成状态。 requiring和loading
的过程是同步的。这也是为何咱们可以在一个循环以后可以看到模块加载完成信息的缘由。
同时这也意味着咱们不能异步的修改exports对象。好比咱们不能像下面这么作:
下面来回答前面提到的循环依赖的问题:若是模块1依赖模块2,同时模块2又依赖模块1,这时候会发生什么呢?
为了找到答案,咱们在lib
目录下建立两个文件,module1.js
和module2.js
,并让他们互相引用:
当执行module1.js
的时候,会看到以下结果:
咱们在module1
尚未彻底加载成功的状况下引用module2
,因为module2
中在module1
尚未彻底加载成功的状况就引用module1
,此时在module2
中可以获得的exports
对象是循环依赖以前的所有属性(也就是require('module2')
以前)。此时只能访问到a
属性,由于b
和c
属性在require('module2')
以后。
node在循环依赖这块的处理十分简单。你能够引用哪些尚未彻底加载的模块,可是只能拿到一部分属性。
经过require
函数咱们能够原生的加载JSON
和C++
扩展。使用的时候甚至不须要指定扩展名。在文件扩展名没有指定的状况下,node首先会尝试加载.js
的文件。若是.js
的文件没有找到,则会尝试加载.json
文件,若是找到.json
文件则会解析.json
文件。若是.json
文件也没有找到,则会尝试加载.node
文件。可是为了不语义模糊,开发者应该在非.js
的状况下指定文件的扩展名。
加载.json
文件对于管理静态配置、或者周期性的从外部文件中读取配置的场景是十分有用的。好比咱们有以下json文件:
咱们能够直接使用它:
运行上面的代码会输出: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
成功后,就能够像引用其余模块同样使用:
从require.extensions
能够看到目前只支持三种类型的扩展:
能够看到每种类型都有不一样的加载函数。对于.js
文件使用module._compile
方法,对于.json
文件使用JSON.parse
方法,对于.node
文件使用process.dlopen
方法。
node中对模块的包裹经常被误解,在理解node对模块的包裹以前,先来回顾下exports/module.exports
的关系。
咱们能够用exports
来暴露属性,可是不能直接替换exports
对象,由于exports
对象只是对module.exporst
的引用。
准确的来讲,exports
对象对于每一个模块来讲是全局的,定义为module
对象上属性的引用。
在解释node包装过程前,咱们再来问一个问题。
在浏览器中,当咱们在全局环境中申明一个变量:var answer = 42;
在定义answer
变量以后的脚本中,answer
变量就属于全局变量。
在node中并非这样的。当咱们在一个模块中定义了一个变量,另外的模块并不能直接访问该模块中的变量,那么在node中变量是如何被局部化的呢?
答案很简单。在编译模块以前,node把模块代码包装在一个函数中,咱们能够经过module
对象上的wrapper
属性来看到这个函数:
node并不会直接执行你写在文件中的代码。而是执行包裹函数的代码,包裹函数会把你写的代码包装在函数体中。这就保证了任何模块中的顶级变量对于别的模块来讲是局部的。
wrapper
函数有5个参数:exports
,require
,module
,__filename
和__dirname
。这也是为何对于每一个模块来讲,这些变量都像是全局的缘由,实际上对每一个模块来讲,这些变量都是独立的。
当node执行包装函数的时候,这些变量都已经被正确赋值。exports
被定义为module.exports
的引用,require
和module
都指向待执行的函数,__filename
和__dirname
表示了被包裹模块的文件名和目录的路径。
若是你运行了一个出错的模块,立马就能看到包裹函数。
能够看到报错的是wrapper函数的第一行。除此以外,因为每一个模块都被函数包裹了一遍,咱们能够经过arguments
来访问wrapper函数全部的参数。
第一个参数是exports
对象,一开始是一个空对象,接着是require/module
对象,这两个对象不是全局变量,都是与index.js
相关的实例化对象。最后两个参数表示文件路径和文件夹路径。
包裹函数的返回值是module.exporst
。在包裹函数内部,咱们能够经过exports
对象来修改module.exports
的属性,可是不能对exports
从新赋值,由于exports
只是一个引用。
上面描述的等价于下面的代码:
若是咱们修改了exports
对象,则exports
对象再也不是module.exports
的引用。这种引用的方式不只在这里能够正常工做,在javascript中都是能够正常工做的。
require
对象并无什么特殊的。require
是一个函数对象,接受模块名或者路径名,并返回module.exports
对象。若是咱们想的话,能够随意的覆盖require
对象。
好比为了测试,咱们但愿能够mock require
函数的默认行为,返回一个模拟的对象,而不是引用模块返回module.exports
对象。对require
进行赋值能够实现这一目的:
在对require
进行从新赋值以后,每次调用require('something')
都会返回mock对象。require
对象也有自身的属性。前面咱们已经看到过了用于解析模块路径的resolve
属性以及require.extensions
属性。
除此以外,还有require.main
属性用来区别当前模块是被引用仍是直接运行的。好比说咱们在print-in-frame.js
文件中有一个printInFrame
函数:
这个函数接受一个数值类型的参数numberic
和一个字符串类型的参数header
,函数中首先根据size
参数打印指定个数*
的frame,并在frame中打印header
。
咱们能够有两种方式来使用这个函数:
~/learn-node $ node print-in-frame 8 Hello
,命令行中给函数传入8和Hello,打印一个8个*
组成的frame,并在frame中输出hello
。require
方式调用:假设print-in-frame.js
暴露出一个printInFrame
函数,咱们能够这样调用:这样会在5个*组成的frame 中打印Hey
。
咱们须要某种方式来区分当前模块是命令行单独调用仍是被其余模块引用的。这种状况,咱们能够经过require.main
来作判断:
这样咱们能够经过这个条件表达式来实现上述应用场景:
若是当前模块没有以模块的方式被其余模块引用,咱们能够根据命令行参数process.argv
来调用printInFrame
函数。不然,咱们设置module.exports
参数为printInFrame
函数。
理解模块缓存是十分重要的。咱们来经过一个简单的例子来说解下,好比说咱们有一个以下的字符画的js文件:
咱们但愿每次require
文件的时候都能显示字符画。好比咱们引用两次字符画的js,但愿能够输出两次字符画:
第二次引用并不会输出字符画,由于此时模块已经被缓存了。在第一次引用后,咱们能够经过require.cache
来查看模块缓存状况。cache
对象是一个简单的键值对,每次引用的模块都会被缓存在这个对象上。cache
上的值就是每一个模块对应的module
对象。咱们能够从require.cache
上移除module
对象来让缓存失效。若是咱们从缓存中缓存中移除module
对象,从新require的时候,node依然会从新加载该模块,并从新缓存该模块。
可是,对于这种状况,上面的修改缓存的方式并非最好的方法。最简单的方法是把ascii-art.js
包装在函数中而后暴露出去,这样的话,当咱们引用ascii-art.js
的时候,会获得一个函数,每次执行的时候都会输出字符画。