前言

刚刚看完了《Node.js入门》,参考node官网和一些相关文章,终于感觉和node亲近了一步,迫不及待的想写篇文章分享一下,希望本文的读者读完后像读了一本好书一样,获益良多。

ps:《Node.js入门》一书仅有38页,是一本很好的入门书籍,如果你也是刚上手node,和我一样手中有一本《深入浅出node.js》相似的高级书籍,但感觉读起来晦涩难懂,不如试试《Node.js入门》吧,会感觉豁然开朗,

借用《Node.js入门》中的编程应用,稍作修改,加上自己的一些思考,讲一下“异步,事件驱动与回调,非阻塞”这些使node.js轻便和高效的特点。特点穿插在应用中讲解。如有错误,欢迎指教!

应用目标:

• 用户可以通过浏览器使用我们的应用。
• 当用户请求http://domain/start时,可以看到一个欢迎页面,页面上有一个 文件上传的表单。
• 用户可以选择一个图片并提交表单,随后文件将被上传到 http://domain/upload,该页面完成上传后会把图片显示在页面上。

实现思路

先来思考一下怎样实现这样一个应用呢?
--啊,要实现应用了,我我...还不知道代码写在哪里,
首先我们要用node跑起js来。
--看用户的请求,这个应用是是跑在服务器的,node要链接服务器吗?
node中可以加载‘http’模块,可以让我们轻松的搭建服务器,真的一点也不难哦。
--还可以学习模块的加载,简直是福利多多,咦,请求页面和显示页面不是一个路径,这个要怎么实现呀?
我们可以创建一个路由模块,对不同的请求有不同的处理,再用总文件将请求路由到相应的处理程序。
--哇哦,有了这个路由机制,我们就可以将start 的路由处理程序设置为上传图片,将upload的处理程序设置为显示图片了
没错,大体的思路就是这样,让我们开始上手吧!

应用实现:

跑起node

首先要让node跑起js来
我们知道,node.js是一个基于chrome V8的一个javaScript运行环境,所以我们这里说的是让node.js跑起js来,而不是让node.js跑起来。
我们先创建一个 helloworld.js 程序,内容为:

console.log("Hello World");

然后在命令行终端中用 node 运行本文件:

node helloworld.js

终端中就会运行出js的结果:Hello World

创建服务器

接下来我们来创建服务器,创建一个server.js文件:

var http = require("http");//引入node内置的‘http’模块
http.createServer(function(request, response) {//创建一个服务器,并在创建方法中规定服务器触发请求的回调函数
    response.writeHead(200, {"Content-Type": "text/plain"});//规定响应头部信息(状态码和文本格式)
    response.write("Hello World");//设定了响应的内容
    response.end();//响应结束
}).listen(8888);//设定服务器监听8888端口。
console.log("Server has started.");

用node跑一下:

node server.js

终端的输出结果为:

Server has started.
Request received.

浏览器端访问:http://localhost:8888/ 显示结果为:

Hello World

代码解读:

1.模块调用

我们把一定实现一定功能的代码放在一个文件里,这个文件也就是一个模块,要实现模块间的调用,就需要模块导出和模块引入,
这里‘http’模块是node内置的,他自身有导出功能,(一会我们还要自己实现模块的导出功能),我们引入了‘http’模块,便能应用它的功能了。

2.事件回调

http的createServer方法,需要传入一个参数,这个参数是一个函数(js中函数是可以作为参数传递的),当请求事件发生时便会触发这个函数,这便是回调函数,回调函数可以是匿名的(就像我们这里一样),也可以是命名的。
事件回调就是:知道有一些事情随时可能发生,回调函数时刻准备着对将要发生的事件作出响应

3.http相关解读:

  • createServer函数参数解读:

createServer的函数参数中有两个参数,第一个表示请求,可以规定请求相关的内容(这里用'request'表示,也可以用'req'...叫什么都可以),第二个表示响应,可以规定响应相关的内容(这里用'response'表示).

  • createServer返回值解读:

createServer返回一个http.server的实例,通过这个server实例调用listen方法,规定了监听的端口号。

加入路由

我们设置路由及相应处理程序,并把请求和他们关联起来
这里有4个程序,需要都看完才能理解,建议大家亲自跑一下。

四个程序关联图:
clipboard.png

在服务器文件server.js中加入路由处理:

