Node.js之模块的基本概念

模块算是Node.js里面比较重要的概念了,以往在网页上写JavaScript,都是通过<script>标签来嵌入JavaScript代码,这样对于代码的组织和复用并不灵活,所以,Node.js使用了模块来管理不同的js 的文件


因此,学习模块的概念,对今后写自定义的模块或者使用模块都是很有帮助的

一.定义

nodejs的模块可以分为两大部分来考虑,模块加载和模块编译


1.模块加载

如果require的是一个核心模块,那么几乎不用任何的查找,直接就可以加载,如果require的是自己定义的模块,那么给出模块的详细地址就行了,也是直接就可以加载,如果require的是第三方的模块,也就是npm上面安装的模块,那么可能要经过多次查找,才能加载,比如require('express'):

1.首先会搜寻名称一样的文件或者文件夹,搜寻方式是从当前目录的node_modules开始,如果没找到就返回父目录继续寻找,也就是说逐级向上递归,直到找到为止,否则报错(因此所有的第三方库文件都是下载到node_modules里面,正是这个原因,编译器会在这个文件夹中搜索)

2.如果找到的是一个文件,那么就要排查判断,node.js的模块分为三大类,js文件,json文件,以及node文件,编译器会先从js开始判断,逐级往后,找到符合后缀名的文件并载入(如果require的时候指定了后缀名,就不需要排查判断了,比如require('./index.js'),那么就直接当做js文件加载)

3.如果找到的是一个文件夹,那么这个文件夹就一定是一个包,编译器会首先搜寻主目录下的package.json文件,取出其中main属性指定的文件名作为要载入的文件,如果没有package.json或者package.json里面没有指定main属性,那么编译器就会默认查找文件名为index的文件,然后跳到第二步(npm上下载的包几乎都是使用这个格式,就是文件夹的名字是包名,然后入口文件是index.js)

4.如果搜寻完整个node_modules文件夹还没有找到就会跳到第一步

这里所谓的加载就是读取文件的内容


2.模块编译

加载完文件后,就开始编译文件了,由于有三种文件,所以编译方式各不相同

其中json文件在编译的时候会调用JSON.parse()再返回结果,所以require一个json文件后就可以直接使用了

对于js文件,会把这个文件放在沙盒(虚拟机)中运行,并且返回这个文件的exports属性作为结果

另外要注意,编译后的模块都会被缓存下来,因此,一个模块被加载编译一次后,以后如果继续加载,就直接返回第一次加载的结果(期间任何的变化都会保存,因为模块本身就是一个对象),不会重新加载

//a.js
var x = "Hi";
exports.set = function() {
	x = "Hello!";
}
exports.get = function() {
	console.log(x);
}

//b.js
var a_1 = require('./a.js'); //加载a.js并赋值给a_1
a_1.set(); //这里设置了a_1中x的值
var a_2 = require('./a.js'); //这里再次加载a.js,发现输出的是Hello而不是Hi
a_2.get(); //Hello!

另外还有node文件,node文件是用C/C++写的库文件,一般作为node.js的内建模块供核心模块调用,属于更底层的模块,效率更高,不过编写node模块比较复杂,所以赞不做讨论


二.使用


1.exports和module.exports的区别

但是,当我们看官方的API文档的时候,发现有两种导出方法,一个是exports,二另一个是module.exports,这两种导出方法有什么区别呢

官方API有一个经典的例子,把require方法简化成下面这个函数:

function require(...) {
  // ...
  function (module, exports) {
    // Your module code here
    exports = some_func;        // re-assigns exports, exports is no longer
                                // a shortcut, and nothing is exported.
    module.exports = some_func; // makes your module export 0
  } (module, module.exports);
  return module;
}
通过观察传进去的参数,我们看到,exports = module.exports,所以说,一开始这两个东西指向同一个对象实例,



如果直接添加一个方法,比如,exports.x = func,那么由于exports指向module.exports,那么由于复制引用的关系,相当于module.exports也添加了同样的方法


exports.x = function() {  //在exports上添加了一个方法,module.exports也添加了同样的方法,因为它们指向了同样的对象实例
	console.log("Hi!");
}
module.exports.x(); //Hi!

如果给exports直接赋值,比如这里exports = some_func,那么exports的指向就改变了,但最后导出的是module,因此,exports指向的方法或者今后在exports上添加的方法都不会影响到module.exports,因此不会被导出



可见,exports和module.exports的关系还是挺复杂的,所以官方API还有一句话:

As a guideline, if the relationship between exports and module.exports seems like magic to you, ignore exports and only use module.exports.

看来只使用module.exports或许就会万无一失了


2.缓存的机制

之前说过,node.js会缓存使用过的模块,按一般的理解来说,肯定是使用hash映射来缓存,所以,以什么作为hash映射的id是十分重要的

通过观察源码,在_load函数中发现:

var filename = Module._resolveFilename(request, parent);
var cachedModule = Module._cache[filename];

也就是说是以filename作为缓存的id的,然后我们可以在程序中输出filename:
//D:exam.js
console.log(module.filename); <span style="font-family:Microsoft YaHei;">//D:\exam.js</span>
发现filename是文件的绝对地址

这么说,node.js是以文件的绝对地址来缓存的,所以,这就引出一个问题,如果文件的层次不同,就算require相同名字的东西,可能获得的对象也互不相同

还以拿上面的例子:

//D:/a.js & D:/Test/a.js
var x = "Hi";
exports.set = function() {
	x = "Hello!";
}
exports.get = function() {
	console.log(x);
}
//D:/Test/c.js
var a = require('./a');
a.set();
//D:/b.js
var a = require('./a');
var c = require('./Test/c');
a.get(); //Hi
这里看到,在b.js和c.js都是require的./a文件,但是由于各自的目录都有一个a.js文件,所以,他们缓存的模块各不相同,因此,就算c.js里面已经改变了a对象中x的值,但是在b.js里面还是初始的值

当然,如果所有require的文件都是自己写的,而文件层次自己也很清楚,类似的情况就不会发生,不过,当一个项目比较大的时候,而文件层次错综复杂,有时候很多文件都多次引用了第三方的模块(由于require第三方模块的时候,都是直接写出模块的文件名而并不关心文件的路径),所以很难注意到require的模块是否是同一个模块,因此,就算我们在某个文件中require了一个第三方模块并初始化(比如log4js之类的需要初始化的模块),而在其他文件中又再次require了相同名字个模块,然后想当然的以为这个模块已经初始化了,或许就到了错误的边缘

PS:细细想一下,上面的情况其实发生的可能性不大,毕竟一个项目一般只有一个package.json,不过清楚了这个道理之后,以后就可以以不同方式配置多个第三方模块,然后在不同的文件层次下引用,相互不影响,所以想起来还是有点小激动的~











  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值