zoukankan      html  css  js  c++  java
  • 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 + " ");
    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 + " ");
    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/)

  • 相关阅读:
    Java模拟ATM运行过程(第一版)
    双色Hanoi塔问题及判断指令
    一点编程风格
    游戏程序员养成计划
    OGRE编译
    Python学习收藏
    case 语句注意
    [c、c++]宏中"#"和"##"的用法(zz)
    网络代码平台tortoise svn+google code
    使用delegate进行解耦
  • 原文地址:https://www.cnblogs.com/luoqian/p/6375528.html
Copyright © 2011-2022 走看看