Node
的目标是成为一个构建快速、可伸缩的网络应用平台,通过通信协议来组织许多Node,非常容易通过扩展来达成构建大型网络应用的目的。
Node
作为后端JavaScript的运行平台,保留了前端浏览器JavaScript中那些熟悉的接口,没有改写语言本身的任何特性,依旧基于作用域和原型链,区别在于它将前端中广泛运用的思想迁移到服务器端。
Node
支持异步I/O、事件与回调函数、单线程,并且跨平台。
基于以上支持点,Node
擅于应用的场景包括:I/O密集型、CPU密集型、分布式应用。
Node
使用模块化来组织JS代码,模块规范采用CommonJS规范。
对于JavaScript语言本身来说,有几个方面的天然缺陷:
在ES6中模块之前,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
中,也不会完全套用CommonJS规范的,而是有一定取舍,也增加些新的特性。对于module.exports
、require()
,Node
实现起来主要有三个步骤:路径分析、文件定位和编译执行。
Node
中的模块包含两种:一类是由Node提供的模块叫核心模块;一类是用户编写的模块叫文件模块。
其中核心模块在Node源代码的编译过程中,编译进了二进制执行文件。Node进程启动时核心模块就直接加载进内存中,所以当其被引入时,直接省去文件定位和编译执行两步,并且在路径分析中优先判断,所以其加载速度最快。
而文件模块则是在运行时动态加载,需要执行完整三步,所以加载速度略慢。
Node通常优先从缓存中加载,不管要加载的是核心模块还是文件模块。区别仅在于核心模块的缓存检查先于文件模块的缓存检查。
在路径分析中,Node会基于require()方法中的模块标识符进行模块查找。模块标识符主要有以下几类:
http
、fs
、path
等,加载速度最快.
或..
开始的相对路径文件模块/
开始的绝对路径文件模块connect
模块在文件定位中,首先会按照缓存加载的优化策略加载二次引入的模块,否则就按照首次加载策略执行文件定位。
最后就是编译执行阶段。当定位到具体文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也不同:
.js
文件。通过fs
模块同步读取文件后编译执行.node
文件。这是用C/C++编写的扩展文件,通过dlopen()
方法加载最后编译生成的文件.json
文件。通过fs
模块同步读取文件后,用JSON.parse()
解析返回结果这里补充下核心模块相关。核心模块又分为JavaScript核心模块和C/C++核心模块,后者又被称为内建模块。核心模块中有的模块全部由C/C++编写,部分是由C/C++完成核心部分,其他部分则由JavaScript实现包装或向外导出,以满足性能平衡需求。Node中的os
、fs
、buffer
等都是部分通过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;
})
喜欢本文请扫下方二维码,关注微信公众号: 前端小二,查看更多我写的文章哦,多谢支持。