nodejs详解

目录

javascript与node.js

javascript与你

由于javascript真正意义上有两种,甚至可以说是三种形态(从最早的作为DHTML进行增强的小工具,到像jQuery那样严格意义上的前端技术,再到现在的服务端技术),因此,比较难找到一个”正确“的方式来学习JavaScript,使得让你书写Node.js应用的时候感觉自己是在真正开发它而不仅仅是使用它。

简短声明

这是一个”从初级入门到高级入门“的介绍资料。

服务器端javascript

javascript最早是运行在浏览器中,然而浏览器只是提供了一个上下文,它定义了使用javascript可以做什么,但并没有”说“太多关于JavaScript语言本身可以做什么。事实上,javascript是一门”完整“的语言:它可以使用在不同的上下文中,其能力与其它同类语言相比有过之而无不及。

Node.js事实上就是另外一种上下文,它允许在后端(脱离浏览器环境)运行JavaScript。

要实现在后台运行javascript代码,代码需要先被解释然后正确的执行。Node.js的原理正是如此,它使用了Google的V8虚拟机(Google的Chrome浏览器使用的JavaScript执行环境),来解释和执行JavaScript代码。

除此之外,伴随着Node.js的还有许多有用的模块,它们可以简化很多重复的劳作,比如向终端输出字段串。

因此,Node.js事实上既是一个运行时环境,同时又是一个库。

要使用Node.js,首先需要进行安装。

 "hello world"

创建一个helloworld.js文件。

写入console.log("Hello World");

保存该文件,通过Node.js来执行

node helloworld.js

终端输出Hello World

一个完整的基于Node.js的web应用

用例

我们来把目标设定简单点,不过也要够实际才行:

用户可以通过浏览器使用我们的应用。

当用户请求http://domain/start时,可以看到一个欢迎页面,页面上有一个文件上传的表单。

用户可以选择一个图片并提交表单,随后文件将被上传到http://domain/upload,该页面完成上传后会把图片显示在页面上。

更进一步地说,在完成这一目标的过程中,我们不仅仅需要基础的代码而不管代码是否优雅。我们还要对此进行抽象,来寻找一种适合构建更为复杂的Node.js应用的方式。

应用不同模块分析

我们来分解一下这个应用,为了实现上文的用例,我们需要实现哪些部分呢?

我们需要提供Web页面,因此需要一个HTTP服务器

对于不同的请求,根据请求的URL,我们的服务器需要给予不同的响应,因此我们需要一个路由,用户把请求对应到请求处理程序(request handler)

路由还应该能处理POST数据,并且把数据封装成更友好的格式传递给请求处理入程序,因此需要请求数据处理功能

我们不仅仅要处理URL对应的请求,还要把内容显示出来,这意味着我们需要一些视图逻辑供请求处理程序使用,以便将内容发送给用户的浏览器

最后,用户需要上传图片,所以我们需要上传处理功能来处理这方面的细节

我们先来想想,使用PHP的话我们会怎么构建这个结构。一般来说我们会用一个Apache HTTP服务器并配上mod_php5模块。

从这个角度看,整个“接收HTTP请求并提供Web页面”的需求根本不需要PHP来处理。

不过对Node.js来说,概念完全不一样了。使用Node.js时,我们不仅仅在实现一个应用,同时还实现了整个HTTP服务器。事实上,我们的Web应用以及对应的Web服务器基本上是一样的。

现在开始从HTTP服务器着手。

构建应用的模块

一个基础的HTTP服务器

    讨论把所有东西放进一个文件里吗?为了保持代码的可读性,把不同功能的代码放入不同的模块中,保持代码分离。

这种方法允许你拥有一个干净的主文件(main file),你可以用Node.js执行它;同时你可以拥有干净的模块,它们可以被主文件和其它的模块调用。

那么,现在我们来创建一个用于启动我们的应用的主文件,和一个保持着我们HTTP服务器代码的模块。

把主文件叫做index.js或多或少是个标准格式。把服务器模块放进叫server.js的文件里则很好理解。

让我们先从服务器模块开始。在你的项目的根目录下创建一个叫server.js的文件,并写入以下代码:

var http = require("http");

http.createServer(function(request, response){

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("Hello World");

response.end();

}).listen(8888);

搞定!你刚刚完成了一个可以工作的HTTP服务器。为了证明这一点,我们来运行并且测试这段代码。首先,用Node.js执行你的脚本:

node server.js

接下来,打开浏览器访问http://localhost:8888/,你会看到一个写着"Hello World"的网页。

这很有趣,不是吗?让我们先来谈谈HTTP服务器的问题,把如何组织项目的事情先放一边吧,你觉得如何?我保证之后我们会解决那个问题的。

分析HTTP服务器

那么接下来,让我们分析一下这个HTTP服务器的构成。

第一行请求(require)Node.js自带的http模块,并且把它赋值给http变量。

接下来我们调用http模块提供的函数:createServer。这个函数会返回一个对象,这个对象有一个叫做listen的方法,这个方法有一个数值参数,指定这个HTTP服务器监听的端口号。

咱们暂时先不管http.createServer的括号里的那个函数定义。

我们本来可以用这样的代码来启动服务器并侦听8888端口:

var http = require("http");

var server = http.createServer();

server.listen(8888);

这段代码只会启动一个侦听8888端口的服务器,它不做任何别的事情,甚至连请求都不会应答。

最有趣(而且,如果你之前习惯使用一个更加保守的语言,比如PHP,它还很奇怪)的部分是createServer()的第一个参数,一个函数定义。

实际上,这个函数定义是createServer()的第一个也是唯一一个参数。因为在JavaScript中,函数和其他变量一样都是可以被传递的。

进行函数传递

举例来说,你可以这样做:

function say(word){

console.log(word);

}

function execute(someFunction, value){

someFunction(value);

}

execute(say, "Hello");

请仔细阅读这段代码!在这里,我们把say函数作为execute函数的第一个变量进行了传递。这里返回的不是say的返回值,而是say本身!

这样一来,say就变成了execute中的本地变量someFunction,execute可以通过调用someFunction()(带括号的形式)来使用say函数。

当然,因为say有一个变量,execute在调用someFunction时可以传递这样一个变量。

我们可以,就像刚才那样,用它的名字把一个函数作为变量传递。但是我们不一定要绕这个“先定义,再传递”的圈子,我们可以直接在另一个函数的括号中定义和传递这个函数:

function execute(someFunction, value){

someFunction(value);

}

execute(function(word){

console.log(word);

}, "Hello");

我们在ececute接受第一个参数的地方直接定义了我们准备传递给execute的函数。

用这种方式,我们甚至不用给这个函数起名字,这也是为什么它被叫做匿名函数。

这是我们和我所认为的“进阶”JavaScript的第一次亲密接触,不过我们还是得循序渐进。现在,我们先接受这一点:在JavaScript中,一个函数可以作为另一个函数接收一个参数。我们可以先定义一个函数,然后传递,也可以在传递参数的地方直接定义函数。

函数传递是如何让HTTP服务器工作的

带着这些知识,我们再来看看我们简约而不简单的HTTP服务器:

var http = require("http");

http.createServer(function(){

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("Hello World");

response.end();

}).listen(8888);

现在它看上去应该清晰了很多:我们想createServer函数传递了一个匿名函数。

用这样的代码也可以达到同样的目的:

var http = require("http");

