博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
nodeJs学习过程之一个图片上传显示的例子
阅读量:5162 次
发布时间:2019-06-13

本文共 19626 字,大约阅读时间需要 65 分钟。

目标

1. 在浏览器地址栏输入“http://demos/start”,进入欢迎页面,页面有一个文件上传表单;

2. 选择一张图片并提交表单,文件被上传到"http://demos/uploads"上传完成把该图片显示在页面上。

功能模块分解

1. 需要提供欢迎页,所以需要一个http服务器;

2. 对于不同请求,根据url,服务器能给与不同响应,需要路由,把请求对应到相应的请求处理程序(request handler)

3. 需要请求处理程序;

4. 路由处理post数据,并把数据封装成更加友好的格式传递给处理程序,需要数据处理功能;

5. 需要视图逻辑供请求处理程序使用,以便将结果返回给浏览器;

6. 需要上传处理功能处理上传细节。

如果是php实现,我们需要apache http服务器并配置mod_php5模块,那么整个“接收请求并回复“并不需要php来做。而使用nodejs,不仅仅是实现应用,同时还实现了http服务器。

一个基础的http服务器

nodejs支持模块化,有利于编写干净的代码。除了内置的模块,我们可以将自己的不同功能的代码放进不同模块中,说白了就是写在不同文件中。

创建js文件取名server.js,写入代码如下:

