Node入门

####一个基于nodejs的web应用
功能要求
- 用户可以通过浏览器使用应用。
- 当用户请求*http://domain/start*时,可以看到一个欢迎页面,页面上有一个文件上传的表单。
- 用户可以选择一个图片并提交表单,随后文件将被上传到*http://domain/upload*,该页面完成上传后会把图片显示在页面上。
模块分析
- 由于提供web页面,因此需要HTTP服务器
- 对于不同的请求,根据请求的URL,服务器给予不同的响应,因为需要一个路由,用于把请求对应到请求处理程序(request handler)
- 路由还应该能处理post数据,并且把数据封装成更友好的格式传递给请求处理程序,因为需要请求数据处理功能。
- 当请求被服务器接收并通过路由传递之后,需要可以对其进行处理,因为我们需要最终的请求处理程序。
- 最后,用户需要上传图片,所以我们需要上传处理功能。

#### 开始构建
##### 一、http服务器
首先创建一个用于启动我们的应用的主文件和一个保存着我们http服务器代码的模块。此模块为`server.js`
```
var http = require('http');

function start(){
function onRequest(request,response){
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("hello world");
response.end();
}
http.createServer(onRequest).listen(8000);

console.log('done');
}

exports.start = start;

```
接下来我们建立一个主入口文件为`index.js`
```
var server = require('./server.js');
server.start();
```
再一次运行`node index.js`,也可以在页面上看到hello world。

##### 二、建立路由
我们要为路由提供请求的URL和其他需要的GET以及POST参数,随后路由需要根据这些数据来执行相应的代码。
所以我们需要查看HTTP请求,从中提取出请求的URL以及GET/POST参数。暂时我们将这一功能放在服务器模块。我们需要的所有的数据都会包含在request对象中,但是为了解析这些数据我们需要额外的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"]
```
这个时候我们需要修改`server.js`文件来获取这些参数,然后将这些参数传入到路由中。首先获取`pathname`,在`server.js`的onRequest函数中添加以下代码
`var pathname = url.parse(request.url).pathname;`
到这里我们可以区分出不同的URL了,但是对不同的URL请求还没有设置不同的请求处理程序,首先我们来为不同的URL写一个路由程序为router.js。
```
function route(pathname){
console.log('you have goto' + pathname);
}
exports.route = route;
```
这里采用的是在`server.js`中引入`router.js`然后将`router.route`传入`server.start()`,可以看到index.js的代码如下:
```
var server = require('./server.js');
var router = require('./router.js');

server.start(router.route);
```
`server.js`的代码如下:
```
var http = require('http');
var url = require('url');

function start(route){
function onRequest(request,response){
var pathname = url.parse(request.url).pathname;
route(pathname);
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("hello world");
response.end();
}
http.createServer(onRequest).listen(8000);

console.log('done');
}

exports.start = start;
```
其实我们这里用到了函数式编程的思想,因为我们可以直接在`server.js`直接引入`router.js`模块,然后调用router对象中的route函数,但是这里并没有这样做,是因为`server.js`并不是真正的需要`router.js`而仅仅是需要它来做一个动作,所以我们并不需要真正的引入它。

##### 三、请求处理程序
我们首先建立请求处理程序requesthandler.js模块:
```
function start() {
console.log("Request handler 'start' was called.");
}

function upload() {
console.log("Request handler 'upload' was called.");
}
exports.start = start;
exports.upload = upload;
```
在这个模块中先定义两种不同的处理方法,然后将这两个方法导出。然后在`index.js`模块中引入`requesthandler.js`模块。并设置一个对象用来保存相对应的方法,`index.js`如下:
```
var server = require('./server.js');
var router = require('./router.js');
var requestHandlers = require("./requestHandlers");
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;

server.start(router.route,handle);
```
这样我们就可以将handle直接传递给start函数,在server.js中再将handle对象传入route函数中,这样就可以做到让router.js来决定执行哪一个请求处理函数。
```
var http = require('http');
var url = require('url');

function start(route,handle){
function onRequest(request,response){
var pathname = url.parse(request.url).pathname;
route(handle,pathname);
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("hello world");
response.end();
}
http.createServer(onRequest).listen(8000);

console.log('done');
}