function onRequest(request, response){

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("Hello World");

response.end();

}

http.createServer(onRequest).listen(8888);

也许现在我们该问这个问题了:我们为什么要用这种方式呢?

基于事件驱动的回调

这个问题可不好回答(至少对我来说),不过这是Node.js原生的工作方式。它是事件驱动的,这也是它为什么这么快的原因。

这一切都归结于"Node.js是事件驱动的"这一事实。好吧,其实我也不是特别确切的了解这句话的意思。不过我会试着解释,为什么它对我们用Node.js写网络应用(Web based application)是有意义的。

当我们使用http.createServer方法的时候,我们当然不只是想要一个侦听某个端口的服务器,我们还想要它在服务器收到一个HTTP请求的时候做点什么。

问题是,这是异步的:请求任何时候都可能到达,但是我们的服务器却跑在一个单进程中。

写PHP应用的时候,我们一点也不为此担心:任何时候当有请求进入的时候,网页服务器(通常是Apache)就为这一请求新建一个进程,并且开始从头到尾执行相应的PHP脚本。

那么在我们的Node.js程序中,当一个新的请求到达8888端口的时候,我们怎么控制流程呢?

嗯,这就是Node.js/JavaScript的事件驱动设计能够真正帮上忙的地方了--虽然我们还得学一些新概念才能掌握它。让我们来看看这些概念是怎么应用在我们的服务器代码里的。

我们创建了服务器,并且向创建它的方法传递了一个函数。无论何时我们的服务器收到一个请求,这个函数就会被调用。

我们不知道这件事情什么时候会发生,但是我们现在有了一个处理请求的地方:它就是我们传递过去的那个函数。至于它是被预先定义的函数还是匿名函数,就无关紧要了。

这个就是传说中的回调。我们给某个方法传递了一个函数,这个方法在有相应事件发生时调用这个函数来进行回调。

至少对我来说,需要一些功夫才能弄懂它。

让我们再来琢磨琢磨这个新概念。我们怎么证明,在创建完服务器之后,即使没有HTTP请求进来、我们的回调函数也没有被调用的情况下,我们的代码还继续有效吗?我们试试这个:

var http = require("http");

function onRequest(request, response){

console.log("Request received.");

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("Hello World");

response.end();

}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");

注意:在onRequest(我们的回调函数)触发的地方,我用console.log输出了一段文本。在HTTP服务器开始工作之后,也输出一段文本。

