node中的模块
SunShinewyf opened this issue · 0 comments
node.js
中的模块机制是基于CommonJs
,对于CommonJs
的module
部分,可以戳这里进行查看。
模块的加载规范
对于js
的模块部分,有好多这方面的文章,所以在这里我就不再赘述了,对于几种模块的加载规范之间的差别,可移步这里
简述 module
定义
在node
中,每一个文件都被当成一个独立的模块,而且每个模块都有自己的作用域,这就很好地保证了不同模块之间变量的相互污染。因为每个模块被node
包装成如下所示:
(function (exports, require, module, __filename, __dirname) {
//the code of singal file
});
exports
属性上的任何方法和属性都可以在外部被调用,模块中的其余变量或属性则不可直接被调用。但是被加载模块中的全局变量可以被外部调用
//a.js
name = 'test';
//b.js
var a = require('./a.js');
console.log(name);
此时可以打印出test
,但是将a.js
改为如下:
//a.js
var name = 'test';
此时就会报错。
对于node
中模块的基础知识,比如文件加载方式这里不展开说,具体可以查看这篇文章
exports
、module
和module.exports
module
是当前模块的对象的引用,结构如下:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/uc/Project/index.js',
loaded: false,
children: [],
paths:
[ ... ] }
module.exports
是module
的一个属性对象(如上可知),它是由模块系统创建的,并且最终返回给调用的模块exports
是module.exports
的一个引用,相当于一个快捷键:
exports = module.exports = {}
//类似于
b = a = {};
//类似于
a = {}
b = a;
最终返回给require
的是module.exports
模块。
两者之间的相互改变有如下几种情况:
- 当改变了
exports
的引用时(即exports
指向了一个新的对象空间),此时不会影响到module.exports
:
module.exports = {
name: 'a'
}
exports = {
age: '21'
}
此时module
中的exports
对象中只有一个name
属性,而不会有age
属性。因为exports
指向了一个新的空间。
- 当没有改变
exports
的引用时,并且添加一个module.exports
中(并且此时module.exports
有显式声明为一个对象实例)没有的属性时,不会改变module.exports
:
module.exports = {
name: 'a'
}
exports.age = 21
此时module
中的exports
对象中只有name
属性,而没有age
属性,因为原先的module.exports
中没有age
属性,无法添加
- 当没有改变
exports
的引用时,并且添加一个module.exports
中(并且此时module.exports
没有显式声明为一个对象实例)没有的属性时,会改变module.exports
:
module.exports.name = 'a';
exports.age = 21
此时module
中的exports
对象中既有name
属性又有age
属性
- 当没有改变
exports
的引用,并且添加一个module.exports
中(并且此时module.exports
没有显式声明为一个对象实例)有的属性时,会覆盖module.exports
原有的属性:
module.exports.name = 'a';
exports.age.name = 'b';
此时module
中的exports
对象中既有name
属性,并且值为b;
总结:其实也就是两个对象之间相互引用的关系,上面的几种情况也是基于这几种情况来说的而已。为了防止这种改变了值但是不生效的情况,可以采用如下策略:
- 对于要导出的属性,可以简单直接挂到
exports
对象上 - 对于要导出类的情况,直接使用
module.exports
进行导出即可 - 如果要使用
exports
导出类,需要使用exports = module.exports = obj
进行hack
即可
模块之间的相互引用
之前在做项目的时候,遇到一个场景:a
模块中引入了b
模块,然后b
模块又引入了a
,然后在b
中访问不到a
的属性,当时还花了好长时间来排查(捂脸)...
上面这个场景可以被简单复现为如下:
//a.js
const b = require('./b.js');
console.log('在 a 中,b.done = ', b.name);
console.log(b);
exports.name = 'a';
//b.js
const a = require('./a.js');
console.log('在 b 中,a.done = ', a.name);
exports.name = 'b';
此时运行b.js
,会发现打印出的b.name=undefined, b={}
。
因为在b
在require a
的时候,发现a
也require
了b
,为了防止循环引用,a
中此时的b
只是一个exports
未加载完成的副本{},所以没有任何值打印,但是在b
中,可以获取到a.name
属性。
所以为了避免出现这种情况,应该尽量避免模块之间的相互引用
主模块
官方文档中对于其实有描述,只是有点简单,之后通过实验了一下,才领会了说的啥。
也就是如果当前要执行当前文件,比如a.js
,如果此时执行a.js
,那么它的require.main === module
就为true
,但是对于如下场景:
//a.js
exports.a = require.main === module
//b.js
var a = require('./a.js');
console.log(a);
此时打印出来的就是false。
因为每个文件模块都会被包装成一个函数,并且会有一个__filename
的参数,而且__filename === require.main.filename
,所以可以通过检查 require.main.filename
来获取当前应用程序的入口点。
模块的缓存
模块在第一次被加载之后就被缓存起来了,这意味着以后每一次再调用相同的模块将会返回同样的一个对象。这种处理方式在大多数情况下是很好的。但是如果有一些比较特殊的场景需要删除这个缓存,要怎么做呢
delete require.cache[moduleName]
其中moduleName
是你想要删除缓存的模块名,并且是真实存在的。
具体的讨论可以移步这里
总结
以上只是总结一下我对模块这块存在的有误区或者不太了解的地方,涉及的点不太深,只是自己的一点记录,有不对的地方欢迎斧正。