Node基础入门
Node功能强大,特别是它能在浏览器以外运行JavaScript。
Node.js是什么?
官网上(http://www.nodejs.org )给Node下的定义是:一个搭建在Chrome JavaScript运行时上的平台,用于构建高速、可伸缩的网络程序。Node.js采用的事件驱动、非阻塞I/O模型,使它既轻量又高效,并成为构建运行在分布式设备上的数据密集型实时程序的完美选择。
在服务器端编程,Node使用的是为Google Chrome提供动力的V8虚拟机。V8让Node在性能上得到了巨大的提升,因为它去掉了中间环节,执行的不是字节码,用的也不是解释器,而是直接编译成了本地机器码。Node在服务器端使用JavaScript还有其他好处。
- 开发人员用一种语言就能编写整个Web应用,这可以减少开发客户端和服务端时所需的语言切换。这样代码可以在客户端和服务端中共享,比如在表单校验或游戏逻辑中使用同样一段代码。
- JSON是目前非常流行的数据交换格式,并且还是JavaScript原生的。
- 有些NoSQL数据库中用的就是JavaScript语言(比如CouchDB和MongoDB),所以跟它们简直是天作之合(比如MongoDB的管理和查询语言都是JavaScript;CouchDB的map/reduce也是JavaScript)。
- JavaScript是一门编译目标语言,现在有很多可以编译成JavaScript的语言5 。
- Node用的虚拟机(V8)会紧跟ECMAScript标准。6 换句话说,在Node中如果想用新的JavaScript语言特性,不用等到所有浏览器都支持。
Hello World HTTP服务器
下面是个简单的HTTP服务器实现,它会用“Hello World”响应所有请求:
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(3000);
console.log('Server running at http://localhost:3000/');
只要有请求过来,它就会激发回调函数function (req, res) ,把“Hello World”写入到响应中返回去。这个事件模型跟浏览器中对onclick 事件的监听类似。在浏览器中,点击事件随时都可能发生,所以要设置一个函数来执行对事件的处理逻辑,而Node在这里提供了一个可以随时响应请求的函数。
下面是同一服务器的另一种写法,这样看起来request 事件更明显:
var http = require('http');
var server = http.createServer();
server.on('request', function (req, res) { //为request设置一个事件监听器
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
})
server.listen(3000);
console.log('Server running at http://localhost:3000/');
流数据
Node在数据流和数据流动上也很强大。你可以把数据流看成特殊的数组,只不过数组中的数据分散在空间上,而数据流中的数据是分散在时间上 的。通过将数据一块一块地传送,开发人员可以每收到一块数据就开始处理,而不用等所有数据都到全了再做处理。下面我们用数据流的方式来处理resource.json:
var stream = fs.createReadStream('./resource.json')
stream.on('data', function (chunk) { //当有新的数据块准备好时会激发data事件
console.log(chunk)
})
stream.on('end', function () {
console.log('finished')
})
只要有新的数据块准备好,就会激发data 事件,当所有数据块都加载完之后,会激发一个end 事件。由于数据类型不同,数据块的大小可能会发生变化。有了对读取流的底层访问,程序就可以边读取边处理,这要比等着所有数据都缓存到内存中再处理效率高得多。
Node中也有可写数据流,可以往里写数据块。当HTTP服务器上有请求过来时,对其进行响应的res 对象就是可写数据流的一种。
可读和可写数据流可以连接起来形成管道,就像shell脚本中用的 | (管道)操作符一样。这是一种高效的数据处理方式,只要有数据准备好就可以处理,不用等着读取完整个资源再把它写出去。 |
我们借用一下前面那个HTTP服务器,看看如何把一张图片流到客户端:
var http = require('http');
var fs = require('fs');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'image/png'});
fs.createReadStream('./image.png').pipe(res); //设置一个从读取流到写出流的管道
}).listen(3000);
console.log('Server running at http://localhost:3000/');
在这行代码中,数据从文件中读进来(fs.createReadStream ),然后数据随着进来就被送到 (.pipe )客户端(res )。在数据流动时,事件轮询还能处理其他事件。
Node在多个平台上均默认提供了DIRT方式,包括各种Windows和类UNIX系统。底层的I/O库(libuv)特意屏蔽了宿主操作系统的差异性,提供了统一的使用方式,如果需要的话,程序可以在多个设备上轻松移植和运行。
Node编程基础
用模块组织代码
Node模块打包代码是为了重用,但它们不会改变全局作用域。Node模块允许你从被引入文件中选择要暴露给程序的函数和变量。如果模块返回的函数或变量不止一个,那它可以通过设定exports 对象的属性来指明它们。但如果模块只返回一个函数或变量,则可以设定module.exports 属性。
模块既可能是一个文件,也可能是包含一个或多个文件的目录。如果模块是个目录,Node通常会在这个目录下找一个叫index.js的文件作为模块的入口。
典型的模块是一个包含exports 对象属性定义的文件,这些属性可以是任意类型的数据,比如字符串、对象和函数。
定义一个Node模块
var canadianDollar = 0.91;
function roundTwoDecimals(amount) {
return Math.round(amount * 100) / 100;
}
exports.canadianToUS = function(canadian) { //canadianToUS函数设定在exports模块中,所以引入这个模块的代码可以使用它
return roundTwoDecimals(canadian * canadianDollar);
}
exports.USToCanadian = function(us) { //USToCanadian也设定在exports模块中
return roundTwoDecimals(us / canadianDollar);
}
exports 对象上只设定了两个属性。也就是说引入这个模块的代码只能访问到canadianToUS 和USToCanadian 这两个函数。而变量canadianDollar 作为私有变量仅作用在canadianToUS 和USToCanadian 的逻辑内部,程序不能直接访问它。
使用这个新模块要用到Node的require 函数,该函数以你要用的模块的路径为参数。Node以同步的方式寻找它,定位到这个模块并加载文件中的内容。
引入一个模块:下面这个是test-currency.js 中的代码,它require 了currency.js模块:
var currency = require('./currency'); //用路径./表明模块跟程序脚本放在同一目录下
console.log('50 Canadian dollars equals this amount of US dollars:');
console.log(currency.canadianToUS(50)); //使用currency模块的 canadianToUS函数
console.log('30 US dollars equals this amount of Canadian dollars:');
console.log(currency.USToCanadian(30)); //使用currency模块的USToCanadian函数
引入一个以./ 开头的模块意味着,如果你准备创建的程序脚本test-currency.js在currency_app目录下,那你的currency.js模块文件
在Node定位到并计算好你的模块之后,require 函数会返回这个模块中定义的exports 对象中的内容,然后你就可以用这个模块中的两个函数做货币转换了。
如果你想把这个模块放到子目录中,比如lib ,只要把require 语句改成下面这样就可以了:
var currency = require('./lib/currency');
组装模块中的exports 对象是在单独的文件中组织可重用代码的一种简便方法。
Node中有一个独特的模块引入机制,可以不必知道模块在文件系统中的具体位置。这个机制就是使用node_modules目录。
尽管Node模块系统的本质简单直接,但还是有两点需要注意一下。
第一,如果模块是目录,在模块目录中定义模块的文件必须被命名为index.js,除非你在这个目录下一个叫package.json的文件里特别指明。要指定一个取代index.js的文件,package.json文件里必须有一个用JavaScript对象表示法(JSON)数据定义的对象,其中有一个名为main 的键,指明模块目录内主文件的路径。 这里有个 package.json 文件的例子,它指定currency.js为主文件:
{
"main": "./currency.js"
}
还有一点需要注意的是,Node能把模块作为对象缓存起来。如果程序中的两个文件引入了相同的模块,第一个文件会把模块返回的数据存到程序的内存中,这样第二个文件就不用再去访问和计算模块的源文件了。实际上第二个引入有机会修改缓存的数据。这种“猴子补丁”(monkey patching)让一个模块可以改变另一个模块的行为,开发人员可以不用创建它的新版本。
Node异步编程
在Node的世界里流行两种响应逻辑管理方式:回调和事件监听。
回调 通常用来定义一次性响应的逻辑。比如对于数据库查询,可以指定一个回调函数来确定如何处理查询结果。这个回调函数可能会显示数据库查询结果,根据这些结果做些计算,或者以查询结果为参数执行另一个回调函数。
事件监听器 ,本质上也是一个回调,不同的是,它跟一个概念实体(事件)相关联。例如,当有人在浏览器中点击鼠标时,鼠标点击是一个需要处理的事件。在Node中,当有HTTP请求过来时,HTTP服务器会发出一个请求事件。你可以监听那个请求事件,并添加一些响应逻辑。
在下面这个例子中,每当有请求事件发出时,服务器就会调用handleRequest 函数:
server.on('request', handleRequest)
一个Node HTTP服务器实例就是一个事件发射器,一个可以继承、能够添加事件发射及处理能力的类(EventEmitter )。Node的很多核心功能都继承自EventEmitter ,你也能创建自己的事件发射器。
回调 是一个函数,它被当做参数传给异步函数,它描述了异步操作完成之后要做什么。回调在Node开发中用得很频繁,比事件发射器用得多,并且用起来也很简单。
事件发射器会触发事件,并且在那些事件被触发时能处理它们。一些重要的Node API组件,比如HTTP服务器、TCP服务器和流,都被做成了事件发射器。你也可以创建自己的事件发射器。
我们之前说过,事件是通过监听器进行处理的。监听器是跟事件相关联的,带有一个事件出现时就会被触发的回调函数。比如Node中的TCP socket,它有一个data 事件,每当socket中有新数据时就会触发:
socket.on('data', handleData);