var http = require("http"); 
var url = require("url");//引入node内置的‘url’模块
function start(route, handle) {//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;//将模块暴露出来

说明:url既可以取出路径名,亦可以取出其他的部分,相关的方法如下图:

url拆分字段的方法

创建一个路由文件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;

创建一个路由处理文件requesthandlers.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,将以上三个程序关联起来:

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

用node跑一下:

node index.js

在浏览器先访问:http://localhost:8888/start ,再访问:http://localhost:8888/upload
终端的输出结果为:

Server has started.
Request for /start received.
About to route a request for /start
Request handler 'start' was called.
Request for /upload received.
About to route a request for /upload
Request handler 'upload' was called.  

http://localhost:8888/start 输出:

Hello World

http://localhost:8888/upload 输出:

Hello World

仔细阅读完这四个小程序之后,想必会使用基本的模块导入和导出了吧。
在主程序中,将handle对象的键设为路径,将对象的值设为相应的路由处理函数,将路由和相应的处理对应了起来。

实现逻辑

我们来让start请求上传图片,upload请求显示图片
这两个文件的关联便是一张图片,我们应该先请求start,再请求upload,
我们希望,upload会等待start响应完之后再响应.

让我们先来做个小测试吧,看看如果start请求如果要处理10秒,upload是否会等它执行完,并且我们希望浏览器会根据请求的不同给出不同的响应,而不是永远输出‘Hello world’,


阻塞式响应

我们修改下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);//休眠10000ms,即10s
    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); 
    }
}
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;

index.js不用修改。

终端运行index.js,
在浏览器访问完 http://localhost:8888/start 后立即访问 http://localhost:8888/upload
我们会发现upload也休眠了10s,并输出了我们想要的结果。


事情似乎进展的很好,但是逻辑似乎有点想不通,我没有让upload延时啊,为什么它也有10秒延时呢?作者说是因为:start中有一个模拟的休眠函数,它阻塞了其他的事件处理。


啊哈,node不是宣称自己非阻塞吗?
node表示很无辜:它一向是这样来标榜自己的:“在node中除了代码,所有 一切都是并行执行的”。这句话的意思是说,Node.js可以在不新增额外线程的情况下,依然可以对任务进行 并行处理,也就是说,node有这个并行非阻塞的能力。
node是单线程的,只是说js的执行是单线程的,node的底层还是多线程的。


node构建服务器的流程图:
node构建服务器的流程图

针对这个程序,我的理解是,

node先创建了start的请求,通过start的请求对象调用了底层libuv的事件循环,事件循环中的I/O观察者循环按序处理当前的事件,发现start请求有回调函数(createServer中的回调函数),然后把回调函数传给js线程,执行回调.
我们请求完start,又请求了upload,upload采用同样的方式通过libuv的事件轮训向js线程中传递回调,因为start 的回调函数还没有执行完,所以upload等待start的回调执行完再执行)


上面是一个阻塞操作,怎么样才能非阻塞呢?快让node大显身手吧!
让我们再看一个非阻塞的例子吧:

非阻塞响应

修改requestHandlers.js:

var exec = require("child_process").exec;//引入node的子进程模块
function start() {
    console.log("Request handler 'start' was called."); 
    var content = "empty";
    exec("find /",{ timeout: 10000, maxBuffer: 20000*1024 }, function (error, stdout, stderr) {//子进程执行了一个‘find /’的命令,这个命令时很耗时的
        content = stdout;
        console.log(content);//为了说明结果而在终端打印
    });
  return content;
}
function upload() {
    console.log("Request handler 'upload' was called."); 
    return "Hello Upload";
}
exports.start = start; 
exports.upload = upload; 

如上运行程序,发现start请求页输出 "empty",而upload会立即执行,没有被start阻塞,终端最后才输出content 的,


为什么会是这个结果呢?
我的理解:
还是那副图
node构建服务器的流程图

  1. 首先说明为什么content的内容是初始值,因为js是异步操作的,start 的回调不等内部的子进程执行完就直接返回了,让出了当前进程。

  2. 然后再解释下为什么upload没有被阻塞
    start这次的回调函数内创建了一个子进程(node可以通过创建子进程来实现对多核多cpu的充分利用),所以这个start在经过事件轮询进入回调之后,他并没有阻塞掉这个js的线程,而是另辟蹊径把耗时的操作交给别的进程了,当upload请求时,进入回调函数阶段后,start一返回upload也立即返回了。


我们了解了阻塞之后,再回到程序上来,
现在我们的目标是用在不阻塞upload的情况下,用非阻塞的形式,正确的得到content的内容。

为了拿到正确的结果,我们决定做一个大胆的尝试,把reponse参数转移到处理函数中来,将子进程的结果直接给response,而不是通过return返回,问题就解决了,
上代码:

迁移response

