因为最近有东西需要用到node.js,所以我就在linux虚拟机上安装了node.js,对于javascript,也是第一次接触。
刚入门,就是一个实用的案例,毕竟这些东西都是实践出真知。这个案例就是一个web应用,允许用户上传图片并在当前网页显示出来。我们来看看,这个案例是如何从一个简简单单的代码变成具有我们上面功能的应用。
我们先看看如何在网页显示我们输出的消息,这条消息自然就是我们每个程序员所写下的第一句话:“Hello Word”。是的,这句话已经成为我们无论学习什么语言或是任何操作系统,都要写的第一句话,因为这句话说明我们已经迈出了新世界的第一步!
先上代码:
var http = require("http"); http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World"); response.end();}).listen(8888);
这段简短的代码就已经告诉我们如何在网页中显示消息。首先,是第一句,注意require()函数,这个函数是表示我们要申请的模块,这里申请的是http模块并把它的返回值赋值给我们声明的变量,这样我们的变量就会得到http模块所提供的所有公共方法(将该变量命名为我们所要申请的模块是一个好习惯,当然你完全可以自定义)。什么是模块?我们可以将模块理解为库或包,事实上,它也差不多正是这样,因为这些模块里面,封装着我们想要使用的函数,接下来我们还会学到,怎样创建自己的模块。好了,现在进入正题。也许有些人会问,什么是http模块?好吧,如果真的有人因为这个问题而无法前进,那么我就在这里讲一下,但是,我真的不是什么高手,javascript我只是稍微懂点基础罢了,大家只要将它简单认为是这样的东西,当我们要在网页显示消息的时候,一定要发出一个请求,告诉服务器,我要在一个网页前显示消息,而服务器就会响应我们的请求显示出来,所以,大家在接下来的代码中可以看到,http模块中的createServer()的参数是一个响应处理的函数(在javascript中,函数是可以作为参数的,因为函数本身是对象),这个函数需要两个参数,request(请求)和response(响应)。对于createServer()这个函数,它的参数是一个requestListener,就是自动被添加到request事件上的函数,然后返回一个网页服务对象,就是显示我们消息的网页(它的作用就是建立一个可以响应请求的服务器)。我们要想输出“Hello Word"这个消息,最主要的是在响应这方面下工夫。我们先要设置一下我们的消息的显示格式,比如在writeHead()函数中设置状态码和内容类型,这里是txt,所以就是“text/plain",然后就是用write()设定我们要输出的消息,最后就是以end()来结束我们这次的响应动作。很多人一定对这个函数有个疑问,就是那两个参数到底是什么?好吧,这里我就稍微讲一下,第一个是状态码,表示网页服务器http响应状态的三位数字,像是我们上面的200,就表示我们的请求已成功,请求所希望的响应头或数据体将随此响应返回,如果是404,就表示请求失败,请求所希望得到的资源未被服务器发现,所以404这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况(现在你知道为什么当我们浏览的网页出现问题时会显示404了吧)。详细的情况请看这条链接:http://baike.baidu.com/view/1790469.htm。第二个是表示MIME类型,所谓的MIME类型,就是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开,多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式,像是图片,txt等等,这里的text/plain就表示普通文档txt,如果是png图片,就是image/png,更多的情况看下面这条链接:http://baike.baidu.com/view/160611.htm
相信大家注意到了,最后的listen()这个监听器。只要有做过界面的人,像是搞过android控件的人,一定对监听器非常熟悉。这里就是监听我们的端口号为8888的网页响应动作,只要我们运行上面的代码,然后在我们的浏览器中输入http://localhost:8888 /就会在当前的网页中看到"Hello Word"。
值得注意的是,我们这里有一个匿名函数,这个函数就是我们处理响应和请求的实际处理函数,我们可以将它们提取出来,像是下面这样:
var http = require("http"); function onRequest(request, response){ response.writeHead(200, {"Content-Type":"text/plain"}); response.write("Hellod Word"); response.end(); } http.createServer(onRequest).listen(8888);
结果都是一样的,至于使用哪种方式,就看个人需要,不过匿名函数的使用方式比较普遍。
为什么要对我们的处理函数进行监听呢?那是因为node.js是基于事件驱动的。好吧,这句话实在是太泛泛而谈,就像我们的大学课堂老师,总是用似是而非的话来忽悠我们(因为他们自己也是似是而非)。node.js的工作原理是这样的:除了我们的代码,它的所有东西都是并行的!几乎所有的东西都是同时运行的!为什么是除了我们的代码呢?你可以这样想象,我们的代码就好比是整个应用的国王,它一直都在昏昏欲睡,然后,它有一大帮手下,当一个手下向他询问任务的时候,它会醒过来,指示任务然后继续昏昏欲睡,在上一个手下在执行任务的时候,会有其他手下继续询问任务并且执行。问题,为什么我们的代码要"昏昏欲睡"呢?只要稍微明白并发的同学就会明白,如果不这样做,就会出现同时多个手下询问任务的情况。这种情况不好吗?是的,很不好,因为这个国王并不是一个多么能干的国王(请别介意,事实上我们所写的代码真的绝大部分都是不"精明"的),所以,出现这种情况,它可能会出现错误,所以,一对一是最好的,而且这样的效率更高。然后,每个手下在完成任务后就会向我们的国王报告情况,这也是一对一的。所以,大家明白了没?(事实上,我也不是很明白,但是接触过并发,所以多多少少都能想象出来),所以,这里就有个监听器,这个监听器就是当我们的任务完成时向我们的国王进行报告。
关于这个例子还没完,因为这里有个重要的知识点必须讲清楚,就是回调。想象这种情况,我们的服务器是跑在一个单线程里的(是的,node.js是单线程的,因为它已经将除了代码以外的操作都设置为并行的),但是我们的服务器是要处理http请求的,这个请求可是任何时候都会过来,我们的服务器能够恰好的响应吗?于是,回调就变得非常重要,就是我们createServer()里的函数参数。无论何时,什么样的请求过来,只要它作为参数传进来,那么,我们的服务器都能根据这个参数自动调用合适的函数,恰当的处理这个请求,这就是回调(不熟悉回调的话,你只要这么想,回调的英文就是callback,我们的函数已经写好了,就像一个选秀选手在后台等着我们的主持人叫他出来表演,当主持人拿到它的号码(参数),就会叫他,这就是回调)。所以,我们的服务器是异步的。
就算我们的回调函数没有被调动,其他代码还是可以被执行的。就像这样:
function onRequest(request, response){ console.log("server has started"); .... }
这里只是在回调函数里添加一句:console.log("server has started")(如果不清楚这一句的话,就将它当成我们javascript中的alert()),本来应该是不会执行的,如果是我们平时的串行代码(就算是我们以前写的并发也是不行的,除非是特殊的方法),但是,神奇的海螺小姐又出现了!它能够被显示出来!我们要分清楚一个问题,就是我们的请求真正想要触发的,是我添加的语句的下面,前面并不是回调函数真正想要执行的部分(我用了“真正”这个词,因为这里必须跟大家说明一下,我们这里处理的只是response的部分,我并没有对request进行任何细节上的处理,所以,理论上,当任何请求过来的时候它都能被执行,至于是否能够被响应,则是看请求的类型),所以,当有请求过来的时候,就算它不能触发我们的回调函数,我们的代码只要能够执行,还是会执行的,因为这时任何请求都能使它运行。
好了,我们继续往下讲,前面的废话实在是太多了(请原谅我,因为我也是一只初学鸡,所以有些问题很大家一样,也是一头雾水,只能将自己知道的所有东西都倒出来了)。
我们上面讲的,只是实现一个非常基础的http服务器,它还并不是一个真正意义上的node.js模块,因为它不能被其他模块调用。那么,我们接下来怎样让它变成真正的模块呢?就是抓住我上面的关键点,能够被其他模块调用。通常,像是上面的创建服务器的代码,我们一般命名为server.js,而且我们都会有一个index.js作为驱使其他模块的国王。接下来我们就是要让这个手下能够听话了:
var http = require("http"); function start(){ function onRequest(request, response){ ... } http.createServer(onRequest).listen(8888); } exports.start = start;
我们这段代码唯一的不同点就是我们在结尾多了这一句:exports.start = start。这段代码就是将我们的start()函数导出去,然后让我们的index.js能够启动这个服务器,因为一般我们对服务器的操作就仅是启动它而已。
我们在index.js中只需这么写:
var server = require("./server"); server.start();
同样的道理,我们请求server模块并将其赋给一个变量,然后调用这个模块中的start()函数。这里与我们上面请求内置模块是一样的操作,但是,自定义的模块的请求是需要在前面加上"./"。
通过上面的例子,我们已经可以初步掌握如何创建模块以及如何在另一个模块中调用其他模块的方法,接下来,还是继续探讨其他模块的创建,就是有关于处理不同的url请求。当然,我们完全可以将代码放在我们已经建好的server模块中,但是我们一般都会新建一个模块,因为处理url请求并不是服务器要做的,它是要交给路由器的(路由器这个词大家一定非常熟悉吧,路由这个名字真的是非常形象,我对于译者的崇拜之情油然而生)。我们还是马上建立一个路由器模块router.js吧:
function route(pathname){ console.log("About to route:" + pathname); } exports.route = route;
然后我们的服务器还是要进行修改:
var http = require("http"); url = require("url"); function start(route){ function onRequest(requset, response){ var pathname = url.parse(request.url).pathname; console.log(pathname + "has received"); route(pathname);
} ... } exports.start = start;
接着就是index.js:
var server = require("./server"); router = require("./router"); server.start(router.route);
相信大家一定还是会注意到,就是导出来的函数在除了index.js之外的其他模块中,根本就不需要申请router模块就能直接使用,像是作为参数传递。我曾经将index.js的server.start()直接改为start(),结果它显示的是start()没有被定义这个错误,然后继续手贱,在server.js中添加一个route的函数并且导出,但是什么情况都没有发生!route函数并没显示任何错误!按道理来说,像是我们的java,如果你的两个类中的方法名是一样的,你必须指点对象,但是这里似乎并不是这样的。关于这个问题,我想放在后面再讲,因为后面的代码中会出现新的start函数。
我们这里要先讲一下路由器的工作原理。路由器会根据请求的url和其他需要的get,post参数来执行相应的处理程序,于是我们需要从http请求中提取出url和get,post参数。这一部分的功能到底是路由器还是服务器或者作为一个新的功能模块,我们这里就不讨论,因为这也太难为我这个初学者了,这是我的第一个应用,我对web编程并没有更多的经验,所以,这里就先放在server模块里。好了,那么,我们所需要的参数都是来自于请求,就是request,那么,我们怎样提取出来呢?解析request,我们需要其他模块,像是我上面用到的url模块,事实上,我们还与querystring模块,至于哪个更好,我们先表下不谈,就着url模块来讲如何提取,下面就是这两个模块的提取原理:
url.parse(string).query | url.parse(string).pathname | | | | | ------ ------------------- http://localhost:8888/start?http=bar&hello=world --- ----- | | | | querystring(string)["http"] | | querystring(string)["hello"]
对于上面的原理,我们只需要知道/start?http=bar&hello=world这一部分。相信大家看图就能明白这里面的组成。/start是我们在index.js中调用的server中的start函数,根就是我们的路径名pathname,后面就是我们的显示消息,“Hello Word",但是这里却是"hell0=word",为什么会是这样呢?如果接触过javascript的同学就不难明白,因为我们的url是被处理过的,所有的字母都已经被转化为小写,并且空格对应于=。这部分我就不再讲解了,因为我觉得我的javascript知识也实在是太匮乏了,所以大家感兴趣的话,还是自己看看相关的书吧。
我们现在已经可以通过不同的url路径来区别不同的请求了,也把路由器和服务器整合起来,现在我们来看看这里面的一些亮点吧。首先,就是我们的服务器要想知道路由器的存在并且加以利用,就像我前面讲过的,我们为什么不可以直接将这个路由器硬编码进我们服务器代码中呢?我们可以这么干:
function start(router.route){ ... }
但是,我们都知道,这样做的坏处就是我们的代码太"硬"了!编写这样的代码,会使得我们的复用性大大下降,所以,node.js采用的是依赖注入的方式来实现松散的注入路由模块。依赖注入,这是一个新字眼,相信我,这个字眼所代表的意思我们在java这样的面相对象编程语言中已经是接触过了,但是它的实际意义却是非常庞大的,像我这样的新手当然是不敢有任何奢望来向大家讲解这个话题,但是我会结合我们上面的例子稍微讲一下。依赖注入,说白一点,就是我们的对象的创建的时候如果需要得到另一个对象的引用(因为我们的对象的创建可能需要另一个对象中的方法),那么我们当然是会将那个对象的引用作为参数传进来。这就是所谓的"依赖",传对象的引用就是所谓的"注入"。这样的行为我们在java中是习以为常的,但是,这种行为会造成我们的代码的耦合性较大,因为我们的对象的创建是需要其他代码的,这样,当这部分的代码发生变化的时候,我们的新对象就要发生变化。哈,这样好像我是在将依赖注入是一种不好的行为,但是,我必须强调,这里的情况是指我们传进的对象是特定的对象。比如说,像是上面的例子,我们可以在新建一个js文件,这个js文件里也有一个导出的route函数,那么,如果是我们以前的思维,那么,在我们的server.js中传进的route函数一定要制定是哪一个,但是,即使你真这么做了,也不需要这么做,因为我们的解释器能够找到正确的执行函数(我不知道这里面是怎么发生,这个问题我想,我后面会专门写篇文章来研究一下,这里就先跳过)。所以,我们的代码就不会发生于特定的对象耦合的情况,这样以后如果要修改的话,就会相对容易,也不会有副作用。其实,我感觉这点可能与javascript中解释器寻找函数的机制是一样的,因为javascript的函数的调用是可以只写函数名的!另外,稍微说点题外话,其实上面的话题是与函数式编程有点,由于这方面我根本没有接触,所以上面讲的自然是小生乱弹了。
上面的例子我们的路由模块并没有针对不同的url执行不同的行为,只是我们的服务器能够与路由沟通了而已。所以,我们的脚步仍要继续。
function start() { console.log("Request handler 'start' was called."); } function upload() { console.log("Request handler 'upload' was called."); } exports.start = start; exports.upload = upload;
这里的requestHandler模块中的start()和upload()函数也并没有什么真正的处理,它们只是占位用的函数,就是我们为我们的start和upload这样的处理占个位置。然后我们的问题又来了,就是我们到底要将这段代码放在哪里?这里是将处理动作和路由联系起来,但是我们需要将这段代码硬编码进我们的服务器里吗?然后这样写:
if(request == x){
handler y;
}
这样我们岂不是要随着我们所要处理的请求的增加而不断扩充if!事实证明,当你的代码中有一大串if,那说明你的代码一定有问题!所以。正确的做法就是我们再重新写一个新的模块,这一次就是requestHandlers模块,然后再将这个对象传进来。
我们的代码如下:
首先,是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对象,而且接下来的语句就让人很头疼:handle["/"] = requestHandlers.start.怎么回事?为什么会这样写啊?其实,javascritp的对象只是键/值对的集合,我们可以想象成是一个键位字符串类型的字典,并且值可以是函数。所以,我们这里就完全看到了这种特性,我们的对象甚至可以这样写:handle["/"],"/"就是一个键,然后它的值就是requestHandlers.start。用这样的方式就能将我们requestHandlers中的处理行为放进一个对象里。
接下来就是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;
router.js也要相应的做出改变:
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对象相应的值是否是处理函数,如果是,我们就调用。
现在我们的服务器,路由和处理程序已经联系在一起了。
好了,我们现在要做的就是i,我们不想要我们的浏览器就只是返回"Hello Word"这么没有营养的信息,我们想要我们的处理程序能够返回有用的相关信息,当然我们完全可以这样做,就是在相关的处理程序的最后return相关信息。是的,这样做的确没有错,但是,会出现问题,就是我们的代码会挂!是的,这里就是阻塞问题了。
上面的操作是阻塞的,我们会想要这样子做,就是当程序还在处理一个请求的同时,我们会想要我们的程序继续处理新的请求。不要感到不可思议,这是web编程,想想我们平时使用浏览器的时候,总是喜欢同时打开多个页面是吧,如果像是上面那样做,我们的代码就会出现阻塞,就会出现一个页面必须等待另一个页面打开完成。那么,该怎么才能使用所谓的非阻塞操作呢?
我们先从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;
这里的区别就是我们将response作为参数传给route函数,然后我们继续看route函数:
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;
接着我们再将requestHandlers.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;
这里我们注意一下这个新的模块"child_process”,这个模块有一个exec函数,这是一个非阻塞的函数,它真的是很可怕的东西!为什么这么说呢?我们来举个例子:
var exec = require("child_process").exec; function start(){ var content = "empty"; exec("ls-lah", function(error, stdout, stderr){ content = stdout; } return content; }
我们这里的exec执行的是一个shell命令(如果你有过linux编程,一定知道这是什么东西),将当前目录下的文件信息输出到浏览器,但是我们一旦真正运行,就会发现,输出是empty。为什么呢?因为我们的exec为什么是非阻塞的呢?因为它回调了一个函数stdout(这个函数的作用就是返回输出的结果),然后赋给content,但是因为我们的exec是非阻塞的,就算我们的exec的回调还没结束,但是我们已经执行到下面的return content了,而且这时,content依然是"empty"。所以,明白非阻塞是很重要的,但是我们也要注意非阻塞有可能给我们造成的麻烦。
继续回到我们上面的例子。为什么我们要将response作为参数并且将这部分的工作移到requestHandlers里呢?因为我们想要的是对response的直接响应。之前我们的代码是这样的:我们的服务器将response传给我们的route,我们的route进行处理好,我们的服务器再对resopnse进行响应,现在是这样:我们的服务器依然是将response传给route,但是现在我们的route就已经对response进行响应,这里就少了一层,但是我们的代码的逻辑更加清晰,因为我们处理response的程序和响应response的程序理应放在一块。
进行到这一步,我们发现,我们的应用根本没有什么实际意义!我们还是只是显示一些信息而已!不行,我们必须做些更有用的东西出来!!这次的目标就是允许用户上传文件,然后我们将这个文件显示出来,比如说图片。好了,我们先来看看如何处理POST请求。
我们的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;
我们这里的修改就是多了body,是的,我们只是显示一个文本区供用户输入,然后通过POST请求提交给服务器,接着服务器通过处理程序将内容显示到浏览器。所以,这里需要生成带文本区的表单。
然后我们就来处理upload。这部分当然是采用非阻塞,因为POST请求一般都是非常重的,所谓的重,就是指用户一般都会上传大量的内容,如果采用阻塞的操作,就会使用户的执行动作完全卡死。为了使整个过程都是非阻塞的,node.js会将POST数据分成许多小的数据块,然后通过触发特定的事件,将这些小的数据块传递给回调函数,这里特定的事件有data事件(表示有新的小数据块到了),end事件(表示所有的数据块已经接受完毕)。所以,我们自然就要告诉解释器,当这些事件触发时应该回调哪些函数。我们可以在request上注册监听器,如:
request.addListener("data", function(chunk){ ... }); request.addListener("end", function(){ ... });
问题来了,就是这部分代码应该放在哪里?这种问题当我们在不断扩展自己程序的功能时非常常见,因为我们必须处理好我们每个模块应该要处理的功能。这里的话,应该是放在服务器里,因为服务器应该是获取POST请求的数据并进行处理然后交给应用层处理。所以我们的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事件会不断收集数据,最后将所有的数据块传递给end事件,end事件中调用的是route函数,因为这是在所有的数据块接收完毕后才执行的动作。这里还有一个地方,就是每次数据到达的时候都会有输出日志,这对于最终的生产环境来说是不好的,但是对于开发阶段来说却能让我们更加直观的追踪我们收到的数据。
接着我们要进行的是upload功能,于是我们处理数据的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:
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请求中的数据感兴趣的只是里面的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: " + postData); querystring.parse((postData)text);
response.end(); } exports.start = start; exports.upload = upload;
以上的例子只是说明我们该处理文件(text数据),但是我们最终的目标是要处理图片,所以,我们需要用到formidable模块,但是这些模块并不是我们的node.js默认的模块,我们需要使用外部模块,我们需要下载并安装,于是我们可以通过这样的命令:
npm install formidable;
这样我们的node.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();
}
});
}
exports.start = start;
exports.upload = upload;
exports.show = show;
这里我们的requestHandlers.js只需做上面一样的修改就行,添加一个专门处理文件读取的fs模块就行。
接下里就是实现我们的最终要求了。我们要做的第一步就是在/start表单中添加一个上传元素,这个的实现很简单,就是将我们的表单进行修改就行,添加一个文件上传组件(如果是对html的表单上的组件不熟悉的话,抱歉,这里实在是没有多少篇幅可以讲这部分的内容,还请自行百度),如:
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>';
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传递给formidable的form.parse函数,但是我们有的只是response和postData数组,所以我们只能将request一路从服务器通过路由接着传递给我们的处理程序。现在的我们已经可以将所有有关于postData的部分从我们的服务器和处理程序中移除了,因为它们对于我们的上传文件已经没有什么作用了。显示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, request);
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
然后是router.js:
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;
formidable会将我们的上传文件自动保存到/tmp目录中,我们需要做的就是保证保存成/tmp/test.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>';
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) {
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有了一定的初步了解了。