当前位置: 首页 > 后端技术 > Node.js

[Node]前后端模块规范及模块加载原则

时间:2023-04-03 17:00:06 Node.js

CommonJS定义模块,导出和require模块规范。为了实现这个简单的标准,Node.从分析定位文件到编译执行,经历了一系列复杂的过程。简单了解Node模块的原理,将有助于我们重新认识基于Node构建的框架。1.CommonJS模块规范CommonJS规范或标准只是一种理论。它期望JavaScript能够跨主机环境执行。它不仅可以开发客户端应用程序,还可以开发服务器端应用程序、命令行工具和桌面图形界面。application等。CommonJS规范中模块的定义分为三部分:模块定义存在于模块中,模块对象代表模块本身,模块上下文提供exports属性,可以定义export方法通过在exports对象上安装方法,例如://math.jsexports.add=function(){//...}模块引用模块提供require()方法将外部模块的API引入当前上下文:varmath=require('math')模块标识模块标识其实就是传递给require()方法的参数可以是驼峰命名的字符串,也可以是文件路径。我一直很困惑,为什么我必须使用module.exports当每个模块都可以使用exports时?因为exports只是module.exports的一个地址引用,如果修改了exports的引用(比如指向一个对象),就不能导出为一个模块。而当module.exports已经有一些属性和方法时,Node会忽略exports,只导出module.exports。所以直接赋值给module.exports会更准确。Node.js借鉴了CommonJS规范的设计,特别是CommonJS的Modules规范,实现了一个模块系统。同时,NPM实现了CommonJS的Packages规范。模块和包构成了Node应用程序开发的基础。2、Node模块加载原理上面的模块规范看起来很简单,只有module、exports和require,但是Node是如何实现的呢?它需要经过三个步骤:路径解析(模块的完整路径)、文件定位(文件扩展名或目录)、编译。2.1路径分析回顾require()接收模块标识作为参数导入模块,Node根据这个标识进行路径分析。不同的标识符使用不同的解析方式,主要分为以下几类:Node提供的核心模块,如http、fs、path核心模块在Node源码编译时存储为二进制可执行文件,Node运行时直接加载starts在内存中,在路径分析中判断优先级,所以加载速度非常快,不需要后续的文件定位、编译和执行。如果要加载与核心模块同名的自定义模块,例如自定义http模块,则必须选择不同的标识符或使用路径。路径形式的文件模块,.,..相对路径模块和/绝对路径模块以.,..或/开头的标识符将被视为文件模块,Node会将require()中的路径转换为真实路径为一个索引,然后编译执行。由于文件模块指定了文件位置,缩短了路径分析时间,加载速度仅比核心模块慢。自定义模块,即非路径文件模块,既不是核心模块,也不是路径文件模块。自定义文件是一种特殊的文件模块。搜索路径时,Node会在模块的路径中一步步搜索路径。一个示例模块路径查找策略如下://paths.jsconsole.log(module.paths)//Terminal$nodepaths.js['/Users/tong/WebstormProjects/testNode/node_modules','/Users/tong/WebstormProjects/node_modules','/Users/tong/node_modules','/Users/node_modules','/node_modules']从上面例子输出的模块路径数组可以看出,搜索模块时,node_modules目录就是沿着当前路径一步步往上查找,直到Up到目标路径,类似于JS原型链或者作用域链。路径越深,速度越慢,因此自定义模块加载最慢。缓存优先级机制:Node会缓存导入的模块以提高性能。与缓存文件的浏览器不同,Node缓存编译和执行的对象,因此require()使用缓存来二次加载同一模块的首选方式。这个缓存优先级是第一优先级,高于核心模块的优先级!2.2文件定位模块路径解析完成后就是文件定位,主要包括文件扩展名解析、目录和包处理。为了表达更清楚,文件位置分为四步:step1:补充扩展名通常require()中的标识符不包含文件扩展名。在这种情况下,Node将按照.js,.json,.node的顺序尝试补充扩展名。尝试添加扩展时,需要调用fs模块同步阻塞判断文件是否存在,所以这里有一个提高性能的小技巧,就是在将.json和.node文件传递给require的时候(),会加快速度。step2:目录处理寻找pakage.json如果添加扩展后没有找到对应的文件,但是得到了一个目录,Node会把这个目录当作一个包。根据CommonJS包规范的实现,Node会在目录中搜索pakage.json(包描述文件),通过JSON.parse()解析成包描述对象,定位到main属性指定的文件名.step3:默认继续搜索索引文件。如果没有pakage.json或者main属性指定的文件名错误,Node会默认使用index作为文件名,依次搜索index.js、index.json、index.node。Step4:进入下一个模块路径当上述目录分析过程中定位不成功时,自定义模块根据路径搜索策略进入上层的node_modules目录。当遍历整个模块路径数组,没有定位到文件时,会抛出查找失败异常。缓存加载的优化策略使得二次导入无需路径解析、文件定位、编译执行等过程,核心模块无需文件定位过程,大大提高了模块重新加载的效率。2.3编译执行Node中的各个节点模块是一个对象。定位到文件后,Node会创建一个新的模块对象,然后根据路径加载编译。不同文件扩展名的加载方式为:.js文件:通过fs模块同步读取并编译执行。json文件:通过fs模块同步读取后,使用JSON.parse()解析并返回结果。节点文件:这是一个用C/C++编写的扩展文件。它通过process.dlopen()方法加载最终编译生成的其他扩展:全部加载为js文件。加载成功后,Node会调用具体的编译方法执行文件,返回给调用者。.json文件的编译是最简单的。JSON.parse()解析对象后,直接赋值给模块对象的exports,而.node文件则由C/C++编译生成。Node直接调用process.dlopen()加载执行是的,下面重点介绍.js文件的编译:在CommonJS模块规范中,有module、exports和require三个变量,在NodeAPI文档中,各module还有两个变量:__filename和__dirname,但是在module中并没有定义这些变量,那么它们是怎么来的呢?实际上,在编译过程中,Node对每个JS文件进行了封装,每个文件都是一个模块,有自己的作用域。比如一个JS文件会被封装成这样:)首先隔离每个模块文件的作用域,通过vm原生模块的runInThisContext()方法(类似eval)返回一个具体的函数对象,最后是当前模块对象的exports属性,require()方法,以及模块对象本身Module,将定位文件时得到的完整路径__filename和文件目录__dirname作为参数传递给该函数执行。模块的exports属性上的任何方法和属性都可以被外部调用,其余的不能调用。至此,module、exports、require的过程就介绍完了。编译成功的模块会将文件路径作为索引缓存在Module._cache对象上,解析路径时会优先查找缓存,提高二次导入的性能。3.Node核心模块综上所述,Node模块分为Node提供的核心模块和用户编写的文件模块。文件模块是运行时动态加载的,包括上述完整的路径解析、文件定位、编译和执行过程。核心模块在Node源代码编译成可执行文件时以二进制文件形式存储,直接加载到内存中,不需要文件。定位并编译执行。核心模块分为用C/C++编写和用JavaScript编写的两部分。在编译所有的C/C++文件之前,编译器需要将所有的JavaScript核心模块编译成C/C++可执行代码,编译好的放在NativeModule的._cache对象上,和文件模块的缓存位置明显不同模块._缓存。核心模块中,部分模块是纯C/C++编写的内置模块,主要为JavaScript核心模块提供API,用户通常不能直接调用,而部分模块由C/C++完成,JavaScript实现封装并传递给Export,如buffer、fs、os等。所以Node的模块类型中存在一个依赖层次:内置模块(C/C++)—>核心模块(JavaScript)—>文件模块。使用require()非常方便,但是从JavaScript到C/C++的过程非常复杂。综上所述,需要经历C/C++层面的内置模块定义、(JavaScript)核心模块的定义和引入、(JavaScript)文件模块的引入。4、前端模块规范对比前端和前端JavaScript。浏览器端JavaScript需要从同一台服务器分发到多个客户端执行,通过网络加载代码。瓶颈在宽带;而服务器端的JavaScript需要多次执行相同的代码。对于磁盘加载,瓶颈在CPU和内存,所以Http两端的JavaScript根本没有责任。Node模块的引入几乎是同步的,如果前端模块同步引入的话,加载脚本的时间会过长,所以CommonJS为后端JavaScript制定的规范并不适用于前端。然后针对前端应用场景出现了AMD和CMD。4.1AMD规范AMD是异步模块定义(AsynchronousModuleDefinition),模块定义是:define(id?,dependencies?,factory);AMD模块需要用define明确定义一个模块,其中moduleid和dependencies是可选的,factory中的内容就是实际代码的内容。例如,在模块中指定一些依赖项:define(['dep1','dep2'],function(){//模块代码});require.js实现了AMD规范的模块化,有兴趣的可以查看require.js文档。4.2CMD规范CMD模块的定义比较简单:define(factory);定义的模块像Node模块一样被隐式封装,在依赖部分支持动态导入,例如:define(function(require,exports,module){//模块代码});require、exports、module通过形参传递给模块。当需要依赖模块时,直接使用require()引入即可。sea.js实现了AMD规范的模块化。如果你有兴趣,可以查看sea.js文档。推荐两本Node书籍:《Node.js 实战》主要使用实例,《深入浅出 Node.js》侧重于实现原理。当然,我的博客会不断总结更新,下一篇会讲CommonJS包规范和NPM包管理。