server.js 把response传递给router:

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"}); 
        route(handle, pathname, response);
    }
    http.createServer(onRequest).listen(8888);
    console.log("Server has started."); 
}
exports.start = start;    

router.js 把response传给requestHandlers.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); 
    }
}
exports.route = route;

requestHandlers.js设置response参数:

var exec = require("child_process").exec;
function start(response) {
    console.log("Request handler 'start' was called."); 
    var content = "empty";
    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;  

index.js不用修改

正如所愿,一次访问start和upload,浏览器中,start在一段时间后输出了‘find /’的内容,upload立即返回了‘Hello upload’.
我们终于用非阻塞的方法实现请求啦!


加入postData

接下来呢,我们希望start和upload程序有交互,即把start请求中输入内容,upload将start的内容输出出来,要传递内容,中间必然需要一个东西先把内容给存起来,应用的目的是要上传图片,用什么存图片呢?有点困难,不如我们先把流程跑通,先让它们传递文本。


我们希望应用的模型是这样的:请求start,然后用start上传内容,在start页面点击上传后便触发了upload请求,进而upload页面显示了内容,也就是说,upload请求是在start页面触发的,start页面触发请求后便跳转到upload页面了。哇哦,酷,原来这还是个多页面应用。

当内容是文本时,我们把start提交的内容存入postData中,upload请求就显示postData中的内容。

思考一下,哪里放postData合适呢?我们想一下什么时候存取postData?

  1. 先说存:
    start页面会提交一个表单,表单一提交,也就要存postData,表单一提交,start请求也就完成了,所以我们要在start请求完成时存postData。

  2. 再说取:
    upload请求一触发也就取出了postData,

存取都和请求有关,好像想到了什么,没错!创建服务器时,回调函数中有设置请求的参数request,我们就在server.js中存取将postData设置为中转站。
赶快开始吧:


server.js中我们设置了request,加入了postData:

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 
            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;

router.js加入了postData参数:

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 也加入了postData参数,并且实现了文本上传显示的逻辑:

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: " + querystring.parse(postData).text);//因为我们只关注文本的内容,所以这里专门解析出文本内容。
    response.end();
}
exports.start = start; 
exports.upload = upload;   

不用修改index.js

最终代码实现

感觉离成功很近了,现在的问题就是我们把文本改成图片,文本可以用一个参数存储,图片呢?图片一般是通过路径来操作的,怎么通过路径存储图片呢?怎么知道图片上传后存在哪里呢?node中有什么关于请求资源路径的模块吗?

真的有诶,那就是node-formidable模块,
先把它下载下来:

npm install formidable

看下它如何工作:

var formidable = require('formidable'), 
http = require('http'),
sys = 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(sys.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);

案例并不难,很好理解,就是页面提交了post请求,然后post页面接收到表单,随便选了一个文件,它解析的表单内容如下:
received upload:

{ fields: { title: '' },
  files: 
   { upload: 
      File {
        domain: null,
        _events: {},
        _eventsCount: 0,
        _maxListeners: undefined,
        size: 13435,
        path: '/var/folders/2w/dr7185jx1y78vc215d29dp6c0000gn/T/upload_d50704d47069efb62043db1fdc868ae4',
        name: 'tide.png',
        type: 'image/png',
        hash: null,
        lastModifiedDate: 2016-06-23T11:44:45.463Z,
        _writeStream: [Object] } } }

重点是path字段,它能解析到资源的路径


我们试着想一下,最终应用该如何实现呢?首先start页面可以进行文件的上传,我们选择了一张图片,表单被提交到upload请求,upload可以拿到post请求的资源的路径,然后我们把路径指向的文件给打印出来
开始吧,可是有个问题啊,upload显示内容的操作是在requestHandlers.js处理程序中的,解析文件路径的操作当然也要在处理程序中,可是解析需要拿到request参数,而request参数是在server.js中设置的,这里我们就要像迁移response那样,也去迁移一下request,当然不需要postData了,去掉它。


修改server.js,去掉postData,传递request参数。

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,传递request参数:

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/plain"});
         response.write("404 Not found");
         response.end();
     }
}
exports.route = route;

修改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">'+
        '<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) {
    var form=new formidable.IncomingForm();
    console.log("about to parse");
    form.parse(request,function (error,fields,files) {
        console.log("parsing done");
        fs.readFile(files.upload.path,"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;

index.js不用修改


展示下效果吧:
访问start页面,我选择了一只泰迪熊的照片上传:
上传图片文件
点击Upload file按钮,
当当当当:
显示图片

预祝大家的node之旅愉快!


爱睡觉的小猫咪
310 声望22 粉丝

勤奋的小前端


下一篇 »
Angular入门