谈谈node.js中的模块系统
Node.js的模块
JavaScript做为一门为网页添加交互功能的简单脚本语言问世,在诞生时并不包含模块系统,随着JavaScript解决问题越来越复杂,把所有代码写在一个文件内,用function区分功能单元已经不能支撑复杂应用开发了,ES6带来了大部分高级语言都有的class和module,方便开发者组织代码
import_from'lodash'; classFun{} exportdefaultFun;
上面三行代码展示了一个模块系统最重要的两个要素import和export
1.export用于规定模块的对外接口
2.import用于输入其他模块提供的功能
而在ES6之前,社区出现了很多模块加载方案,最主要的有CommonJS和AMD两种,Node.js诞生早于ES6,模块系统使用的是类似CommonJS的实现,遵从几个原则
1.一个文件是一个模块,文件内的变量作用域都在模块内
2.使用module.exports对象导出模块对外接口
3.使用require引入其它模块
circle.js
const{PI}=Math; module.exports=functionarea(r){ PI*r**2; };
上面代码就实现了Node.js的一个模块,模块没有依赖其它模块,导出了方法area计算圆的面积
test.js
constarea=require('./circle.js'); console.log(`半径为4的圆的面积是${area(4)}`);
模块依赖了circle.js,使用其对外暴露的area方法,计算圆的面积
module.exports
模块对外暴露接口使用module.exports,常见的有两种用法:为其添加属性或赋值到新对象
test.js
//添加属性 module.exports.prop1=xxx; module.exports.funA=xxx; module.exports.funB=xxx; //赋值到全新对象 module.exports={ prop1, funA, funB, };
两种写法是等价的,使用时候没区别
constmod=require('./test.js'); console.log(mod.prop1); console.log(mod.funA());
还有另外一种直接使用exports对象的方法,但是只能对其添加属性,不能赋值到新对象,后面会介绍原因
//正确的写法:添加属性 exports.prop1=xxx; exports.funA=xxx; exports.funB=xxx; //赋值到全新对象 module.exports={ prop1, funA, funB, };
require('id')
模块类型
require用法比较简单,id支持模块名和文件路径两种类型
模块名
constfs=require('fs'); const_=require('lodash');
示例中的fs、lodash都是模块名,fs是Node.js内置的核心模块,lodash是通过npm安装到node_modules下的第三方模块,如果出现重名,优先使用系统内置模块
因为一个项目内可能会包含多个node_modules文件夹(Node.js比较失败的设计),第三方模块查找过程会遵循就近原则逐层上溯(可以在程序中打印module.paths查看具体查找路径),直到根据NODE_PATH环境变量查找到文件系统根目录,具体过程可以参考官方文档
此外,Node.js还会搜索以下的全局目录列表:
- $HOME/.node_modules
- $HOME/.node_libraries
- $PREFIX/lib/node
其中$HOME是用户的主目录,$PREFIX是Node.js里配置的node_prefix。强烈建议将所有的依赖放在本地的node_modules目录,这样将会更快地加载,且更可靠
文件路径
模块还可以可以使用文件路径加载,这是项目内自定义模块的通用加载方式,路径可以省略拓展名,会按照.js、.json、.node顺序尝试
- 以'/'为前缀的模块是文件的绝对路径,按照系统路径查找模块
- 以'./'为前缀的模块是相对于当前调用require方法的文件,不受后续模块在哪里被使用到影响
单次加载&循环依赖
模块在第一次加载后会被缓存到Module._cache,如果每次调用require('foo')都解析到同一文件,则返回相同的对象,同时多次调用require(foo)不会导致模块的代码被执行多次。Node.js根据实际的文件名缓存模块,因此从不同层级目录引用相同模块不会重复加载。
理解的模块单次加载机制方便我们理解模块循环依赖后的现象
a.js
console.log('a开始'); exports.done=false; constb=require('./b.js'); console.log('在a中,b.done=%j',b.done); exports.done=true; console.log('a结束');
b.js
console.log('b开始'); exports.done=false; consta=require('./a.js'); console.log('在b中,a.done=%j',a.done); exports.done=true; console.log('b结束');
main.js
console.log('main开始'); consta=require('./a.js'); constb=require('./b.js'); console.log('在main中,a.done=%j,b.done=%j',a.done,b.done);
当main.js加载a.js时,a.js又加载b.js,此时,b.js会尝试去加载a.js
为了防止无限的循环会返回一个a.js的exports对象的未完成的副本给b.js模块,然后b.js完成加载,并将exports对象提供给a.js模块
因此示例的输出是
main开始
a开始
b开始
在b中,a.done=false
b结束
在a中,b.done=true
a结束
在main中,a.done=true,b.done=true
看不懂上面的过程也没关系,日常工作根本用不到,即使看懂了也不要在项目中使用循环依赖!
工作原理
Node.js每个文件都是一个模块,模块内的变量都是局部变量,不会污染全局变量,在执行模块代码之前,Node.js会使用一个如下的函数封装器将模块封装
(function(exports,require,module,__filename,__dirname){ //模块的代码实际上在这里 });
- __filename:当前模块文件的绝对路径
- __dirname:当前模块文件据所在目录的绝对路径
- module:当前的模块实例
- require:加载其它模块的方法,module.require的快捷方式
- exports:导出模块接口的对象,module.exports的快捷方式
回头看看最开始的问题,为什么exports对象不支持赋值为其它对象?把上面函数添加一句exports对象来源就很简单了
constexports=module.exports; (function(exports,require,module,__filename,__dirname){ //模块的代码实际上在这里 });
其它模块require到的肯定是模块的module.exports对象,如果吧exports对象赋值给其它对象,就和module.exports对象断开了连接,自然就没用了
在Node.js中使用ESModule
随着ES6使用越来越广泛,Node.js也支持了ES6Module,有几种方法
babel构建
使用babel构建是在v12之前版本最简单、通用的方式,具体配置参考@babel/preset-env
.babelrc
{ "presets":[ ["@babel/preset-env",{ "targets":{ "node":"8.9.0", "esmodules":true } }] ] }
原生支持
在v12后可以使用原生方式支持ESModule
- 开启--experimental-modules
- 模块名修改为.mjs(强烈不推荐使用)或者package.json中设置"type":module
这样Node.js会把js文件都当做ESModule来处理,更多详情参考官方文档
以上就是谈谈node.js中的模块系统的详细内容,更多关于node.js模块的资料请关注毛票票其它相关文章!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。