本书致力于教会你如何用Node.js来开发应用,过程当中会传授你全部所需的“高级”JavaScript知识。本书毫不是一本“Hello World”的教程。javascript
你正在阅读的已是本书的最终版。所以,只有当进行错误更正以及针对新版本Node.js的改动进行对应的修正时,才会进行更新。php
本书中的代码案例都在Node.js 0.6.11版本中测试过,能够正确工做。html
本书最适合与我有类似技术背景的读者: 至少对一门诸如Ruby、Python、PHP或者Java这样面向对象的语言有必定的经验;对JavaScript处于初学阶段,而且彻底是一个Node.js的新手。前端
这里指的适合对其余编程语言有必定经验的开发者,意思是说,本书不会对诸如数据类型、变量、控制结构等等之类很是基础的概念做介绍。要读懂本书,这些基础的概念我都默认你已经会了。java
然而,本书仍是会对JavaScript中的函数和对象做详细介绍,由于它们与其余同类编程语言中的函数和对象有很大的不一样。node
读完本书以后,你将完成一个完整的web应用,该应用容许用户浏览页面以及上传文件。git
固然了,应用自己并无什么了不得的,相比为了实现该功能书写的代码自己,咱们更关注的是如何建立一个框架来对咱们应用的不一样模块进行干净地剥离。 是否是很玄乎?稍后你就明白了。程序员
本书先从介绍在Node.js环境中进行JavaScript开发和在浏览器环境中进行JavaScript开发的差别开始。github
紧接着,会带领你们完成一个最传统的“Hello World”应用,这也是最基础的Node.js应用。web
最后,会和你们讨论如何设计一个“真正”完整的应用,剖析要完成该应用须要实现的不一样模块,并一步一步介绍如何来实现这些模块。
能够确保的是,在这过程当中,你们会学到JavaScript中一些高级的概念、如何使用它们以及为何使用这些概念就能够实现而其余编程语言中同类的概念就没法实现。
该应用全部的源代码均可以经过 本书Github代码仓库.
抛开技术,咱们先来聊聊你以及你和JavaScript的关系。本章的主要目的是想让你看看,对你而言是否有必要继续阅读后续章节的内容。
若是你和我同样,那么你很早就开始利用HTML进行“开发”,正因如此,你接触到了这个叫JavaScript有趣的东西,而对于JavaScript,你只会基本的操做——为web页面添加交互。
而你真正想要的是“干货”,你想要知道如何构建复杂的web站点 —— 因而,你学习了一种诸如PHP、Ruby、Java这样的编程语言,并开始书写“后端”代码。
与此同时,你还始终关注着JavaScript,随着经过一些对jQuery,Prototype之类技术的介绍,你慢慢了解到了不少JavaScript中的进阶技能,同时也感觉到了JavaScript绝非仅仅是window.open() 那么简单。 .
不过,这些毕竟都是前端技术,尽管当想要加强页面的时候,使用jQuery总让你以为很爽,但到最后,你顶可能是个JavaScript用户,而非JavaScript开发者。
而后,出现了Node.js,服务端的JavaScript,这有多酷啊?
因而,你以为是时候该从新拾起既熟悉又陌生的JavaScript了。可是别急,写Node.js应用是一件事情;理解为何它们要以它们书写的这种方式来书写则意味着——你要懂JavaScript。此次是玩真的了。
问题来了: 因为JavaScript真正意义上以两种,甚至能够说是三种形态存在(从中世纪90年代的做为对DHTML进行加强的小玩具,到像jQuery那样严格意义上的前端技术,一直到如今的服务端技术),所以,很难找到一个“正确”的方式来学习JavaScript,使得让你书写Node.js应用的时候感受本身是在真正开发它而不只仅是使用它。
由于这就是关键: 你自己已是个有经验的开发者,你不想经过处处寻找各类解决方案(其中可能还有不正确的)来学习新的技术,你要确保本身是经过正确的方式来学习这项技术。
固然了,外面不乏很优秀的学习JavaScript的文章。可是,有的时候光靠那些文章是远远不够的。你须要的是指导。
本书的目标就是给你提供指导。
业界有很是优秀的JavaScript程序员。而我并不是其中一员。
我就是上一节中描述的那个我。我熟悉如何开发后端web应用,可是对“真正”的JavaScript以及Node.js,我都只是新手。我也只是最近学习了一些JavaScript的高级概念,并无实践经验。
所以,本书并非一本“从入门到精通”的书,更像是一本“从初级入门到高级入门”的书。
若是成功的话,那么本书就是我当初开始学习Node.js最但愿拥有的教程。
JavaScript最先是运行在浏览器中,然而浏览器只是提供了一个上下文,它定义了使用JavaScript能够作什么,但并无“说”太多关于JavaScript语言自己能够作什么。事实上,JavaScript是一门“完整”的语言: 它能够使用在不一样的上下文中,其能力与其余同类语言相比有过之而无不及。
Node.js事实上就是另一种上下文,它容许在后端(脱离浏览器环境)运行JavaScript代码。
要实如今后台运行JavaScript代码,代码须要先被解释而后正确的执行。Node.js的原理正是如此,它使用了Google的V8虚拟机(Google的Chrome浏览器使用的JavaScript执行环境),来解释和执行JavaScript代码。
除此以外,伴随着Node.js的还有许多有用的模块,它们能够简化不少重复的劳做,好比向终端输出字符串。
所以,Node.js事实上既是一个运行时环境,同时又是一个库。
要使用Node.js,首先须要进行安装。关于如何安装Node.js,这里就不赘述了,能够直接参考官方的安装指南。安装完成后,继续回来阅读本书下面的内容。
好了,“废话”很少说了,立刻开始咱们第一个Node.js应用:“Hello World”。
打开你最喜欢的编辑器,建立一个helloworld.js文件。咱们要作就是向STDOUT输出“Hello World”,以下是实现该功能的代码:
console.log("Hello World");
保存该文件,并经过Node.js来执行:
node helloworld.js
正常的话,就会在终端输出Hello World 。
好吧,我认可这个应用是有点无趣,那么下面咱们就来点“干货”。
咱们来把目标设定得简单点,不过也要够实际才行:
差很少了,你如今也能够去Google一下,找点东西乱搞一下来完成功能。可是咱们如今先不作这个。
更进一步地说,在完成这一目标的过程当中,咱们不只仅须要基础的代码而无论代码是否优雅。咱们还要对此进行抽象,来寻找一种适合构建更为复杂的Node.js应用的方式。
咱们来分解一下这个应用,为了实现上文的用例,咱们须要实现哪些部分呢?
咱们先来想一想,使用PHP的话咱们会怎么构建这个结构。通常来讲咱们会用一个Apache HTTP服务器并配上mod_php5模块。
从这个角度看,整个“接收HTTP请求并提供Web页面”的需求根本不须要PHP来处理。
不过对Node.js来讲,概念彻底不同了。使用Node.js时,咱们不只仅在实现一个应用,同时还实现了整个HTTP服务器。事实上,咱们的Web应用以及对应的Web服务器基本上是同样的。
听起来好像有一大堆活要作,但随后咱们会逐渐意识到,对Node.js来讲这并非什么麻烦的事。
如今咱们就来开始实现之路,先从第一个部分--HTTP服务器着手。
当我准备开始写个人第一个“真正的”Node.js应用的时候,我不但不知道怎么写Node.js代码,也不知道怎么组织这些代码。
我应该把全部东西都放进一个文件里吗?网上有不少教程都会教你把全部的逻辑都放进一个用Node.js写的基础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服务器的构成。
第一行请求(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");
咱们在 execute 接受第一个参数的地方直接定义了咱们准备传递给 execute 的函数。
用这种方式,咱们甚至不用给这个函数起名字,这也是为何它被叫作 匿名函数。
这是咱们和我所认为的“进阶”JavaScript的第一次亲密接触,不过咱们仍是得按部就班。如今,咱们先接受这一点:在JavaScript中,一个函数能够做为另外一个函数接收一个参数。咱们能够先定义一个函数,而后传递,也能够在传递参数的地方直接定义函数。
带着这些知识,咱们再来看看咱们简约而不简单的HTTP服务器:
var http = require("http");
http.createServer(function(request, response) {
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原生的工做方式。它是事件驱动的,这也是它为何这么快的缘由。
你也许会想花点时间读一下Felix Geisendörfer的大做Understanding node.js,它介绍了一些背景知识。
这一切都归结于“Node.js是事件驱动的”这一事实。好吧,其实我也不是特别确切的了解这句话的意思。不过我会试着解释,为何它对咱们用Node.js写网络应用(Web based application)是有意义的。
当咱们使用 http.createServer 方法的时候,咱们固然不仅是想要一个侦听某个端口的服务器,咱们还想要它在服务器收到一个HTTP请求的时候作点什么。
问题是,这是异步的:请求任什么时候候均可能到达,可是咱们的服务器却跑在一个单进程中。
写PHP应用的时候,咱们一点也不为此担忧:任什么时候候当有请求进入的时候,网页服务器(一般是Apache)就为这一请求新建一个进程,而且开始从头至尾执行相应的PHP脚本。
那么在咱们的Node.js程序中,当一个新的请求到达8888端口的时候,咱们怎么控制流程呢?
嗯,这就是Node.js/JavaScript的事件驱动设计可以真正帮上忙的地方了——虽然咱们还得学一些新概念才能掌握它。让咱们来看看这些概念是怎么应用在咱们的服务器代码里的。
咱们建立了服务器,而且向建立它的方法传递了一个函数。不管什么时候咱们的服务器收到一个请求,这个函数就会被调用。
咱们不知道这件事情何时会发生,可是咱们如今有了一个处理请求的地方:它就是咱们传递过去的那个函数。至于它是被预先定义的函数仍是匿名函数,就可有可无了。
这个就是传说中的 回调 。咱们给某个方法传递了一个函数,这个方法在有相应事件发生时调用这个函数来进行 回调 。
至少对我来讲,须要一些功夫才能弄懂它。你若是仍是不太肯定的话就再去读读Felix的博客文章。
让咱们再来琢磨琢磨这个新概念。咱们怎么证实,在建立完服务器以后,即便没有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 对象。
OK,就像我保证过的那样,咱们如今能够回到咱们如何组织应用这个问题上了。咱们如今在 server.js 文件中有一个很是基础的HTTP服务器代码,并且我提到一般咱们会有一个叫 index.js 的文件去调用应用的其余模块(好比 server.js 中的HTTP服务器模块)来引导和启动应用。
咱们如今就来谈谈怎么把server.js变成一个真正的Node.js模块,使它能够被咱们(还没动工)的 index.js 主文件使用。
也许你已经注意到,咱们已经在代码中使用了模块了。像这样:
var http = require("http");
...
http.createServer(...);
Node.js中自带了一个叫作“http”的模块,咱们在咱们的代码中请求它并把返回值赋给一个本地变量。
这把咱们的本地变量变成了一个拥有全部 http 模块所提供的公共方法的对象。
给这种本地变量起一个和模块名称同样的名字是一种惯例,可是你也能够按照本身的喜爱来:
var foo = require("http");
...
foo.createServer(...);
很好,怎么使用Node.js内部模块已经很清楚了。咱们怎么建立本身的模块,又怎么使用它呢?
等咱们把 server.js 变成一个真正的模块,你就能搞明白了。
事实上,咱们不用作太多的修改。把某段代码变成模块意味着咱们须要把咱们但愿提供其功能的部分 导出 到请求这个模块的脚本。
目前,咱们的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参数,随后路由须要根据这些数据来执行相应的代码(这里“代码”对应整个应用的第三部分:一系列在接收到请求时真正工做的处理程序)。
所以,咱们须要查看HTTP请求,从中提取出请求的URL以及GET/POST参数。这一功能应当属于路由仍是服务器(甚至做为一个模块自身的功能)确实值得探讨,但这里暂定其为咱们的HTTP服务器的功能。
咱们须要的全部数据都会包含在request对象中,该对象做为onRequest()回调函数的第一个参数传递。可是为了解析这些数据,咱们须要额外的Node.JS模块,它们分别是url和querystring模块。
url.parse(string).query | url.parse(string).pathname | | | | | ------ ------------------- http://localhost:8888/start?foo=bar&hello=world --- ----- | | | | querystring(string)["foo"] | | querystring(string)["hello"]
固然咱们也能够用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;
如你所见,这段代码什么也没干,不过对于如今来讲这是应该的。在添加更多的逻辑之前,咱们先来看看如何把路由和服务器整合起来。
咱们的服务器应当知道路由的存在并加以有效利用。咱们固然能够经过硬编码的方式将这一依赖项绑定到服务器上,可是其它语言的编程经验告诉咱们这会是一件很是痛苦的事,所以咱们将使用依赖注入的方式较松散地添加路由模块(你能够读读Martin Fowlers关于依赖注入的大做来做为背景知识)。
首先,咱们来扩展一下服务器的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服务器已经在使用路由模块了,并会将请求的路径传递给路由:
bash$ node index.js Request for /foo received. About to route a request for /foo
(以上输出已经去掉了比较烦人的/favicon.ico请求相关的部分)。
请容许我再次脱离主题,在这里谈一谈函数式编程。
将函数做为参数传递并不只仅出于技术上的考量。对软件设计来讲,这实际上是个哲学问题。想一想这样的场景:在index文件中,咱们能够将router对象传递进去,服务器随后能够调用这个对象的route函数。
就像这样,咱们传递一个东西,而后服务器利用这个东西来完成一些事。嗨那个叫路由的东西,能帮我把这个路由一下吗?
可是服务器其实不须要这样的东西。它只须要把事情作完就行,其实为了把事情作完,你根本不须要东西,你须要的是动做。也就是说,你不须要名词,你须要动词。
理解了这个概念里最核心、最基本的思想转换后,我天然而然地理解了函数编程。
我是在读了Steve Yegge的大做名词王国中的死刑以后理解函数编程。你也去读一读这本书吧,真的。这是曾给予我阅读的快乐的关于软件的书籍之一。
回到正题,如今咱们的HTTP服务器和请求路由模块已经如咱们的指望,能够相互交流了,就像一对亲密无间的兄弟。
固然这还远远不够,路由,顾名思义,是指咱们要针对不一样的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 handler y也使得系统丑陋不堪。
仔细想一想,有一大堆东西,每一个都要映射到一个字符串(就是请求的URL)上?彷佛关联数组(associative array)能完美胜任。
不过结果有点使人失望,JavaScript没提供关联数组 -- 也能够说它提供了?事实上,在JavaScript中,真正能提供此类功能的是它的对象。
在这方面,http://msdn.microsoft.com/en-us/magazine/cc163419.aspx有一个不错的介绍,我在此摘录一段:
在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. About to route a request for / Request handler 'start' was called.
很好。不过如今要是请求处理程序可以向浏览器返回一些有意义的信息而并不是全是“Hello World”,那就更好了。
这里要记住的是,浏览器发出请求后得到并显示的“Hello World”信息还是来自于咱们server.js文件中的onRequest函数。
其实“处理请求”说白了就是“对请求做出响应”,所以,咱们须要让请求处理程序可以像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(),等你处理完以后我会去调用该回调函数的,谢谢!”
(若是想要了解更多关于事件轮询细节,能够阅读Mixu的博文——理解node.js的事件轮询。)
接下来,咱们会介绍一种错误的使用非阻塞操做的方式。
和上次同样,咱们经过修改咱们的应用来暴露问题。
此次咱们仍是拿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/plain"});
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请求)是比较复杂的,它不在本书的范畴,但,如何使用外部模块倒是在本书涉猎内容以内。
考虑这样一个简单的例子:咱们显示一个文本区(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>';
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;
好了,如今咱们的应用已经很完善了,均可以得到威比奖(Webby Awards)了,哈哈。(译者注:威比奖是由国际数字艺术与科学学院主办的评选全球最佳网站的奖项,具体参见详细说明)经过在浏览器中访问http://localhost:8888/start就能够看到简单的表单了,要记得重启服务器哦!
你可能会说:这种直接将视觉元素放在请求处理程序中的方式太丑陋了。说的没错,可是,我并不想在本书中介绍诸如MVC之类的模式,由于这对于你了解JavaScript或者Node.js环境来讲没多大关系。
余下的篇幅,咱们来探讨一个更有趣的问题: 当用户提交表单时,触发/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>';
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模块来实现:
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>';
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 Geisendörfer开发的node-formidable模块。它对解析上传的文件数据作了很好的抽象。 其实说白了,处理文件上传“就是”处理POST数据 —— 可是,麻烦的是在具体的处理细节,因此,这里采用现成的方案更合适点。
使用该模块,首先须要安装该模块。Node.js有它本身的包管理器,叫NPM。它可让安装Node.js的外部模块变得很是方便。经过以下一条命令就能够完成该模块的安装:
npm install formidable
若是终端输出以下内容:
npm info build Success: formidable@1.0.9
npm ok
就说明模块已经安装成功了。
如今咱们就能够用formidable模块了——使用外部模块与内部模块相似,用require语句将其引入便可:
var formidable = require("formidable");
这里该模块作的就是将经过HTTP POST请求提交的表单,在Node.js中能够被解析。咱们要作的就是建立一个新的IncomingForm,它是对提交表单的抽象表示,以后,就能够用它解析request对象,获取表单中须要的数据字段。
node-formidable官方的例子展现了这两部分是如何融合在一块儿工做的:
var formidable = require('formidable'),
http = require('http'),
util = require('util');
http.createServer(function(req, res) {
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
// parse a file upload
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(util.inspect({fields: fields, files: files}));
});
return;
}
// show a file upload form
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>'
);
}).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>';
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();
}
});
}