exports.start = start;

```
router.js中先判断一下是否handle[pathname] === 'function'如果是就执行对应的方法。
```
function route(handle,pathname){
console.log('you have goto' + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname]();
}else {
console.log('no request handler found for' + pathname);
}
}

exports.route = route;
```
这里已经实现不同的url能有不同请求处理程序,但是还没有在页面上进行响应。我们可以直接采用`return ()`的方法来返回响应的内容,但是这种方法在未来如果有阻塞操作的时候就会发生问题,所以我们采取另外一种方式。将*response*对象(从服务器的回调函数*onRequest()*获取)通过请求路由传递给请求处理程序。 随后,处理程序就可以采用该对象上的函数来对请求作出响应。首先在`server.js`上将response传递给route函数,将所有response操作都删除。把这些response操作都放在router.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(8000);
console.log('done');
}
exports.start = start;
```
在router.js中,也是将传入的response对象再传递到handle[pathname]对应的函数中。
```
function route(handle,pathname,response){
console.log('you have goto' + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response);
}else {
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404 Not found");
response.end();
console.log('no request handler found for' + pathname);
}
}
exports.route = route;
```
在requesthandler.js中接收传入的response对象,然后引入了一个node.js中一个模块,*child_process*。之所以用它,是为了实现一个既简单又实用的非阻塞操作:*exec()*。exec()的作用就是用Node.js来执行一个shell命令。在下面的代码中,我们用它来获取当前目录下所有的文件(“ls -lah”),然后,当*/start*URL请求的时候将文件信息输出到浏览器中。
```
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;
```
这里用了exec()一个简单的非阻塞操作,把response的操作放在它的回调函数中。这样就达到了我们要求非阻塞的目的。

