《深入浅出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<l ){
        sum += args[i++];
    }
    return sum;
}
module.exports = {add};

//test.js   模块引用文件,假设与math.js文件在同一目录下
var math = require("./math");   // 字符串 ./math 就是模块标识;本行代码就是模块引用
math.add(10, 2);

// 执行test.js文件: node test.js
// 打印:
// 12

模块引用:在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中。

模块定义:在模块中,上下文提供require()方法来引入外部模块。对应引入的功能,上下文也提供了module.exports对象用于导出当前模块的方法和变量,并且它还是唯一导出的出口。这里的module是一个对象,表示模块本身,而exports就是它的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性就能导出。然后在另一个文件中,通过require()方法引入模块后,就能调用定义的属性和方法了。

模块标识:它是传递给requrie()方法的参数,它必须是符合小驼峰命名的字符串,或者是以...开头的相对路径,或者绝对路径,它可以没有文件名后缀.js

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;
})

喜欢本文请扫下方二维码,关注微信公众号: 前端小二,查看更多我写的文章哦,多谢支持。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值