1 var http = require('http');2 3 http.createServer(function(req,res){4   res.writeHead(200,{'Content-Type':'text/html'});5   res.write('

Node.js

');6 res.end("");7 }).listen(3000,'127.0.0.1');
node server.js

浏览器输入"http://localhost:3000",显示”Node.js“。

服务器简单分析

第一行,require请求nodejs内置的http模块,赋给http变量;接着调用http模块的createServer方法,返回一个对象,这个对象有一个叫做listen的方法,这个方法的参数指定http服务器监听的端口号。createServer方法的匿名函数是该方法仅仅需要的一个参数,js中函数和变量一样可以传递。

使用这种方式是因为nodejs是事件驱动的。

当我们使用php时,任何时候每当有请求进入,apache就为这一新请求新建一个进程,并从头到尾的执行php脚本。htpp.createServer不仅仅是建一个侦听某端口的服务器,我们还想要他在收到http请求时做点什么。而nodejs是异步的:请求任何时候都可能到达,但是他却跑在一个单进程中。

当一个请求到达3000端口时怎么控制流程呢?这时候nodejs/javascript的事件驱动机制派上用场了。

我们向创建服务器的方法传递了一个函数,任何时候服务器收到请求,这个函数就会被调用,被用作处理请求的的地方。

服务器如何处理请求

当服务器加收请求回调的匿名函数被触发的时候,有两参数传入:request和response。利用这两个对象的方法处理请求的细节,并返回应答。

使用response.writeHead()发送一个http状态200和http头的内容类型等,使用response.write()在响应主体中发送文本,而使用response.end()完成应答。

封装为模块:

1 var http = require('http'); 2  3 function serverStart(){ 4         http.createServer(function(req,res){ 5             console.log('http request recieve'); 6              7           res.writeHead(200,{'Content-Type':'text/html'}); 8           res.write('

Node.js

'); 9 res.end("");10 }).listen(3000,'127.0.0.1');11 12 console.log('http server start on port 3000');13 }14 15 exports.serverStart = serverStart;

第一行http是内置的模块,这里封装了我们自己的模块,通过关键字exports露出接口方法,模块名为该文件名server,之后”server模块“可以被其他模块引用。

创建index.js文件,写入代码并引用server模块:

1 var server = require("./server");2 3 server.serverStart();

 

对请求进行路由选择

为路由提供请求的url和其他GET和POST参数,路由根据这些数据来分发请求到具体的处理程序。因此,我们需要查看并解析http请求,提取出url和get/post参数,暂且将这一解析功能归于http服务器一部分。请求数据都包含在request对象中,引入内置模块url和querystring来解析它。

1 var http = require('http'); 2 var url = require('url'); 3  4 function serverStart(){ 5         http.createServer(function(req,res){ 6             var pathname = url.parse(req.url).pathname; 7             console.log('http request for '+pathname+' recieved'); 8              9           res.writeHead(200,{'Content-Type':'text/html'});10           res.write('

Node.js

'); 11 res.end("");12 }).listen(3000,'127.0.0.1');13 14 console.log('http server start on port 3000');15 }16 17 exports.serverStart = serverStart;

通过第6行的解析,不同的请求路径在第7行输出不同的pathname。

建立一个名为router.js的文件,编写路由代码如下:

1 function route(pathname){2   console.log('about to route a request for'+pathname);3 }4 5 exports.route = route;

服务器应该知道路由的存在,并加以组合有效地利用。这里可以采用硬编码方式或依赖注入的方式实现组合,后者更好。

改造server.js如下:

1 var http = require('http'); 2 var url = require('url'); 3  4 function serverStart(route){ 5         http.createServer(function(req,res){ 6             var pathname = url.parse(req.url).pathname; 7             console.log('http request for '+pathname+' recieved'); 8              9             route(pathname);10             11           res.writeHead(200,{'Content-Type':'text/html'});12           res.write('

Node.js

'); 13 res.end("");14 }).listen(3000,'127.0.0.1');15 16 console.log('http server start on port 3000');17 }18 19 exports.serverStart = serverStart;

改造index.js如下:

1 var server = require("./server");2 var router = require("./router");3 4 server.serverStart(router.route);

编译运行,可看到路由模块被server模块引入。

行为驱动执行

在index中,我们将router对象传递进去,服务器随后调用这个对象的route函数。但其实服务器自身并不需要这个对象,它只是需要执行某个动作,而这个动作由该对象来完成。具体的细节服务器并不关心,因为这是执行这个动作的那个对象的事。这就是函数式编程。但归根结底,到了最后一层总有一个对象是真正关心细节并完成细节,输出结果的。像现实世界的上级把任务给下级,下级给再下级,最终总要有人完成这个任务才好!

路由到处理程序

路由用来转发请求,将请求分发给实际的处理程序,让他们解决请求并回复,因此新建请求处理模块requestHandlers.js,代码如下:

1 function start(){ 2   console.log("request handler 'start' was called"); 3 } 4  5 function upload(){ 6   console.log("request handler 'upload' was called"); 7 } 8  9 exports.start = start;10 exports.upload = upload;

有了这个模块,路由就有路可寻了。

现在需要将处理程序通过一个对象来传递并将对象注入到route()函数中,而javascript的对象是”key:value“组合,满足我们的需要。将存放处理程序的对象引入index,如下:

1 var server = require("./server"); 2 var router = require("./router"); 3 var requestHandlers = require("./requestHandlers"); 4  5 var handler = {}; 6 handler['/'] = requestHandlers.start; 7 handler['/start'] = requestHandlers.start; 8 handler['/upload'] = requestHandlers.upload; 9 10 server.serverStart(router.route,handler);

路径为”/“或"/start"的请求由start处理,"/upload"由upload处理。

将该对象作为参数传给服务器,server.js模块代码改为:

1 var http = require('http'); 2 var url = require('url'); 3  4 function serverStart(route,handle){ 5         http.createServer(function(req,res){ 6             var pathname = url.parse(req.url).pathname; 7             console.log('http request for ''+pathname+'' recieved'); 8              9             route(handle,pathname);10             11           res.writeHead(200,{'Content-Type':'text/html'});12           res.write('

Node.js

'); 13 res.end("");14 }).listen(3000,'127.0.0.1');15 16 console.log('http server start on port 3000');17 }18 19 exports.serverStart = serverStart;

handle对象传递给了route,此时还得修改router模块,如下:

1 function route(handle,pathname){ 2   console.log('about to route a request for'+pathname); 3    4     if(typeof handle[pathname] === 'function'){ 5       handle[pathname](); 6     } 7     else{ 8       console.log('no request handler found for'+pathname); 9     }10 }11 12 exports.route = route;

首先检查对应的处理程序是否存在,存在则直接调用相应的函数,否则提示没有找到对应的函数。这时,服务器,路由,处理程序就联系在一起了。

让请求处理程序作出响应

web程序一般是基于http的请求-应答模式,这样服务器和浏览器可以实现通话。

一种不好的实现方式

这种方式让服务器的函数直接返回展示给用户的信息。这就需要同时修改服务器、路由、请求处理函数的部分代码:

requestHandlers.js

1 function start(){ 2   console.log("request handler 'start' was called"); 3   return "hello start!"; 4 } 5  6 function upload(){ 7   console.log("request handler 'upload' was called"); 8   return "hello upload!"; 9 }10 11 exports.start = start;12 exports.upload = upload;

requestHandlers.js

1 function route(handle,pathname){ 2   console.log('about to route a request for'+pathname); 3    4     if(typeof handle[pathname] === 'function'){ 5       return handle[pathname](); 6     } 7     else{ 8       console.log('no request handler found for'+pathname); 9       return "404 not found!"10     }11 }12 13 exports.route = route;

server.js

1 var http = require('http'); 2 var url = require('url'); 3  4 function serverStart(route,handle){ 5         http.createServer(function(req,res){ 6             var pathname = url.parse(req.url).pathname; 7             console.log('http request for '+pathname+' recieved'); 8                          9             10           res.writeHead(200,{'Content-Type':'text/html'});11           var content = route(handle,pathname);12           res.write(content);  13           res.end("");14         }).listen(3000,'127.0.0.1');15         16         console.log('http server start on port 3000');17 }18 19 exports.serverStart = serverStart;

这样的模式运行的也很好,但当有请求处理程序需要执行非阻塞操作,就“挂”了。

阻塞与非阻塞

修改requestHandlers.js

1 function start(){ 2   console.log("request handler 'start' was called"); 3    4   function sleep(milliSeconds){ 5       var startTime = new Date().getTime(); 6       while(new Date().getTime() < startTime + milliSeconds) 7         ;   8   } 9   10   sleep(10000);11   return "hello start!";12 }13 14 function upload(){15   console.log("request handler 'upload' was called");16   return "hello upload!";17 }18 19 exports.start = start;20 exports.upload = upload;

上述的sleep函数会让方法停顿10秒,然后才会返回结果。实际场景中,需要计算或是查找数据库等阻塞操作非常多。

现在通过start和upload即可模拟阻塞,打开两个空白页面分别输入“localhost:3000/start”和"localhost:3000/upload",先进行start请求在快速切换到量以页面进行upload请求,发现start页面请求恢复用了10秒,而upload也用了10秒。原因是,start中的sleep阻塞了所有其他处理。

而nodejs的特性之一是——单线程,可以在不增加线程情况下,对任务进行并行处理。这里的机制是它使用轮询(event loop)来实现并行操作,所有尽量避免阻塞操作而是用非阻塞操作。要使用非阻塞,需要使用回调,将函数作为参数传递给其他需要费时间处理的函数。

对于nodejs可以形象比喻成“嘿,probablyExpensiveFunction()(需要费时间处理的函数),继续做你的事情,我(nodejs线程)先不等你了,继续去处理后面的代码,请你提供一个回调callBackFunction(),等你处理完了我会去调用该回调函数的。”

一种错误的非阻塞方式

requestHandlers.js

1 var exec = require("child_process").exec; 2  3 function start(){ 4   console.log("request handler 'start' was called"); 5   var content = "empty"; 6    7   exec("ls-lah",function(error,stdout,stderr){ 8       content = stdout; 9   });    10   return content;11 }12 13 function upload(){14   console.log("request handler 'upload' was called");15   return "hello upload!";16 }17 18 exports.start = start;19 exports.upload = upload;

引入另一个模块“child_process”,实现非阻塞操作exec()。

exec()的作用是从nodejs执行一个shell命令,例子中使用它获取当前目录下所有文件("ls-lah"),然后当start请求时将文件信息输出到浏览器。执行该例子,浏览器输出“empty”。原因在于,nodejs是同步执行,而为了非阻塞,exec()使用了回调函数,回调函数作为参数传递给exec(),而exec()是异步操作,即当他还没来得及执行回调,nodejs的同步性致使nodejs执行了exec()的下一个语句,这时候content并没有等来exec()的赋值,因此他还是“empty”。这和javascript中的同步异步很相像,每当需要执行ajax,并且需要ajax的结果时,我们将下一步的代码放在ajax执行成功时的回调里。

非阻塞方式对请求进行响应

我们可以采用值传递的方式将请求处理函数返回的内容再回传给http服务器。上述方式都是将请求处理函数的结果返回给http服务器,而另一种方式是将服务器传递给内容,即将response对象传递给请求处理程序,程序随后采用该对象上的方法对对请求进行响应。

server.js

1 var http = require('http'); 2 var url = require('url'); 3  4 function serverStart(route,handle){ 5         http.createServer(function(req,res){ 6             var pathname = url.parse(req.url).pathname; 7             console.log('http request for '+pathname+' recieved'); 8                                                9           route(handle,pathname,res);10          11         }).listen(3000,'127.0.0.1');12         13         console.log('http server start on port 3000');14 }15 16 exports.serverStart = serverStart;

以上将server.js中有关response的函数调用全部移除,将response对象传给route。

router.js

1 function route(handle,pathname,response){ 2   console.log('about to route a request for'+pathname); 3    4     if(typeof handle[pathname] === 'function'){ 5       return handle[pathname](response); 6     } 7     else{ 8       console.log('no request handler found for'+pathname); 9       response.writeHead(404,{"Content-Type":"text/plain"});10       response.write("404 not found!");11       response.end();12     }13 }14 15 exports.route = route;

requestHandlers.js

1 var exec = require("child_process").exec; 2  3 function start(response){ 4   console.log("request handler 'start' was called");   5    6   exec("ls-lah",function(error,stdout,stderr){ 7        response.writeHead(200,{"Content-Type":"text/plain"}); 8        response.write(stdout); 9        response.end();10   });    11   12 }13 14 function upload(response){15   console.log("request handler 'upload' was called");16   17   response.writeHead(200,{"Content-Type":"text/plain"});18     response.write("hello upload");19     response.end();20 }21 22 exports.start = start;23 exports.upload = upload;

处理程序接收response对象,由该对象做出直接响应。在此编译,打开浏览器请求,“localhost:3000/start”不会对"localhost:3000/upload"造成阻塞。

更有用的场景

 

处理post请求

1 var exec = require("child_process").exec; 2  3 function start(response){ 4   console.log("request handler 'start' was called");   5    6   var body = ''+''+''+'
'+'
'+ 7 '
'+'
'; 8 9 response.writeHead(200,{"Content-Type":"text/html"});10 response.write(body);11 response.end(); 12 13 }14 15 function upload(response){16 console.log("request handler 'upload' was called");17 18 response.writeHead(200,{"Content-Type":"text/plain"});19 response.write("hello upload");20 response.end();21 }22 23 exports.start = start;24 exports.upload = upload;

以上是修改后的requestHandlers.js,运行后是个简单的表单,包括文本域和提交按钮。post数据有时候会很大,为使过程不阻塞,nodejs会将post数据拆分成很多很小的数据块,然后通过触发特定的事件,将这些小数据块传给回调函数。特定事件有data(表示新的小数据块到了)和end(表示所有数据块都已经接收完毕)。这时候我们需要告诉nodejs事件触发的时候调用哪些回调函数,通过什么方式告诉呢?在request对象上注册监听器(listener)来实现。获取所有来自请求端的数据并将数据传给应用层处理,是http服务器的事。所以直接在server里处理post数据,然后将最终的数据传给路由和请求处理器,让他们来进一步处理。

server.js

1 function serverStart(route,handle){ 2         http.createServer(function(req,res){ 3             var postData = ""; 4             var pathname = url.parse(req.url).pathname; 5             console.log('http request for '+pathname+' recieved'); 6              7             request.setEncoding("utf8"); 8              9             request.addListener("data",function(postDataChunk){10               postData += postDataChunk;11               console.log('received post data chunk'+postDataChunk+".");12             });13             14             request.addListener("end",function(postDataChunk){15               route(handle,pathname,res,postData);16             });                                      17                    18         }).listen(3000,'127.0.0.1');19         20         console.log('http server start on port 3000');21 }22 23 exports.serverStart = serverStart;

上述代码做了3件事,首先设置接收数据格式为utf8,然后绑定data事件的监听器,用于接收每次传递的新数据块,复制给postData,最后将路由调用移到end中,确保数据接收完成在触发并且只触发一次。

requestHandlers.js

1 var querystring = require("querystring"); 2 function start(response,postData){ 3   console.log("request handler 'start' was called");   4    5   var body = ''+''+''+'
'+'
'+ 6 '
'+'
'; 7 8 response.writeHead(200,{"Content-Type":"text/html"}); 9 response.write(body);10 response.end(); 11 12 }13 14 function upload(response,postData){15 console.log("request handler 'upload' was called");16 17 response.writeHead(200,{"Content-Type":"text/plain"});18 response.write("you are sent:"+querystring.parse(postData).text);19 response.end();20 }21 22 exports.start = start;23 exports.upload = upload;

通过querystring筛选出我们感兴趣的部分,以上就是关于post数据处理的内容。

处理文件上传

处理文件上传就是处理post数据,有时候麻烦的是细节处理。所以这里使用现成的方案,即外部模块node-formidable,用nmp命令安装:

nmp install formidable

该模块做的就是通过http post请求提交的表单,在nodejs中可以被解析。我们创建一个IncomingForm,它是对提交的表单的抽象表示,之后就可以用他来解析request对象,获取需要的字段数据。关于该模块具体信息及工作原理,其官网有实例。

现在的问题是保存在本地硬盘的内容如何显示在浏览器中。我们需要将其读取到服务器,使用fs模块完成。

1 var querystring = require("querystring"), 2     fs = require("fs"); 3 function start(response,postData){ 4   console.log("request handler 'start' was called");   5    6   var body = ''+''+''+'
'+'
'+ 7 '
'+'
'; 8 9 response.writeHead(200,{"Content-Type":"text/html"});10 response.write(body);11 response.end(); 12 13 }14 15 function upload(response,postData){16 console.log("request handler 'upload' was called");17 18 response.writeHead(200,{"Content-Type":"text/plain"});19 response.write("you are sent:"+querystring.parse(postData).text);20 response.end();21 }22 23 function show(response,postData){24 console.log("request handler 'show' was called");25 fs.readFile("/tmp/test.png","binary",function(error,file){26 if(error){27 response.writeHead(500,{"Content-Type":"text/plain"});28 response.write(error+"\n");29 response.end();30 }else{31 response.writeHead(200,{"Content-Type":"image/png"});32 response.write(file,"binary");33 response.end();34 }35 });36 }37 38 exports.start = start;39 exports.upload = upload;40 exports.show = show;

将其添加到index.js的路由映射表中:

1 var server = require("./server"); 2 var router = require("./router"); 3 var requestHandlers = require("./requestHandlers"); 4  5 var handler = {}; 6 handler['/'] = requestHandlers.start; 7 handler['/start'] = requestHandlers.start; 8 handler['/upload'] = requestHandlers.upload; 9 handler['/show'] = requestHandlers.show;10 11 server.serverStart(router.route,handler);

编译,浏览器请求即可看见图片。

最后,

  需要在/start表单添加文件上传元素

  将formidable整合到upload中用于将上传的图片保存到指定路径

  将上传的图片内嵌到/upload url输出的html中

第一项,移除此前的文本区,添加上传组件和“multipart/form-data”编码类型:

1 var querystring = require("querystring"), 2     fs = require("fs"); 3 function start(response,postData){ 4   console.log("request handler 'start' was called");   5    6   var body = ''+''+''+'
'+ 7 '
'+'
'; 8 9 response.writeHead(200,{"Content-Type":"text/html"});10 response.write(body);11 response.end(); 12 13 }14 15 function upload(response,postData){16 console.log("request handler 'upload' was called");17 18 response.writeHead(200,{"Content-Type":"text/plain"});19 response.write("you are sent:"+querystring.parse(postData).text);20 response.end();21 }22 23 function show(response,postData){24 console.log("request handler 'show' was called");25 fs.readFile("/tmp/test.png","binary",function(error,file){26 if(error){27 response.writeHead(500,{"Content-Type":"text/plain"});28 response.write(error+"\n");29 response.end();30 }else{31 response.writeHead(200,{"Content-Type":"image/png"});32 response.write(file,"binary");33 response.end();34 }35 });36 }37 38 exports.start = start;39 exports.upload = upload;40 exports.show = show;
View Code

这时候我们需要在upload中对请求中的上传文件进行处理,这样的话需要将request传给node-formidable的form-parse函数。由于nodejs不会对数据作缓冲,于是必须将request对象传入。

server.js:

1 var http = require('http'); 2 var url = require('url'); 3  4 function serverStart(route,handle){ 5         http.createServer(function(req,res){ 6              7             var pathname = url.parse(req.url).pathname; 8             console.log('http request for '+pathname+' recieved'); 9             10           route(route,handle,res,req);11                    12         }).listen(3000,'127.0.0.1');13         14         console.log('http server start on port 3000');15 }16 17 exports.serverStart = serverStart;
View Code

router.js:

1 function route(handle,pathname,response,request){ 2   console.log('about to route a request for'+pathname); 3    4     if(typeof handle[pathname] === 'function'){ 5       return handle[pathname](response,request); 6     } 7     else{ 8       console.log('no request handler found for'+pathname); 9       response.writeHead(404,{"Content-Type":"text/plain"});10       response.write("404 not found!");11       response.end();12     }13 }14 15 exports.route = route;
View Code

将上传和重命名的操作放在一起:

1 var querystring = require("querystring"), 2     fs = require("fs"), 3     formidable = require("formidable"); 4 function start(response){ 5   console.log("request handler 'start' was called");   6    7   var body = ''+''+''+'
'+ 8 '
'+'
'; 9 10 response.writeHead(200,{"Content-Type":"text/html"});11 response.write(body);12 response.end(); 13 14 }15 16 function upload(response,postData){17 console.log("request handler 'upload' was called");18 19 var form = new formidable.IncomingForm();20 form.parse(request,function(error,fields,files){21 fs.renameSync(file.upload.path,"/tmp/test.png");22 response.writeHead(200,{"Content-Type":"text/html"});23 response.write("image:
");24 response.write("");25 response.end();26 });27 28 }29 30 function show(response,postData){31 console.log("request handler 'show' was called");32 fs.readFile("/tmp/test.png","binary",function(error,file){33 if(error){34 response.writeHead(500,{"Content-Type":"text/plain"});35 response.write(error+"\n");36 response.end();37 }else{38 response.writeHead(200,{"Content-Type":"image/png"});39 response.write(file,"binary");40 response.end();41 }42 });43 }44 45 exports.start = start;46 exports.upload = upload;47 exports.show = show;
View Code

好,编译,重启浏览器。选择本地图片,上传并显示!

 

结构图如下:

 

该实例有关的技术点:

  服务器端javascript

  函数式编程

  阻塞与非阻塞

  单线程

  回调

  事件

  nodejs模块

没有介绍到的还有数据库操作、单元测试、开发外部模块等。

 

注:本文摘抄自《nodejs入门》

 

 

 

转载于:https://www.cnblogs.com/thingk/p/3434032.html

你可能感兴趣的文章
EMS-Demo 雇员管理系统演示
查看>>
软件工程第二次作业——心得体会(结对编程)
查看>>
ORA-12560: TNS: 协议适配器错误 ORA-12154: TNS: 无法解析指定的连接标识符
查看>>
读书印记 - 《菊与刀》
查看>>
第一个小demo: spring boot + mybatis + thymeleaf
查看>>
mysql获取字段信息
查看>>
Tomcat 网站部署(三)
查看>>
JS实现全选与取消 Jquery判断checkbox是否被选中
查看>>
如果重新设计网络,有没有可能合并IP地址跟MAC地址?
查看>>
德州扑克总纲
查看>>
linux下password的用法
查看>>
[poj1986]Distance Queries(LCA)
查看>>
BPM配置故事之案例11-操作外部数据源
查看>>
uniGUI试用笔记(一)
查看>>
漫谈python中的搜索/排序
查看>>
js_类数组转化为数组
查看>>
centos 7 安装 rvm 超时
查看>>
类库间无项目引用时,在编译时拷贝DLL
查看>>
module 'socket' has no attribute的解决方案
查看>>
Java NIO vs. IO
查看>>