##### 四、添加交互(处理post请求)
我们需要实现的是用户选择一个文件,上传该文件,然后在浏览器中看到上传的文件。 为了保持简单,我们假设用户只会上传图片,然后我们应用将该图片显示到浏览器中。
首先我们在请求/start时会返回一个文本框和一个提交按钮,当用户点击提交按钮的时候使用post方法,此时页面显示的是用户在文本框里输入的文字。
```
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;
```
这个时候已经能够在请求/start时返回相应的页面,但是还没有为/upload添加效果。我们采用异步回调来实现非阻塞地处理post请求的数据。因为post请求一般都比较”重“——用户可能会输入大量的内容,用阻塞的方式处理大量数据请求必然会导致用户操作的阻塞。所以在node.js中会将post数据拆分成很多小的数据块,然后通过触发特定的事件,将这些小数据块传递给回调函数。这里特定的事件是data事件(表示新的小数据块到达了),以及end事件(表示所有的数据已经接收完毕。我们需要做的是告诉Node.js当这些事件触发的时候,回调哪些函数。所以我们的实现思路是在server.js中设置data和end的回调函数,在data事件回调中收集所有的post数据,当接收到所有数据,触发end事件后,其回调函数调用请求路由,并将数据传递给它,然后请求路由将该数据传递给请求处理程序。所以在server.js中设置data和end事件。
```
var http = require('http');
var url = require('url');

function start(route,handle){
function onRequest(request,response){
var postDate = '';
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
request.setEncoding("utf8");
request.addListener('data',function(postDateChunk){
postDate +=postDateChunk;
console.log('received post data chunk'+postDateChunk)
});
request.addListener('end',function(){
route(handle,pathname,response,postDate);
})
}
http.createServer(onRequest).listen(8000);

console.log('done');
}

exports.start = start;
```
然后在router.js中传入postDate,并且将postData传入请求处理程序中,代码如下:
```
function route(handle,pathname,response,postDate){
console.log('you have goto' + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response,postDate);
}else {
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404 Not found");
response.end();
console.log('no request handler found for' + pathname);
}
}

exports.route = route;
```
最后修改requesthandler.js将upload函数添加postDate参数,然后把数据展示出来,代码如下:
```
function upload(response,postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(postData);
response.end();
}
```
这个时候就实现了在/start时显示文本框和提交按钮,在提交之后只显示输入的内容。但是我们是把请求的整个消息体传递给了请求路由和请求处理程序,但是我们只应该把post数据中的text部分提取出来。所以我们引入一个新的模块querystring,在requesthandler.js中改变response.write为
`response.write(querystring.parse(postDate).text)`。这就完整的完成的处理post请求的功能。

##### 添加交互(处理文件上传)
首先我们要引入一个外部的模块formidable,它能够解析上传的文件数据也就是处理post数据。对于formidable的介绍在本文末尾。接下来我们先安装这个模块。
`npm install formidable`
由于我们要把上传的文件显示到浏览器中,我们要先把它保存在本地的磁盘中,再读出显示到浏览器中。我们就需要使用fs模块将文件读取到服务器中。然后添加一个/showURL请求处理程序,该程序直接将读取并保存在/tmp/test.png的文件展示在浏览器中。在requesthandler.js新增的代码如下:
```
var fs = require('fs');
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.show = show;
```
这个时候我们就可以打开localhost:8888/show就可以看到我们保存在/tmp/test.png文件。最后我们要实现的是先上传文件,然后再显示,所以要做的事情有
- 1、在/start表单中去掉文本框,新增一个添加文件上传元素。
```
'<form action="/upload"method="post"enctype="multipart/form-data">'+
'<input type="file" name="upload">'
```
enctype属性规定在发送到服务器之前应该如何对表单数据进行编码,如果设置为multipart/form-data就表示不对字符进行编码,**并且规定在使用包含文件上传控件的表单时,必须使用该值**。
- 2、将node-formidable整合到我们的upload请求处理程序中,用于将上传的图片保存到/tmp/test.png
我们需要在upload处理程序中对上传的文件进行处理,这样的话,就需要将request对象传递给node-formidable的form.parse函数,但是我们现在只有response和postDate,所以我们这里可以改变原来的方法,不再传递postData,直接传递request对象。首先移除对postData的处理以及request.setEncoding(这部分node-formidable自身会处理),并传递request对象给路由。

```
var http = require('http');
var url = require('url');
var formidable = require('formidable');


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(8000);

console.log('done');
}

exports.start = start;
```

然后在router.js中传入request对象
```
function route(handle,pathname,response,request){
console.log('you have goto' + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response,request);
}else {
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404 Not found");
response.end();
console.log('no request handler found for' + pathname);
}
}

exports.route = route;
```
现在就可以在upload函数中使用request对象了
- 3、将上传的图片内嵌到/uploadURL输出的html中。

```
var querystring = require('querystring');
var fs = require('fs');

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" enctype="multipart/form-data">'+
'<input type="file" name="upload">'
'<input type="submit" value="submit text" />'+
'</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,flies){
console.log('parsing done');
fs.renameSync(files.upload.path,"/tmp/test.png");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("received image :<br/>");
response.write("<img src='/show/'>");
response.end();
})

}
function show(response,request) {
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;
```
终于完成了!
##### 遇到问题
当我在运行最后的结果时,遇到一个错误是cross-device link not permitted,意思是跨磁盘操作是不允许的。当我把tmp/test.png创建到C盘下就不会报错。

##### 附录: node-formidable模块
formidable模块是专门用来解析表单数据的,特别是上传的数据。下面来看一下formidable的常用API以及使用方法。
- 创建一个新的上传表单
`var form = new formidable.IncomingForm()`
- 设置编码
`form.encoding = 'utf-8'`
- 设置传入的文件的目录,你也可以通过fs.rename()来移动它,默认的是os.tmpDir();
`form.uploadDir = "/my/dir"`
- 转换请求中包含的表单数据,callback会包含所有字段域和文件信息。
`form.parse(request,function(err,fields,files){
//...
})`

##### 参考资料
[node入门](http://www.nodebeginner.org/index-zh-cn.html) 强烈推荐!
[node-formidable详解](https://cnodejs.org/topic/4f16442ccae1f4aa2700104d)
[node.js API](https://nodejs.org/dist/latest-v7.x/docs/api/)

posted @ 2017-02-07 18:47  罗Q  阅读(225)  评论(0编辑  收藏  举报