《深入浅出Node.js》(node的模块规范与模块实现)

Node前言介绍 Node的目标是成为一个构建快速、可伸缩的网络应用平台,通过通信协议来组织许多Node,非常容易通过扩展来达成构建大型网络应用的目的。
Node作为后端JavaScript的运行平台,保留了前端浏览器JavaScript中那些熟悉的接口,没有改写语言本身的任何特性,依旧基于作用域和原型链,区别在于它将前端中广泛运用的思想迁移到服务器端。
Node支持异步I/O、事件与回调函数、单线程,并且跨平台。
基于以上支持点,Node擅于应用的场景包括:I/O密集型、CPU密集型、分布式应用。
Node使用模块化来组织JS代码,模块规范采用CommonJS规范。
对于JavaScript语言本身来说,有几个方面的天然缺陷:

  • 没有模块系统。
  • 标准库较少。ES仅定义部分核心库,对于文件系统、I/O流等常见需求却没有标准API。
  • 没有标准接口。js中没有定义过如Web服务器或数据库之类的标准统一接口。
  • 缺乏包管理系统。这导致js应用中没有自动加载和安装依赖的能力。
在ES6中模块之前,CommonJS可以一定程度上弥补没有标准的缺陷。
CommonJS快速介绍 CommonJS对于模块的定义很简单,分为模块定义、模块引用和模块标识3个部分。
//math.js模块定义文件 function add(){ var sum = 0, i = 0, args = arguments, l = args.length; while( i

模块引用:在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中。
模块定义:在模块中,上下文提供require()方法来引入外部模块。对应引入的功能,上下文也提供了module.exports对象用于导出当前模块的方法和变量,并且它还是唯一导出的出口。这里的module是一个对象,表示模块本身,而exports就是它的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性就能导出。然后在另一个文件中,通过require()方法引入模块后,就能调用定义的属性和方法了。
模块标识:它是传递给requrie()方法的参数,它必须是符合小驼峰命名的字符串,或者是以...开头的相对路径,或者绝对路径,它可以没有文件名后缀.js
【《深入浅出Node.js》(node的模块规范与模块实现)】CommonJS构建的这套模块导出和引入机制使得用户完全不要考虑变量污染。
Node的模块实现 在Node中,也不会完全套用CommonJS规范的,而是有一定取舍,也增加些新的特性。对于module.exportsrequire()Node实现起来主要有三个步骤:路径分析、文件定位和编译执行。
Node中的模块包含两种:一类是由Node提供的模块叫核心模块;一类是用户编写的模块叫文件模块。
  • 其中核心模块在Node源代码的编译过程中,编译进了二进制执行文件。Node进程启动时核心模块就直接加载进内存中,所以当其被引入时,直接省去文件定位和编译执行两步,并且在路径分析中优先判断,所以其加载速度最快。
  • 而文件模块则是在运行时动态加载,需要执行完整三步,所以加载速度略慢。
Node通常优先从缓存中加载,不管要加载的是核心模块还是文件模块。区别仅在于核心模块的缓存检查先于文件模块的缓存检查。
在路径分析中,Node会基于require()方法中的模块标识符进行模块查找。模块标识符主要有以下几类:
  • 核心模块,如httpfspath等,加载速度最快
  • ...开始的相对路径文件模块
  • /开始的绝对路径文件模块
  • 非路径形式的文件模块,如自定义的connect模块
在文件定位中,首先会按照缓存加载的优化策略加载二次引入的模块,否则就按照首次加载策略执行文件定位。
最后就是编译执行阶段。当定位到具体文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也不同:
  • .js文件。通过fs模块同步读取文件后编译执行
  • .node文件。这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件
  • .json文件。通过fs模块同步读取文件后,用JSON.parse()解析返回结果
  • 其余扩展名文件,则者被当作.js文件载入。
这里补充下核心模块相关。核心模块又分为JavaScript核心模块和C/C++核心模块,后者又被称为内建模块。核心模块中有的模块全部由C/C++编写,部分是由C/C++完成核心部分,其他部分则由JavaScript实现包装或向外导出,以满足性能平衡需求。Node中的osfsbuffer等都是部分通过C/C++写的。
在Node的所有模块类型中,存在着这样的依赖层级关系:文件模块依赖核心模块,核心模块依赖内建模块。通常文件模块不推荐依赖内建模块,如需调用则直接调用核心模块即可,因为核心模块中都已基本封装了内建模块。
除了JavaScript模块外,Node中还可以写C/C++扩展模块,注意这与内建模块是不同的。C/C++扩展模块加载的是.node文件,Node会调用process.dlopen()来加载文件。使用C/C++扩展模块的好处是加载后不需要编译,直接执行之后就可以被外部调用了,加载速度略快于JavaScript模块。
以上简单介绍了Node中的模块:文件模块、核心模块、内建模块和C/C++扩展模块它们各自的区别,下面弄清下它们之间的调用关系:
**C/C++内建模块属性最底层的模块,它属于核心模块,主要提供API给JavaScript核心模块和第三方JavaScript文件模块调用。**如果不是很了解C/C++内建模块的,尽量避免使用process.binding()方法直接调用。
JavaScript核心模块主要扮演的职责有两类:一类是作为C/C++内建模块的封装层和桥接层,供文件模块调用;一类是纯粹的功能模块,它不需要跟底层打交道,但又非常重要。
文件模块通常由第三方编写,包括普通的JavaScript模块和C/C++扩展模块,主要调用方向为普通JavaScript模块调用扩展模块。
多模块兼容写法 Node使用JavaScript语言有一个很好的优点,那就是一些模块可以在前后端实现共用,这是因为很多API在各个宿主环境下都提供。但实际情况下,前后端的环境有时还是会有区别的。
可以看到Node的模块引入过程主要都是同步的,因为服务器端从磁盘加载资源,所以速度很快,加载的瓶颈在于CPU和内存等资源。而前端由于UI在初始化过程中用户体验的问题,应尽可能减少同步引入模块,避免阻塞,其加载瓶颈在带宽。
所以CommonJS规范更适合于后端,而前端的模块引入使用AMD规范更适宜,或者也可以使用CMD规范。我更习惯于用AMD规范。
为了让同一个模块可以运行在前后端,在写模块时就需要考虑兼容前端也实现模块规范的环境。为保持前后端一致性,类库代码可以包装在一个闭包内,这方面比较典型的就是JQuery了。下面实现一个简单的模块兼容示例,它将兼容Node、AMD、CMD和常见浏览器环境:
(function (name, factory) { // 检测上下文环境是否为AMD或CMD var hasDefine = typeof define === "function", // 检测上下文环境是否为Node,也就是支持CommonJS规范 hasModule = typeof module !== "undefined" && module.exports; if(hasDefine){ // AMD环境或CMD环境 define("method", [], factory); }else if(hasModule) { // 定义为普通Node模块 module.exports = factory(); }else { //将模块的执行结果挂在window变量中,在浏览器中this指向window对象 this.name = factory(); }})("privateModule", function () { var hello = function () { return "Hello Nitx!"; }return hello; })

喜欢本文请扫下方二维码,关注微信公众号: 前端小二,查看更多我写的文章哦,多谢支持。
《深入浅出Node.js》(node的模块规范与模块实现)
文章图片

    推荐阅读