SunShinewyf/issue-blog

node中的模块

SunShinewyf opened this issue · 0 comments

node.js中的模块机制是基于CommonJs,对于CommonJsmodule部分,可以戳这里进行查看。

模块的加载规范

对于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中模块的基础知识,比如文件加载方式这里不展开说,具体可以查看这篇文章

exportsmodulemodule.exports

  • module是当前模块的对象的引用,结构如下:
Module {
 id: '.',
 exports: {},
 parent: null,
 filename: '/Users/uc/Project/index.js',
 loaded: false,
 children: [],
 paths:
  [ ... ] }
  • module.exportsmodule的一个属性对象(如上可知),它是由模块系统创建的,并且最终返回给调用的模块
  • exportsmodule.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={}
因为在brequire a的时候,发现arequireb,为了防止循环引用,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是你想要删除缓存的模块名,并且是真实存在的。

具体的讨论可以移步这里

总结

以上只是总结一下我对模块这块存在的有误区或者不太了解的地方,涉及的点不太深,只是自己的一点记录,有不对的地方欢迎斧正。