前面的话
在Node的实现中,并没有完全按照CommonJS规范来实现。相反,它在模块规格上做了一定的权衡,同时增加了一些它需要的特性。本文将详细介绍NodeJS的模块实现
从别处引进
Nodejs不同于javascript。javascript中的顶部对象是窗口,而node中的顶部对象是全局的
[注意]其实javascript也有一个全局对象,但是它并不访问外界,而是使用window对象指向全局对象
在javascript中,通过var a=100;你可以通过窗户得到100英镑
但是在nodejs中,它不能通过global.a访问,并且它没有定义
这是因为var a=100此语句中的变量a只是模块范围内的变量a,而不是全局对象下的变量a
在nodejs中,文件是一个模块,每个模块都有自己的作用域。var声明的变量不是全局变量,但属于当前模块
如果要在全局范围内声明一个变量,如下所示
摘要
Node中的模块分为两类:一类是Node提供的模块,称为核心模块;另一个是用户编写的模块,称为文件模块
在Node源代码的编译过程中,核心模块被编译成二进制执行文件。Node进程启动时,一些核心模块直接加载到内存中,所以在引入这个核心模块时,可以省略文件定位和编译执行两个步骤,路径分析中优先考虑判断,因此其加载速度最快
文件模块在运行时动态加载,需要完整的路径分析、文件定位、编译和执行过程,比核心模块慢
接下来,我们开始详细的模块加载过程
模块加载
在javascript中,模块可以加载脚本标签,但是在nodejs中,如何在一个模块中加载另一个模块呢?
使用require()方法介绍
[缓存加载]
在展开引入require()方法的标识符分析之前,我们需要知道,正如前端浏览器缓存静态脚本文件以提高性能一样,Node缓存引入的模块以减少二次引入的开销。不同的是,浏览器只缓存文件,而节点在编译和执行后缓存对象
无论是核心模块还是文件模块,require()方法对于同一个模块的二次加载总是采用缓存优先级,这是第一优先级。区别在于核心模块的缓存检查先于文件模块的缓存检查
[标识符分析]
require()方法接受标识符作为参数。在节点实现中,基于这样的标识符来搜索模块。模块标识符在Node中主要分为以下几类:[1]核心模块,如http、fs、path等。[2]相对路径文件模块以。或者.[3]以/开头的绝对路径文件模块;[4]非路径形式的文件模块,如自定义连接模块
根据参数的不同格式,require命令会转到不同的路径来查找模块文件
1.如果参数字符串以“/”开头,则表示加载了位于绝对路径中的模块文件。例如,require('/home/marco/foo.js ')将加载/home/marco/foo.js。
2.如果参数字符串以”开头。/,表示模块文件是以相对路径加载的(与当前执行脚本的位置相比)。例如,require('。/circle ))将在与当前脚本相同的目录中加载circle.js
3.如果参数字符串不以”开头。/“或”/,表示加载了默认的核心模块(位于Node的系统安装目录中)或位于各级node_modules目录中的已安装模块(全局安装或部分安装)。
[注意]如果是当前路径下的文件模块,必须以开头。/,否则nodejs将尝试加载核心模块或node_modules中的模块
//a . js console . log(' AAA ');//b.jsrequire('。/a ');//' AAA ' require(' a ');//错误报告[文件扩展名分析]
Require()在分析标识符的过程中,会出现文件扩展名不包含在标识符中的情况。Commonjs模块规范也允许标识符不包含文件扩展名。在这种情况下,Node将首先找出是否有不带后缀的文件,如果没有,则按照的顺序组成扩展名。js,json和。节点,并依次尝试
在尝试的过程中,需要调用fs模块来判断文件是否存在阻塞的情况。因为节点是单线程的,所以这是一个可能导致性能问题的地方。诀窍是:如果是的话。节点和。在json文件中,通过给传递给require()的标识符添加一个扩展名,速度会加快一点。另一个诀窍是与缓存同步,可以大大缓解Node单线程阻塞调用的缺陷
[目录分析和包装]
在分析标识符的过程中,分析文件扩展名后,require()可能找不到对应的文件,而是得到一个目录,这种情况经常出现在引入自定义模块,逐个搜索模块路径的时候。此时,节点会将目录视为一个包
在这个过程中,Node在一定程度上支持CommonJS包规范。首先Node在当前目录下搜索package . JSON(common js包规范定义的包描述文件),通过JSON.parse()解析包描述对象,取出主属性指定的文件名进行定位。如果文件名缺少扩展名,将进入扩展名分析步骤
而如果主属性指定的文件名错误,或者根本没有package.json文件,Node会以index作为默认文件名,然后依次搜索index.js、index.json和index.node
如果在目录分析过程中没有成功找到文件,用户定义的模块将进入下一个模块路径进行搜索。如果遍历模块路径数组,仍然没有找到目标文件,将引发查找失败的异常
访问变量
如何访问一个模块中定义的变量?
【全球】
最简单的方法是将一个模块定义的变量复制到全局环境中,然后另一个模块可以访问全局环境
//a . jsvar a=100;global.a=a//b.jsrequire('。/a ');console . log(global . a);//100虽然这种方法简单,但不建议使用,因为会污染全球环境
【模块】
常见的方法是使用nodejs提供的Module对象模块,保存一些与当前模块相关的信息
函数模块(id,parent){ this . id=id;this . exports={ };this.parent=parentif(parent parent . children){ parent . children . push(this);} this.filename=nullthis.loaded=falsethis . children=[];}module.id模块的标识符,通常是带有绝对路径的模块文件名。模块. file name的文件名,带有绝对路径。Module.loaded返回一个布尔值,该值指示模块是否已完成加载。Module.parent返回一个对象,该对象表示调用该模块的模块。Module.children返回一个数组,该数组表示该模块要使用的其他模块。module.exports表示模块外部输出的值。
【出口】
module.exports属性指示当前模块的输出接口。当其他文件加载该模块时,它们实际上读取了该模块
//a . jsvar a=100;module . exports . a=a;//b.jsvar结果=require('。/a ');console.log(结果);//'{ a: 100} '为方便起见,Node为每个模块提供了一个导出变量,指向模块。导出.因此,在将模块接口导出到外部世界时,可以向exports对象添加方法
console . log(module . exports===exports);//true[注意]您不能直接将exports变量指向某个值,因为这相当于切断了exports和module.exports之间的链接
模块编译
编译和执行是模块实现的最后阶段。定位特定文件后,Node会新建一个模块对象,然后根据路径加载编译。不同的文件扩展名有不同的加载方法,如下所示
Js文件——通过fs模块同步读取文件后编译执行
节点文件——这是一个用C/C编写的扩展文件,最终编译生成的文件是用dlopen()方法加载的
Json文件——通过fs模块同步读取文件,然后用JSON.parse()解析返回的结果
其他扩展文件——加载为。js文件
每个成功编译的模块将缓存其文件路径作为模块上的索引。_缓存对象以提高二次导入的性能
根据不同的文件扩展名,Node会调用不同的读取方法,例如,的调用。json文件如下:
//jsonmodule . _ extensions[]的本机扩展。json']=函数(模块,文件名){ var content=nativemodule . require(' fs ')。readFileSync(文件名,‘utf8’);try { module . exports=JSON . parse(strip BoM(content));} catch(err){ err . message=filename ' : ' err . message;抛出错误;}};其中,模块。_extensions将被分配给require()的extensions属性,因此您可以通过访问代码中的require.extensions来了解系统中现有的扩展加载模式。编写以下代码进行测试:
console . log(require . extensions);实施结果如下:
{'.js ' :[函数],'。JSON ' :[函数],'。Node' : [function]}确定文件扩展名后,节点会调用特定的编译方法来执行文件并返回给调用者
JavaScript模块的编译]
回到CommonJS模块规范,我们知道每个模块文件中有三个变量:require、exports和module,但是它们在模块文件中没有定义,那么它们从何而来呢?甚至在Node的API文档中,我们知道每个模块中有两个变量,filename和dirname,它们来自哪里?如果把直接定义模块的过程放在浏览器端,会有全局变量的污染
事实上,在编译过程中,Node包装了JavaScript文件的内容。在头部和尾部增加了(函数(导出、要求、模块、文件名、目录名);
一个普通的JavaScript文件将被包装如下
(函数(导出、要求、模块、文件名、目录名){ var math=require(' math ');exports.area=函数(半径){ return Math。PI *半径*半径;};});这样,每个模块文件的范围就被隔离了。包装好的代码将由vm原生模块的runInThisContext()方法执行(类似于eval,但是上下文清晰,没有全局污染),并返回一个具体的函数对象。最后,将当前模块对象的exports属性、require()方法、模块(模块对象本身)以及在文件位置获得的完整文件路径和文件目录作为参数传递给此函数()以供执行
这就是为什么这些变量没有在每个模块文件中定义。执行后,模块的exports属性返回给调用方。可以从外部调用exports属性上的任何方法和属性,但不能直接调用模块中的其他变量或属性
至此,需求、导出、模块的流程都完成了,这就是节点对CommonJS模块规范的实现
[C/C模块编译]
节点调用process.dlopen()方法来加载和执行。在Node的架构下,dlopen()方法在Windows和*nix平台下有不同的实现,并由libuv兼容层封装
事实上。节点不需要编译,因为是C/C模块写完后编译生成的,所以只有加载和执行的过程。在执行过程中,模块的exports对象与。节点模块,然后返回给调用方
C/C模块给Node用户带来的优势主要是在执行效率方面,而缺点是C/C模块的编写门槛比JavaScript高
JSON文件的编译]
编译。json文件是三种编译方法中最简单的一种。与fs模块同步读取JSON文件内容后,Node调用JSON.parse()方法获取对象,然后分配给模块对象的导出进行外部调用
JSON文件在用作项目的配置文件时非常有用。如果将JSON文件定义为配置,不需要调用fs模块进行异步读取和解析,只需要调用require()引入即可。此外,您还可以享受模块缓存的便利,再次引入时不会对性能产生影响
CommonJS
在介绍了Node的模块实现之后,回到头部,学习CommonJS规范,相对容易理解。
CommonJS规范的提出主要是为了弥补javascript目前没有标准的缺陷,使其具备开发大规模应用的基本能力,而不是停留在小脚本程序的阶段
CommonJS对模块的定义非常简单,主要分为三个部分:模块引用、模块定义和模块标识
[模块参考]
var math=require(' math ');在CommonJS规范中,有一个require()方法,它接受模块标识,从而将模块的API引入到当前上下文中
[模块定义]
在模块中,上下文提供了一个require()方法来引入外部模块。与引入的函数相对应,上下文提供了exports对象用来导出当前模块的方法或变量,是唯一的导出出口。在模块中,还有一个模块对象,代表模块本身,导出是模块的一个属性。在节点中,文件是一个模块,可以通过将导出对象上的方法作为属性挂载来定义导出方法:
//math . jsexports . add=function(){ var sum=0,i=0,args=args . length;while(I l){ sum=args[I];}返回总和;};在另一个文件中,在我们通过require()方法引入模块之后,我们可以调用定义的属性或方法
//program . jsvar math=require(' math ');exports . increment=function(val){ return math . add(val,1);};[模块识别]
模块标识符实际上是传递给require()方法的参数,它必须是以小驼峰命名的字符串,或者是以.或绝对路径。它可以没有文件名后缀. js。
模块的定义很简单,界面也很简洁。其意义在于将聚类的方法和变量限制在私有范围内,并支持导入和导出功能来平滑连接上下游依赖关系。每个模块都有一个独立的空间,它们不会相互干扰,引用时也显得干净整洁
以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。