内容简介:原文地址:node中采用了两个核心模块来管理模块依赖:可以认为
原文地址: medium.freecodecamp.org/requiring-m…
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
对象
每个module对象都有id属性用来区分不同的模块。id属性一般都是模块对应的绝对路径,在REPL中会简单的设置为 <repl>
。Node模块和系统磁盘上的文件是一一对应的。引用模块实际上会将文件中的内容加载到内存中。node支持通过多种方式来引用文件(比如说通过相对路径或者预配置路径),在把文件中内容加载到内存之前,需要先找到文件的绝对路径。 当不指定路径直接引用 find-me
模块的时候: require('find-me');
node会依次遍历 module.paths
指定的路径来寻找 find-me
模块:
上面的路径是从当前路径到根目录所有目录下的 node_modules
文件夹的路径,除此之外也包括一些遗留的但是已经不推荐使用的路径。如果node在上述路径中都找不到 find-me.js
就会抛出一个“cannot find module error.”的异常。
如果在当前文件夹下创建一个node_modules文件夹,并创建一个find-me.js文件,这时 require('find-me')
就能够找到find-me了。
如果其他路径下也存在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:
Requiring a folder
模块不一定只是一个文件,读者也可以创建一个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
如果只想解析模块但不执行模块,可以使用 require.resolve
函数。 resolve
和 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
对象:
在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, and synchronous loading of modules
任何模块中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对象。比如我们不能像下面这么做:
Circular module dependency
下面来回答前面提到的循环依赖的问题:如果模块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在循环依赖这块的处理十分简单。你可以引用哪些还没有完全加载的模块,但是只能拿到一部分属性。
JSON and C/C++ addons
通过 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
方法。
All code you write in Node will be wrapped in functions
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中都是可以正常工作的。
The require object
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
函数。
All modules will be cached
理解模块缓存是十分重要的。我们来通过一个简单的例子来讲解下,比如说我们有一个如下的字符画的js文件:
我们希望每次 require
文件的时候都能显示字符画。比如我们引用两次字符画的js,希望可以输出两次字符画:
第二次引用并不会输出字符画,因为此时模块已经被缓存了。在第一次引用后,我们可以通过 require.cache
来查看模块缓存情况。 cache
对象是一个简单的键值对,每次引用的模块都会被缓存在这个对象上。 cache
上的值就是每个模块对应的 module
对象。我们可以从 require.cache
上移除 module
对象来让缓存失效。如果我们从缓存中缓存中移除 module
对象,重新require的时候,node依然会重新加载该模块,并重新缓存该模块。 但是,对于这种情况,上面的修改缓存的方式并不是最好的方法。最简单的方法是把 ascii-art.js
包装在函数中然后暴露出去,这样的话,当我们引用 ascii-art.js
的时候,会得到一个函数,每次执行的时候都会输出字符画。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 万字综述,核心开发者全面解读PyTorch内部机制
- [译] 开发者争论 split-lock 检测机制
- normandie出错重试与缓存机制对交互的影响,开发者注意
- 让开发者专注于应用开发,OpenCenter 3.0 开发者预览版发布
- 让开发者专注于应用开发,OpenCenter 3.0 开发者预览版发布
- GitHub 推出开发者赚钱新利器,100% 全给开发者!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
深入浅出SQL(中文版)
贝里 编 / O‘Reilly Taiwan公司 / 东南大学 / 2009-6 / 98.00元
你将从《深入浅出SQL(中文版)》学到什么?在如今的世界,数据就是力量,但是成功的真正秘诀却是管理你的数据的力量。《深入浅出SQL(中文版)》带你进入SQL语言的心脏地带,从使用INSERT和SELECT这些基本的查询语法到使用子查询(subquery)、连接(join)和事务(transaction)这样的核心技术来操作数据库。到读完《深入浅出SQL(中文版)》之时,你将不仅能够理解高效数据库设......一起来看看 《深入浅出SQL(中文版)》 这本书的介绍吧!