当我们与往常一样,运行它node server.js时,它会马上在命令行上输出"Server has started."。当我们向服务器发出请求(在浏览器访问http://localhost:8888/),"Request received."这条消息就会在命令行中出现。

这就是事件驱动的异步服务器端JavaScript和它的回调啦!

(请注意,当我们在服务器访问网页时,我们的服务器可能会输出两次"Request received."。那是因为大部分服务器都会在你访问http://localhost:8888/时尝试读取http://localhost:8888/favicon.ico)

服务器是如何处理请求的

好的,接下来我们简单分析一下我们服务器代码中剩下的部分,也就是我们的回调函数onRequest()的主体部分。

当回调启动,我们的onRequest()函数被触发的时候,有两个参数被传入:

request和response。

它们是对象,你可以使用它们的方法来处理HTTP请求的细节,并且响应请求(比如向发出请求的浏览器发回一些东西)。

所以我们的代码就是:当收到请求时,使用response.writeHead()函数发送一个HTTP状态200和HTTP头的内容类型(content-type),使用response.write()函数在HTTP相应主体中发送文本“Hello World”。

最后,我们调用response.end()完成响应。

目前来说,我们对请求的细节并不在意,所以我们没有使用request对象。

服务端的模块放在哪里

我们现在在server.js文件中有一个非常基础的HTTP服务器代码,而且我提到通常我们会有一个叫index.js的文件去调用应用的其他模块(比如server.js中的HTTP服务器模块)来引导和启动应用。

现在看看怎么把server.js变成一个真正的Node.js模块,使它可以被我们(还没动工)的index.js主文件使用。

我们已经在代码中使用了模块。像这样:

var http = require("http");

...

http.createServer(...);

Node.js中自带了一个叫做"http"的模块,我们在我们的代码中请求它并把返回值赋给一个本地变量。

这把我们的本地变量变成了一个拥有所有http模块所提供的公共方法的对象。

给这种本地变量起一个和模块名称一样的名字是一种惯例,但是你也可以按照自己的喜好来:

事实上,我们不用做太多的修改。把某段代码变成模块意味着我们需要把我们希望提供其功能的部分导出到请求这个模块的脚本。

目前,我们的HTTP服务器需要导出的功能非常简单,因为请求服务器模块的脚本仅仅是需要启动服务器而已。

我们把我们的服务器脚本放到一个叫做start的函数里,然后我们会导出这个函数。

var http = require("http");

function start(){

function onRequest(request, response){

console.log("Request received.");

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("Hello World");

response.end();

}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");

}

exports.start = start;

这样,我们现在就可以创建我们的主文件index.js并在其中启动我们的HTTP了,虽然服务器的代码还在server.js中。

创建index.js文件并写入以下内容:

var server = require("./server");

server.start();

正如你所看到的,我们可以像使用任何其它的内置模块一样使用server模块:请求这个文件并把它指向一个变量,其中已导出的函数就可以被我们使用了。

我们现在就可以从我们的主要脚本启动我们的应用,而它还是老样子:

node index.js

我们现在可以把我们的应用的不同部分放入不同的文件里,并且通过生成模块的方式把它们连接在一起了。

我们仍然只拥有整个应用的最初部分:我们可以接收HTTP请求。但是我们得做点什么--对于不同的URL请求,服务器应该有不同的反应。

对于一个非常简单的应用来说,你可以直接在回调函数onRequest()中做这件事情。不过就像我说过的,我们应该加入一些抽象的元素,让我们的例子变得更有趣一点儿。

处理不同的HTTP请求在我们的代码中是一个不同的部分,叫做“路由选择”--那么,我们接下来就创造一个叫做路由的模块。

如何来进行请求的"路由"

我们要为路由提供请求的URL和其它需要的GET及POST参数,随后路由需要根据这些数据来执行相应的代码(这里“代码”对应整个应用的第三部分:一系列在接收到请求时真正工作的处理程序)。

我们需要的所有数据都会包含在request对象中,该对象作为onRequest()回调函数的第一个参数传递。但是为了解析这些数据,我们需要额外的Node.js模块,它们分别是url和querystring模块。

当然我们也可以用querystring模块来解析POST请求体中的参数,稍后会有演示。

现在我们来给onRequest()函数加上一些逻辑,用来找出浏览器请求的URL路径:

var http = require("http");

var url = require("url");

function start(){

function onRequest(request, response){

var pathname = url.parse(request.url).pathname;

console.log("Request for " + pathname + " received.");

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("Hello World");

response.end();

}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");

}

exports.start = start;

我们的应用现在可以通过请求的URL路径来区别不同请求了-这使我们得以使用路由(还未完成)来将请求以URL路径为基准映射到处理程序上。

在我们所要构建的应用中,这意味着来自/start和/upload的请求可以使用不同的代码来处理。稍后我们将看到这些内容是如何整合到一起的。

现在我们可以来编写路由,建立一个名为router.js的文件,添加以下内容:

function route(pathname){

console.log("About to route a request for " + pathname);

}

exports.route = route;

这段代码貌似什么也没干,不过对于现在来说这是应该的。在添加更多的逻辑以前,我们先来看看如何把路由和服务器整合起来。

我们的服务器应当知道路由的存在并加以有效利用。当然可以通过硬编码的方式将这一依赖绑定到服务器上,但是其它语言的编程经验告诉我们这会是一件非常痛苦的事,因此我们将使用依赖注入的方式较松散地添加路由模块。

首先,我们来扩展一下服务器的start()函数,以便将路由函数作为参数传递过去:

var http = require("http");

var url = require("url");

function start(route){

function onRequest(request, response){

var pathname = url.parse(request.url).pathname;

console.log("Request for " + pathname + " received.");

route(pathname);

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("Hello World");

response.end();

}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");

}

exports.start = start;

同时,我们会相应扩展index.js,使得路由函数可以被注入到服务器中:

var server = require("./server");

var router = require("./router");

server.start(router.route);

在这里,我们传递的函数依旧什么也没做。

如果现在启动应用(node index.js,始终记得这个命令行),随后请求一个URL,你将会看到应用输出相应的信息,这表明我们的HTTP服务器已经在使用路由模块了,并会将请求的路径传递给路由:

node index.js

Request for /foo received.

About to route a request for /foo

以上输出已经去掉了favicon.icon请求相关部分。

行为驱动执行

在index文件中,我们可以将router对象传递进去,服务器随后可以调用这个对象的route函数。

就像这样,我们传递一个东西,然后服务器利用这个东西来完成一些事。

但是服务器其实不需要这样的东西。它只需要把事情做完就行,其实为了把事情做完,你根本不需要东西,你需要的是动作。

路由给真正的请求处理程序

路由,是指我们要针对不同的URL有不同的处理方式。例如处理/start的“业务逻辑”就应该和处理/upload的不同。

在现在的实现下,路由过程会在路由模块中“结束”,并且路由模块并不是真正针对请求“采取行动”的模块,否则当我们的应用程序变得更为复杂时,将无法很好地扩展。

我们暂时把作为路由目标的函数称为请求处理程序。现在我们不要急着来开发路由模块,因为如果请求处理没有就绪的话,再怎么完善路由模块也没有多大意义。

应用程序需要新的部件,我们来创建一个叫做requestHandlers的模块,并对于每一个请求处理程序,添加一个占位用函数,随后将这些函数作为模块的方法导出:

function start(){

console.log("Request handler ‘start‘ was called.");

}

function upload(){

console.log("Request handler ‘upload‘ was called.");

}

exports.start = start;

exports.upload = upload;

这样我们就可以把请求处理程序和路由模块连接起来,让路由“有路可寻”。

在这里我们得做个决定:是将requestHandlers模块硬编码到路由里来使用,还是再添加一点依赖注入?虽然和其它模式一样,依赖注入不应该仅仅为使用而使用,但在现在这个情况下,使用依赖注入可以让路由和请求处理程序之间的耦合更加松散,也因此能让路由的重用性更高。

这意味着我们得将请求处理程序从服务器传递到路由中,但感觉上这么做更离谱了,我们得一路把这堆请求处理程序从我们的主文件传递到服务器中,再将之从服务器传递到路由。

那么我们要怎么传递这些请求处理程序呢?别看现在我们只有2个处理程序,在一个真实的应用中,请求处理程序的数量会不断增加,我们当然不想每次有一个新的URL或请求处理程序时,要为了在路由里完成请求到处理程序的映射而反复折腾。除此之外,在路由里有一大堆 if request == x then call handle y也使得系统丑陋不堪。

仔细想想,有一大堆东西,每个都要映射到一个字符串(就是请求的URL)上?似乎关联数组(associative array)能完美胜任。

不过结果有点令人失望,JavaScript没提供关联数组--也可以说它提供了?事实上,在JavaScript总,真正能提供此类功能的是它的对象。

在这方面,msdn提供了一段摘要:

在C++或C#中,当我们谈到对象,指的是类或者结构体的示例。对象根据他们实例化的模板(就是所谓的类),会拥有不同的属性的和方法。但在JavaScript里对象不是这个概念。在JavaScript中,对象就是一个键值对的集合,你可以把JavaScript的对象想象成一个键为字符串类型的字典。

但如果JavaScript的对象仅仅是键值对的集合,它又怎么会拥有方法呢?

现在我们已经确定将一系列请求处理程序通过一个对象来传递,并且需要松耦合的方式将这个对象注入到route()函数中。

我们先将这个对象引入到主文件index.js中:

var server = require("./server");

var router = require("./router");

var requestHandlers = require("./requestHandlers");

var handle = {};

handle["/"] = requestHandlers.start;

handle["/start"] = requestHandlers.start;

handle["/upload"] = requestHandlers.upload;

server.start(router.route, handle);

虽然handle并不仅仅是一个”东西“(一些请求处理程序的集合),我还是建议以一个动词作为其命名,这样做可以让我们在路由中使用更流畅的表达式,稍后会有说明。

正如所见,将不同的URL映射到相同的请求处理程序上是很容易的:只要在对象中添加一个键为"/"的属性,对应requestHandlers.start即可,这样我们就可以干净简洁地配置/start和/的请求都交由start这一处理程序处理。

在完成了对象的定义后,我们把它作为额外的参数传递给服务器,为此将server.js修改如下:

var http = require("http");

var url = require("url");

function start(route, handle) {

function onRequest(request, response) {

var pathname = url.parse(request.url).pathname;

console.log("Request for " + pathname + " received.");

route(handle, pathname);

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("Hello World");

response.end();

}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");

}

exports.start = start;

这样我们就在start()函数里添加了handle参数,并且把handle对象作为第一个参数传递给了route()回调函数。

然后我们相应地在route.js文件中修改route()函数:

function route(handle, pathname) {

console.log("About to route a request for " + pathname);

if (typeof handle[pathname] === ‘function‘) {

handle[pathname]();

} else {

console.log("No request handler found for " + pathname);

}

}

exports.route = route;

通过以上代码,我们首先检查给定的路径对应的请求处理程序是否存在,如果存在的话直接调用相应的函数。我们可以用从关联数组中获取元素一样的方式从传递的对象中获取请求处理函数,因此就有了简洁流畅的形如handle[pathname]();的表达式。

有了这些,我们就把服务器、路由和请求处理程序在一起了。现在我们启动应用程序并在浏览器中访问http://localhost:8888/start,以下日志可以说明系统调用了正确的请求处理程序:

Server has started.

Request for /start received.

About to route a request for /start

Request handler ‘start‘ was called.

并且在浏览器中打开http://localhost:8888可以看到这个请求同样被start请求处理程序处理了:

Request for / received.

Aboute to route a request for /

Request handler ‘start‘ was called.

让请求处理程序作出响应

其实处理请求就是对请求做出响应,因此,我们需要让请求处理程序能够像onRequest函数那样可以和浏览器进行"对话"。

不好的实现方式

对于我们这样拥有PHP或者Ruby技术背景的开发者来说,最直截了当的实现方式事实上并不是非常靠谱:看似有效,实则未必如此。

这里按直截了当的实现方式,让请求处理程序通过onRequest函数直接返回(return())他们要展示给用户的信息。

让我们从让请求处理程序返回需要在浏览器中显示的信息开始。我们需要将requestHandler.js修改为如下形式:

function start() {

console.log("Request handler ‘start‘ was called.");

return "Hello Start";

}

function upload() {

console.log("Request handler ‘upload‘ was called.");

return "Hello Upload";

}

exports.start = start;

exports.upload = upload;

好的。同样的,请求路由需要将请求处理程序返回给它的信息返回给服务器。因此,我们需要将router.js修改为如下形式:

function route(handle, pathname) {

console.log("About to route a request for " + pathname);

if (typeof handle[pathname] === ‘function‘) {

return handle[pathname]();

} else {

console.log("No request handler found for " + pathname);

return "404 Not found";

}

}

exports.route = route;

正如上述代码所示,当请求无法路由的时候,我们也返回了一些相关的错误信息。

最后,我们需要对我们的server.js进行重构以使得它能够将请求处理程序通过请求路由返回的内容响应给浏览器,如下所示:

var http = require("http");

var url = require("url");

function start(route, handle) {

function onRequest(request, response) {

var pathname = url.parse(request.url).pathname;

console.log("Request for " + pathname + " received.");

response.writeHead(200, {"Content-Type": "text/plain"});

var content = route(handle, pathname);

response.write(content);

response.end();

}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");

}

exports.start = start;

如果我们运行重构后的应用,一切都会工作很好

http://localhost:8888/start,浏览器会输出Hello Start

http://localhost:8888/upload,浏览器会输出Hello Upload

http://localhost:8888/foo 会输出404 Not Found

当有请求处理程序需要进行非阻塞的操作时候,我们的应用就"挂"了。

阻塞与非阻塞

正如此前所提到的,当在请求处理程序中包括非阻塞操作时就会出问题。但是,在说这之前,我们先看看什么是阻塞操作。

这里,我们来修改下start请求处理程序,我们让它等待10秒以后再返回"Hello Start"。因为,JavaScript中没有类似sleep()这样的操作,所以这里只能够来点小Hack来模拟实现。

让我们将requestHandlers.js修改成如下形式:

function start() {

console.log("Request handler ‘start‘ was called.");

function sleep(milliSeconds) {

var startTime = new Date().getTime();

while (new Date().getTime() < startTime + milliSeconds);

}

sleep(10000);

return "Hello Start";

}

function upload() {

console.log("Request handler ‘upload‘ was called.");

return "Hello Upload";

}

exports.start = start;

exports.upload = upload;

上述代码中,当函数start()被调用时,Node.js会先等待10秒,之后才会返回"hello Start"。当调用upload()的时候,会和此前一样立即返回。

(当然了,这里只是模拟休眠10秒,实际场景中,这样的阻塞操作有很多,比方说一些长时间的计算操作等。)

接下来就让我们来看看,我们的改动带来了哪些变化。

如往常一样,我们先要重启下服务器。为了看到效果,我们要进行一些相对复杂的操作:首先,打开两个浏览器窗口或者标签页。在第一个浏览器窗口的地址栏中输入http://localhost:8888/start,但是先不要打开它!

在第二个浏览器窗口的地址栏中输入http://localhost:8888/upload,同样的,先不要打开它!

接下来,做如下操作:在第一个窗口中("/start")按下回车,然后快速切换到第二个窗口中("/upload")按下回车。

注意,发生了什么:/start URL加载花了10秒,这和我们预期的一样。但是,/upload URL居然也花了10秒,而它在对应的请求处理程序中并没有类似于sleep()这样的操作!

这到底是为什么呢?原因就是start()包含了阻塞操作。形象的说就是“它阻塞了所有其他的处理工作”。

这显然是个问题,因为Node一向是这样来标榜自己的:“在node中除了代码,所有一切都是并行执行的”。

这句话的意思是说,Node.js可以在不新增额外线程的情况下,依然可以对任务进行并行处理--Node.js是单线程的

。它通过事件轮询(event loop)来实现并行操作,对此,我们应该要充分利用这一点--尽可能的避免阻塞操作,取而代之,多使用非阻塞操作。

然而,要用非阻塞操作,我们需要使用回调,通过将函数作为参数传递给其它需要花时间做处理的函数(比方说,休眠10秒,或者查询数据数据库,又或者是进行大量的计算)。

对于Node.js来说,它是这样处理的:“probablyExpensiveFunction()”(这里指的就是需要花时间处理的函数),你继续处理你的事情,我(Node.js线程)先不等你了,我继续去处理你后面的代码,请你提供一个callbackFunction(),等你处理完之后我会去调用该回调函数的。

接下来,我们会介绍一种错误的使用非阻塞操作的方式。

和上次一样,我们通过修改我们的应用来暴露问题。

这次我们还是拿start请求处理程序来“开刀”。将起修改成如下形式:

var exec = require("child_process").exec();

function start() {

console.log("Request handler ‘start‘ was called.");

var content = "empty";

exec("ls -lah", function(error, stdout, stderr) {

content = stdout;

});

return content;

}

function upload() {

console.log("Request handler ‘upload‘ was called.");

return "Hello Upload";

}

exports.start = start;

exports.upload = upload;

上述代码中,我们引入了一个新的Node.js模块,child_process。之所以用它,是为了实现一个既简单又使用的非阻塞操作:exec()。

exec()做了什么呢?它从Node.js来执行一个shell命令。在上述例子中,我们用它来获取当前目录下所有的文件("ls -lah"),然后,当/startURL请求的时候将文件信息输出到浏览器中。

上述代码是非常直观的:创建了一个新的变量content(初始值为"empty"),执行"ls -lah"命令,将结果赋值给content,最后将content返回。

和往常一样,我们启动服务器,然后访问"http://localhost:8888/start"。

之后会载入一个漂亮的web页面,其内容为"empty"。怎么回事?

这个时候,你可能大致已经猜到了,exec()在非阻塞这块发挥了神奇的功效。它其实是个很好的东西,有了它,我们可以执行非常耗时的shell操作而无须迫使我们的应用停下来等待该操作。

(如果想要证明这一点,可以将"ls -lah"换成比如"find /")这样更耗时的操作来效果)。

然而,针对浏览器显示的结果来看,我们并不满意我们的非阻塞操作,对吧?

好,接下来,我们来修正这个问题。在这过程中,让我们先来看看为什么当前的这种方式不起作用。

问题就在于,为了进行非阻塞工作,exec()使用了回调函数。

在我们的例子中,该回调函数就是作为第二个参数传递给exec()的匿名函数:

function (error, stdout, stderr) {

content = stdout;

}

现在就到了问题根源所在了:我们的代码是同步执行的,这就意味着在调用exec()之后,Node.js会立即执行return content;在这个时候,content仍然是"empty",因为传递给exec()的回调函数还未执行到--因为exec()的操作是异步的。

我们这里"ls -lah"的操作其实是非常快的(除非当前目录下有上百万个文件)。这也是为什么回调函数也会很快的执行到--不过,不管怎么说它还是异步的。

为了让效果更加明显,我们想象一个更耗时的命令:"find /",它在我机器上需要执行1分钟左右的时间,然后,尽管在请求处理程序中,我把"ls -lah"换成"find /",当打开/start URL的时候,依然能够立即获得HTTP响应--很明显,当exec()在后台执行的时候,Node.js自身会继续执行后面的代码。并且我们这里假设传递给exec()的回调函数,只会在"find
/"命令执行完成之后才会被调用。

那究竟我们要如何才能实现将当前目录下的文件列表显示给用户呢?

好,了解了这种不好的实现方式之后,我们接下来来介绍如何以正确的方式让请求处理程序对浏览器请求作出响应。

以非阻塞操作进行请求响应

我刚刚提到了这样一个短语--“正确的方式”。而事实上通常“正确的方式”一般都不简单。

不过,用Node.js就有这样一种实现方案:函数传递。下面就让我们来具体看看如何实现。

到目前为止,我们的应用已经可以通过应用各层之间传递值的方式(请求处理程序->请求路由->服务器)将请求处理程序返回的内容(请求处理程序最终要显示给用户的内容)传递给HTTP服务器。

现在我们采用如下这种新的实现方式:相对采用将内容传递给服务器的方式,我们这次采用将服务器"传递"给内容的方式。从实践角度来说,就是将response对象(从服务器的回调函数onRequest()获取)通过请求路由传递给请求处理程序。随后,处理程序就可以采用该对象上的函数来对请求作出响应。原理就是如此,接下来让我们来一步步实现这种方案。

先从server.js开始:

var http = require("http");

var url = require("url");

function start(route, handle) {

function onRequest(request, response) {

var pathname = url.parse(request.url).pathname;

console.log("Request for " + pathname + " received.");

route(handle, pathname, response);

}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");

}

exports.start = start;

相对此前从route()函数获取返回值的做法,这次我们将response对象作为第三个参数传递给route()函数,并且,我们将onRequest()处理程序中所有有关response的函数回调都移除,因为我们希望这部分工作让route()函数来完成。

下面就来看看我们的router.js:

function route(handle, pathname, response) {

console.log("About to route a request for " + pathname);

if (typeof handle[pathname] === ‘function‘) {

handle[pathname](response);

} else {

console.log("No request handler found for " + pathname);

response.writeHead(404, {"Content-Type": "text/plain"});

response.write("404 Not found");

response.end();

}

}

exports.route = route;

同样的模式:相对此前从请求处理程序中获取返回值,这次取而代之的是直接传递response对象。

如果没有对应的请求处理器处理,我们就直接返回"404"错误。

最后,我们将requestHandler.js修改为如下形式:

var exec = require("child_process").exec;

function start(response) {

console.log("Request handler ‘start‘ was called.");

exec("ls -lah", function(error, stdout, stderr) {

response.writeHead(200, {"Content-Type": "text/plain"});

response.write(stdout);

response.end();

});

}

function upload(response) {

console.log("Request handler ‘upload‘ was called.");

response.writeHead(200, {"Content-Type": "text/plian"});

response.write("Hello Upload");

response.end();

}

exports.start = start;

exports.upload = upload;

我们的处理程序函数需要接收response参数,为了对请求作出直接的响应。

start处理程序在exec()的匿名回调函数中做请求响应的操作,而upload处理程序仍然是简单的回复"Hello World",只是这次是使用response对象而已。

这时再次我们启动应用(node index.js),一切都会工作的很好。

如果想要证明/start处理程序中耗时的操作不会阻塞对/upload请求做出立即响应的话,可以将requestHandlers.js修改为如下形式:

var exec = require("child_process").exec;

function start(response) {

console.log("Request handler ‘start‘ was called.");

exec("find /",

{

timeout: 10000,

maxBuffer: 20000*1024

},

function(error, stdout, stderr) {

response.writeHead(200, {"Content-Type": "text/plain"});

response.write(stdout);

response.end();

}

);

}

function upload(response) {

console.log("Request handler ‘upload‘ was called.");

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("Hello Upload");

response.end();

}

exports.start = start;

exports.upload = upload;

这样一来,当请求http://localhost:8888/start的时候,会花10秒钟的时间才载入,而当请求http://localhost:8888/upload的矢耦,会立即响应,纵然这个时候/start响应还在处理中。

更有用的场景

到目前位置,我们做的已经很好了,但是,我们的应用没有实际用途。

服务器,请求路由以及请求处理程序都已经完成了,下面让我们按照此前的用例给网站添加交互:用户选择一个文件,上传该文件,然后在浏览器中看到上传的文件。为了保持简单,我们假设用户只会上传图片,然后我们应用将该图片显示到浏览器中。

好,下面就一步步来实现,鉴于此前已经对javascript原理性技术性的内容做过大量介绍了,这次我们加快点速度。

要实现该功能,分为如下两步:首先,让我们来看看如何处理POST请求(非文件上传),之后,我们使用Node.js的一个用于文件上传的外部模块。之所以采用这种实现方式有两个理由。

第一,尽管在Node.js中处理基础的POST请求相对比较简单,但在这过程中还是能学到很多。

第二,用Node.js来处理文件上传(multipart POST请求)是比较复杂的,它不在本书的范畴,但,如何使用外部模块却是在本书涉及内容之内。

处理POST请求

考虑这样一个简单的例子:我们显示一个文本区(textarea)供用户输入内容,然后通过POST请求提交给服务器。最后,服务器接受到请求,通过处理程序将输入的内容展示到浏览器中

/start请求处理程序用于生成带文本区的表单,因此,我们将requestHandlers.js修改为如下形式:

function start(response) {

console.log("Request handler ‘start‘ was called.");

var body = [

‘<html>‘,

‘<head>‘,

‘<meta http-equiv=\‘Content-Type\‘ content=\‘text/html;\‘ charset=\‘utf-8\‘ />‘,

‘</head>‘,

‘<body>‘,

‘<form action=\‘/upload\‘ method=\‘post\‘>‘,

‘<textarea name=\‘text\‘ rows=\‘20\‘ cols=\‘60\‘></textarea>‘,

‘<input type=\‘submit\‘ value=\‘Submit text\‘ />‘,

‘</form>‘,

‘</body>‘,

‘</html>‘

].join("");

response.writeHead(200, {"Content-Type": "text/html"});

response.write(body);

response.end();

}

function upload(response) {

console.log("Request handler ‘upload‘ was called.");

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("Hello Upload");

response.end();

}

exports.start = start;

exports.upload = upload;

现在可以通过http://localhost:8888/start就可以看到简单的表单,需要重启服务器。

接下探讨一个问题:当用户提交表单时,触发/upload请求处理程序处理POST请求的问题。

现在,采用异步回调来实现非阻塞地处理POST请求的数据。

这里采用非阻塞方式处理是明智的,因为POST请求一般都比较重--用户可能会输入大量的内容。用阻塞的方式处理大数据量的请求必然会导致用户操作的阻塞。

为了使整个过程非阻塞,Node.js会将POST数据拆分成很多小的数据块,然后通过触发特定的事件,将这些小数据块传递给回调函数。这里的特定的事件有data事件(表示新的小数据块到到达了)以及end事件(表示所有的数据都已经接收完毕)。

我们需要告诉Node.js当这些事件触发的时候,回调哪些函数。我们通过在request对象上注册监听器(listener)来实现。这里的request对象是每次接收到HTTP请求时候,都会把该对象传递给onRequest回调函数。

如下所示:

request.addListener("data", function(chunk) {

//called when a new chunk of data was received

});

request.addListener("end", function() {

//called when all chunks of data have been received

});

问题来了,这部分逻辑写在哪里?我们现在只是在服务器中获取到了request对象--我们并没有像之前response对象那样,把request对象传递给请求路由和请求处理程序。

在我看来,获取所有来自请求的数据,然后将这些数据给应用层处理,应该是HTTP服务器要做的事情。因此,我建议,我们直接在服务器中处理POST数据,然后将最终的数据传递给请求路由和请求处理器,让他们来进行进一步的处理。

因此,实现思路就是,将data和end事件的回调函数直接放在服务器中,在data事件回调中收集所有的POST数据,当接收到所有数据,触发end事件后,其回调函数调用请求路由,并将数据传递给它,然后,请求路由再将该数据传递给请求处理程序。

先从server.js开始:

var http = require("http");

var url = require("url");

function start(route, handle) {

function onRequest(request, response) {

var postData = "";

var pathname = url.parse(request.url).pathname;

console.log("Request for " + pathname + " received.");

request.setEncoding("utf8");

request.addListener("data", function(postDataChunk) {

postData += postDataChunk;

console.log("Received POST data chunk ‘" + postDataChunk + "‘.");

});

request.addListener("end", function() {

route(handle, pathname, response, postData);

});

}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");

exports.start = start;

}

上述代码做了三件事情:首先,我们设置了接收数据的编码格式为utf-8,然后注册了"data"事件的监听器,用于收集每次接收到的新数据块,并将其赋值给postData变量,最后,我们将请求路由的调用移动end事件处理程序中,以确保它只会当所有数据接收完毕后才触发,并且只触发一次。我们同时还把POST数据传递给请求路由,因为这些数据,请求处理程序会用到。

上述代码在每个数据块到达的时候输出了日志,这对于最终环境来说,是很不好的(数据量可能会很大),但是,在开发阶段是很有用的,有助于让我们看到发生了什么。

我建议可以尝试下,尝试着去输入一小段文本,以及大段内容,当大段内容的时候,就会发现data事件会触发多次。

接下来在/upload页面,展示用户输入的内容。要实现该功能,我们需要将postData传递给请求处理程序,修改router.js为如下形式:

function route(handle, pathname, response, postData) {

console.log("About to route a request for " + pathname);

if (typeof handle[pathname] === ‘function‘) {

handle[pathname](response, postData);

} else {

console.log("No request handler found for " + pathname);

response.writeHead(404, {"Content-Type": "text/plain"});

response.write("404 Not found");

response.end();

}

}

exports.route = route;

然后,在requestHandlers.js中,我们将数据包含在对upload请求的响应中:

function start(response, postData) {

console.log("Request handler ‘start‘ was called.");

var body = [

‘<html>‘,

‘<head>‘,

‘<meta http-equiv=\‘Content-Type\‘ content=\‘text/html;\‘ charset=\‘utf-8\‘ />‘,

‘</head>‘,

‘<body>‘,

‘<form action=\‘/upload\‘ method=\‘post\‘>‘,

‘<textarea name=\‘text\‘ rows=\‘20\‘ cols=\‘60\‘></textarea>‘,

‘<input type=\‘submit\‘ value=\‘Submit text\‘ />‘,

‘</form>‘,

‘</body>‘,

‘</html>‘

].join("");

response.writeHead(200, {"Content-Type": "text/html"});

response.write(body);

response.end();

}

function upload(response, postData) {

console.log("Request handler ‘upload‘ was called.");

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("You‘ve sent: " + postData);

response.end();

}

exports.start = start;

exports.upload = upload;

我们现在可以接收POST数据并在请求处理程序中处理该数据。

最后,当前我们是把请求的整个消息体传递给了请求路由和请求处理程序。我们应该只把POST数据中,我们感兴趣的部分传递给请求路由和请求处理程序。在我们这个例子中,我们感兴趣的其实只是text字段。

我们可以使用此前介绍过querystring模块来实现requestHandlers.js:

var querystring = require("querystring");

function start(response, postData) {

console.log("Request handler ‘start‘ was called.");

var body = [

‘<html>‘,

‘<head>‘,

‘<meta http-equiv=\‘Content-Type\‘ content=\‘text/html;\‘ charset=\‘utf-8\‘ />‘,

‘</head>‘,

‘<body>‘,

‘<form action=\‘/upload\‘ method=\‘post\‘>‘,

‘<textarea name=\‘text\‘ rows=\‘20\‘ cols=\‘60\‘></textarea>‘,

‘<input type=\‘submit\‘ value=\‘Submit text\‘ />‘,

‘</form>‘,

‘</body>‘,

‘</html>‘

].join("");

response.writeHead(200, {"Content-Type": "text/html"});

response.write(body);

response.end();

}

function upload(response, postData) {

console.log("Request handler ‘upload‘ was called.");

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("You‘ve sent the text: " + querystring.parse(postData).text);

response.end();

}

exports.start = start;

exports.upload = upload;

以上就是关于处理POST数据的全部内容。

处理文件上传

最后,我们来实现最终的用例:允许用户上传图片,并将该图片在浏览器中显示出来。

回到90年代,这个用例完全可以满足用于IPO的商业模型,如今,通过它可以接到两个内容:如何安装外部Node.js模块,以及如何将它们应用到我们应用中。

这里我们要用到的外部模块是Felix Geisendorfer开发的node-formidable模块。它对解析上传的文件数据做了很好的抽象。处理文件上传"就是"处理POST数据--但是,麻烦的是在具体的处理细节,所以,这里采用现成的方案更合适点。

使用该模块,首先需要安装该模块。Node.js有它自己的包管理器,叫NPM。它可以让安装Node.js的外部模块变得非常方便。通过如下一条命令就可以完成该模块的安装:

npm install formidable

如果安装出现错误,执行

npm config set registry http://registry.cnpmjs.org

再重新npm install formidable

如果终端输出如下内容:

npm info build Success: [email protected]

npm ok

说明模块已经安装成功了。

现在我们就可以用formidable模块了--使用外部模块与内部模块类似,用require语句将其引入即可:

var formidable = require("formidable");

这里该模块做的就是将通过HTTP POST请求提交的表单,在Node.js中可以被解析。我们要做的就是创建一个新的IncomingForm,它是对提交表单的抽象表示,之后,就可以用它解析request对象,获取表单中需要的数据字段。

node-formidable官方的例子展示了这两部分是如何融合在一起工作的:

server.js

var formidable = require(‘formidable‘),

http = require(‘http‘),

sys = require(‘sys‘);

http.createServer(function(req, res){

if (req.url == ‘/upload‘ && req.method.toLowerCase() == ‘post‘) {

var form = new formidable.IncomingForm();

form.parse(req, function(err, fields, files) {

res.writeHead(200, {‘content-type‘: ‘text/plain‘});

res.write(‘received upload:\n\n‘);

res.end(sys.inspect({fields:fields, files: files}));

});

return;

}

res.writeHead(200, {‘content-type‘: ‘text/html‘});

res.end(

[

‘<form action="/upload" enctype="multipart/form-data" method="post">‘,

‘<input type="text" name="title" /><br />‘,

‘<input type="file" name="upload" multiple="multiple" /><br />‘,

‘<input type="submit" value="upload" />‘,

‘</form>‘

].join(‘‘)

);

}).listen(8888);

如果我们将上述代码,保存到一个文件中,并通过node来执行,就可以进行简单的表单提交了,包括文件上传。然后,可以看到通过调用form.parse传递给回调函数的files对象的内容,如下所示:

received upload:

{

fields: {title: ‘Hello World‘},

files: {

upload: {

size: 1558,

path: ‘/tmp/1c747974a27a6292743669e91f29350b‘,

name: ‘us-flag.png‘,

type: ‘image/png‘,

lastModifiedDate: Tue, 21 Jun 2011 07:02:41 GMT,

_writeStream: [Object],

length: [Getter],

filename: [Getter],

mime: [Getter]

}

}

}

为了实现我们的功能,我们需要将上述代码应用到我们的应用中,另外,我们还要考虑如何将上传文件的内容(保存在/tmp目录中)显示到浏览器中。

我们先来解决后面那个问题:对于保存在本地硬盘的文件,如何才能在浏览器中看到呢?

显然,我们需要将该文件读取到我们的服务器中,使用一个叫fs的模块。

我们来添加/showURL的请求处理程序,该处理程序直接硬编码将文件/tmp/test.png内容展示到浏览器中。当然了,首先需要将该图片保存到这个位置才行。

将requestHandlers.js修改为如下形式:

var querystring = require("querystring"),

fs = require("fs");

function start(response, postData) {

console.log("Request handler ‘start‘ was called.");

var body = [

‘<html>‘,

‘<head>‘,

‘<meta http-equiv=\‘Content-Type\‘ content=\‘text/html;\‘ charset=\‘utf-8\‘ />‘,

‘</head>‘,

‘<body>‘,

‘<form action=\‘/upload\‘ method=\‘post\‘>‘,

‘<textarea name=\‘text\‘ rows=\‘20\‘ cols=\‘60\‘></textarea>‘,

‘<input type=\‘submit\‘ value=\‘Submit text\‘ />‘,

‘</form>‘,

‘</body>‘,

‘</html>‘

].join("");

response.writeHead(200, {"Content-Type": "text/html"});

response.write(body);

response.end();

}

function upload(response, postData) {

console.log("Request handler ‘upload‘ was called.");

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("You‘ve sent the text: " + querystring.parse(postData).text);

response.end();

}

function show(response, postData) {

console.log("Request handler ‘show‘ was called.");

fs.readFile("/tmp/test.png", "binary", function(error, file) {

if (error) {

response.writeHead(500, {"Content-Type": "text/plain"});

response.write(error + "\n");

response.end();

} else {

response.writeHead(200, {"Content-Type": "image/png"});

response.write(file, "binary");

response.end();

}

});

}

exports.start = start;

exports.upload = upload;

exports.show = show;

我们还需要将这新的请求处理程序,添加到index.js中的路由映射表中:

var server = require("./server");

var router = require("./router");

var requestHandlers = require("./requestHandlers");

var handle = {};

handle["/"] = requestHandlers.start;

handle["/start"] = requestHandlers.start;

handle["/upload"] = requestHandlers.upload;

handle["/show"] = requestHandlers.show;

server.start(router.route, handle);

重启服务器之后,通过访问http://localhost:8888/show,就可以看到保存在/tmp/test.png的图片了。

最后我们需要做的是:

在/start表单中添加一个文件上传元素

将node-formidable整合到我们的upload请求处理程序中,用于将上传的图片保存到/tmp/test.png

将上传的图片内嵌到/uploadURL输出的HTML中

第一项很简单。只需要在HTML表单中,添加一个multipart/form-data的编码类型,移除此前的文本区,添加一个文件上传组件,并将提交按钮的文案改为"Upload file"即可。如下requestHandler.js所示:

var querystring = require("querystring"),

fs = require("fs");

function start(response, postData) {

console.log("Request handler ‘start‘ was called.");

var body = [

‘<html>‘,

‘<head>‘,

‘<meta http-equiv=\‘Content-Type\‘ content=\‘text/html;\‘ charset=\‘utf-8\‘ />‘,

‘</head>‘,

‘<body>‘,

‘<form action=\‘/upload\‘ enctype="multipart/form-data" method=\‘post\‘>‘,

‘<input type="file" name="upload" />‘,

‘<input type=\‘submit\‘ value=\‘Upload file\‘ />‘,

‘</form>‘,

‘</body>‘,

‘</html>‘

].join("");

response.writeHead(200, {"Content-Type": "text/html"});

response.write(body);

response.end();

}

function upload(response, postData) {

console.log("Request handler ‘upload‘ was called.");

response.writeHead(200, {"Content-Type": "text/plain"});

response.write("You‘ve sent the text: " + querystring.parse(postData).text);

response.end();

}

function show(response, postData) {

console.log("Request handler ‘show‘ was called.");

fs.readFile("/tmp/test.png", "binary", function(error, file) {

if (error) {

response.writeHead(500, {"Content-Type": "text/plain"});

response.write(error + "\n");

response.end();

} else {

response.writeHead(200, {"Content-Type": "image/png"});

response.write(file, "binary");

response.end();

}

});

}

exports.start = start;

exports.upload = upload;

exports.show = show;

很好。下一步相对比较复杂。这里有这样一个问题:我们需要在upload处理程序中对上传的文件进行处理,这样的花,我们就需要将request对象传递给node-formidable的form.parse函数。

但是,我们有的只是response对象和postData数组。看样子,我们只能不得不将request对象从服务器开始一路通过请求路由,再传递给请求处理程序。或许还有更好的方案,但是,不管怎么说,目前这样做可以满足我们的需求。

到这里,我们可以将postData从服务器以及请求处理程序中移除了一方面,对于我们处理文件上传来说已经不需要了,另外一方面,它甚至可能会引发这样一个问题:我们已经“消耗”了request对象中的数据,这意味着,对于form.parse来说,当它想要获取数据的时候就什么也获取不到了。(因为Node.js不会对数据做缓存)

我们从server.js开始:移除对postData的处理以及request.setEncoding(这部分node-formidable自身会处理),转而采用将request对象传递给请求路由的方式:

var http = require("http");

var url = require("url");

function start(route, handle) {

function onRequest(request, response) {

var pathname = url.parse(request.url).pathname;

console.log("Request for " + pathname + " received.");

route(handle, pathname, response, request);

}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");

}

exports.start = start;

接下来是router.js我们不再需要传递postData了,这次要传递request对象:

function route(handle, pathname, response, request) {

console.log("About to route a request for " + pathname);

if (typeof handle[pathname] === ‘function‘) {

handle[pathname](response, request);

} else {

console.log("No request handler found for " + pathname);

response.writeHead(404, {"Content-Type": "text/html"});

response.write("404 Not found");

response.end();

}

}

exports.route = route;

现在,request对象就可以在我们的upload请求处理程序中使用了。node-formidable会处理将上传的文件保存到本地/tmp目录中,而我们需要做的是确保该文件保存成/tmp/test.png。没错,我们保持简单,并假设只允许上传PNG图片。

这里采用fs.renameSync(path1, path2)来实现。要注意的是,正如其名,该方法是同步执行的,也就是说,如果该重命名的操作很耗时的话会阻塞。这块我们先不考虑。

接下来,我们把处理文件上传以及重命名的操作放到一起,如下requestHandlers.js所示:

var querystring = require("querystring"),

fs = require("fs"),

formidable = require("formidable");

function start(response) {

console.log("Request handler ‘start‘ was called.");

var body = [

‘<html>‘,

‘<head>‘,

‘<meta http-equiv=\‘Content-Type\‘ content=\‘text/html;\‘ charset=\‘utf-8\‘ />‘,

‘</head>‘,

‘<body>‘,

‘<form action=\‘/upload\‘ enctype="multipart/form-data" method=\‘post\‘>‘,

‘<input type="file" name="upload" multiple="multiple" />‘,

‘<input type=\‘submit\‘ value=\‘Upload file\‘ />‘,

‘</form>‘,

‘</body>‘,

‘</html>‘

].join("");

response.writeHead(200, {"Content-Type": "text/html"});

response.write(body);

response.end();

}

function upload(response, request) {

console.log("Request handler ‘upload‘ was called.");

var form = new formidable.IncomingForm();

console.log("about to parse");

form.parse(request, function(error, fields, files) {

console.log("parsing done");

fs.renameSync(files.upload.path, "/tmp/test.png");

response.writeHead(200, {"Content-Type": "text/html"});

response.write("received image:<br />");

response.write("<img src=‘/show‘ />");

response.end();

});

}

function show(response, postData) {

console.log("Request handler ‘show‘ was called.");

fs.readFile("/tmp/test.png", "binary", function(error, file) {

if (error) {

response.writeHead(500, {"Content-Type": "text/plain"});

response.write(error + "\n");

response.end();

} else {

response.writeHead(200, {"Content-Type": "image/png"});

response.write(file, "binary");

response.end();

}

});

}

exports.start = start;

exports.upload = upload;

exports.show = show;

好了,重启服务器,我们应用所有的功能就可以用了。选择一张本地图片,将其上传到服务器,然后浏览器就会显示该图片。

总结与展望

我们开发完了一个Node.js的web应用,应用虽小,但却"五脏俱全"。期间,我们介绍了很多技术点:服务端JavaScript、函数式编程、阻塞与非阻塞、回调、事件、内部和外部模块等待。

还有本书没有介绍到的,如何操作数据库、如何进行单元测试、如何开发Node.js的外部模块以及一些简单的诸如如何获取GET请求之类的方法。

有什么问题可以向社区寻求解答。

参考地址:https://github.com/joyent/node/wiki

时间: 2024-08-30 01:41:04

nodejs详解的相关文章

NodeJS写日志_Log4js使用详解

今天和大家分享一下NodeJS中写日志的一个常用第三方包:Log4js. 跟随主流Blog特色,先简单介绍下Log4js的基本信息.介绍Log4js之前,需要先说一下Log4***,Log4***是由Apache提供的多平台下多语言下日志书写扩展包,目的很简单就是使日志书写更加方便简洁,同时对不同的业务日志能够进行灵活的分文件记录,同时也包含着详细的等级配置,为之后分级输出,检索,及程序自动解析提供更加便捷的支持(一家之言,非官方描述,领会精神).Log4***有很多语言的实现,比如Log4cp

分针网—IT教育:nodeJS新建一个项目及代码详解

有了解nodejs的童鞋们知道,nodejs有很多的依赖,那么我们在新建一个项目的时候,如何快速的导入这些依赖从而创建一个nodejs项目呢,接下来会详细介绍. 这里安利一个前端开发工具--webstorm.真的很好用,除了有点(或许不是一点点)占内存,打开的时候有点慢.在工作中用这个会很方便(在考虑要不要写一篇前端开发工具的比较,吼吼吼~(≧▽≦)/~) ?下面我们来用命令创建一个nodejs项目. 1.我们在c盘的ws文件夹下创建项目. 打开DOS窗口,进入到ws文件夹 2.在此文件夹下创建

Windows系统下nodejs、npm、express的下载和安装教程详解

这篇文章主要介绍了Windows系统下nodejs.npm.express的下载和安装教程详解,非常不错,具有参考借鉴价值,需要的朋友可以参考下1. node.js下载 首先进入http://nodejs.org/dist/,这里面的版本呢,几乎每个月都出几个新的,建议大家下载最新版本,看看自己的电脑是多少位的,别下错了. 下载完解压到你想放的位置就好了,解压后你会发现里面有node.exe.我解压到了D:\software_install婴儿起名http://www.bbqmw.net/qm_

nodejs QueryString模块 详解

QueryString模块 "QueryString" 模块用于实现URL参数字符串与参数对象的互相转换 此类一共包括4个方法: querystring.stringify(obj, [sep], [eq]) querystring.parse(str, [sep], [eq], [options]) querystring.escape querystring.unescape 一:querystring.stringify(obj,[sep],[eq]) 对象格式化成参数字符串 ,

测试框架mochajs详解

测试框架mochajs详解 章节目录 关于单元测试的想法 mocha单元测试框架简介 安装mocha 一个简单的例子 mocha支持的断言模块 同步代码测试 异步代码测试 promise代码测试 不建议使用箭头函数 钩子函数 钩子函数的描述参数 异步的钩子函数 全局钩子 延迟启动测试 测试用例TODO 仅执行一个用例集/用例 跳过哪些用例集/用例 重新执行用例 动态生成用例 测试时间 测试超时 用例集执行超时 用例执行超时 钩子函数超时 diff差异比较功能 mocha使用命令和参数 mocha

RequireJS-CommonJS-AMD-ES6 Import/Export详解

RequireJS-CommonJS-AMD-ES6 Import/Export详解 为什么起了一个这个抽象的名字呢,一下子提了四个名词分别是:RequireJS,CommonJS,AMD,ES6,答案是因为现实很骨感,我们必须很勇敢才能正视这一段悲催的往事.如今的JavaScript平台正值如日中天,大家可能会忽略他的过去和弊端,这些弊端中一直被人诟病的就是JavaScript的包管理,比如类似Java中的import,其实理论上来讲这种基本元素的缺失大大的阻碍了人们对一种语言的认可,认为他难

phonegap安装 环境搭建与配置详解

首先介绍搭建环境的基本准备: 1 java JDK   (搜索一下.......) 2 android SDK 最新版(我当时的版本是 API 19  点击打开链接) 3 Ant 打包工具  (百度一个或者 点击打开链接) 4 eclipse   (建议使用google的  因为内置ADT     点击打开链接 ) 5 ADT (ecplise 里的插件,安卓开发工具插件     下载上面的链接,内部集成了ADT) 6 Node.js   (点击打开链接) 7 sublime  点击打开链接 ;

源映射(Source Map)详解

一.什么是源映射 为了提高性能,很多站点都会先压缩 JavaScript 代码然后上线, 但如果代码运行时出现错误,浏览器只会显示在已压缩的代码中的位置,很难确定真正的源码错误位置. 这时源映射就登场了. 源映射(Source Map)是一种数据格式,它存储了源代码和生成代码之间的位置映射关系. 源映射一般使用 .map 扩展名,源映射本质是一个 JSON 文本文档,其 MIME 类型也一般设为 application/json. 二.如何使用源映射 在 JavaScript 代码中添加注释:

CSS3中的弹性流体盒模型技术详解(一)

从这篇文章开始,会利用几个篇幅,我将跟大家分享 从CSS1到CSS3都是如何实现页面布局的(当然,所指的范畴是利用CSS技术). 由于盒子模型技术内容比较多,这篇文章我将着重讲解知识点. 下一篇文章,我会带领大家开发一个兼容 pc端浏览器和 移动端浏览器的弹性布局web界面的实例.希望大家能从中领受到CSS3在布局方面的强大功能. 好了,长话短说,现在开始我们的<CSS3中的弹性流体盒模型技术详解>之旅吧! 在讲解CSS3中新增的弹性布局属性之前,我们先回顾一下CSS1 和 CSS2中都已经定