node练手小项目:在线聊天系统

新手node入门,用这个小项目练练手,写这篇文章也是为了本身巩固下知识。html

先看效果图:

先是让用户输入名字 get yourself a nickname :)node

clipboard.png

输入好了以后进入,而后随便说点什么,git

clipboard.png
能够多我的在线聊天,github

clipboard.png
也能够上传图片:npm

clipboard.png
更改字体颜色:编程

clipboard.png

制做过程

  • 文件分布:clipboard.png入口文件server.js
    clipboard.png

讲解:

  • 服务器server.js
    先举一个例子:咱们把咱们的服务器脚本放到一个叫作 start 的函数里,而后咱们会导出这个函数。
var http = require("http");

function start() {
  function onRequest(request, response) {
    console.log("Request received.");
    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;

这样,咱们如今就能够建立咱们的主文件 index.js 并在其中启动咱们的HTTP了,虽然服务器的代码还在 server.js 中。数组

建立 index.js 文件并写入如下内容:浏览器

var server = require("./server");

server.start();

正如你所看到的,咱们能够像使用任何其余的内置模块同样使用server模块:请求这个文件并把它指向一个变量,其中已导出的函数就能够被咱们使用了。咱们如今能够把咱们的应用的不一样部分放入不一样的文件里,而且经过生成模块的方式把它们链接到一块儿了。缓存

  • 路由:
    对于不一样的URL请求,服务器应该有不一样的反应。

对于一个很是简单的应用来讲,你能够直接在回调函数 onRequest() 中作这件事情。不过就像我说过的,咱们应该加入一些抽象的元素,让咱们的例子变得更有趣一点儿。bash

处理不一样的HTTP请求在咱们的代码中是一个不一样的部分,叫作“路由选择”——那么,咱们接下来就创造一个叫作 路由 的模块吧。
咱们要为路由提供请求的URL和其余须要的GET及POST参数,随后路由须要根据这些数据来执行相应的代码

咱们须要的全部数据都会包含在request对象中,该对象做为onRequest()回调函数的第一个参数传递。可是为了解析这些数据,咱们须要额外的Node.JS模块,它们分别是url和querystring模块。
如今咱们来给onRequest()函数加上一些逻辑,用来找出浏览器请求的URL路径:

var http = require("http");
var url = require("url");

function start() {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");
    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路径为基准映射处处理程序上。

在咱们所要构建的应用中,这意味着来自/start和/upload的请求可使用不一样的代码来处理。
router.js:

function route(pathname) {
  console.log("About to route a request for " + pathname);
}

exports.route = route;

如你所见,这段代码什么也没干,不过对于如今来讲这是应该的。在添加更多的逻辑之前,咱们先来看看如何把路由和服务器整合起来。
咱们的服务器应当知道路由的存在并加以有效利用。咱们固然能够经过硬编码的方式将这一依赖项绑定到服务器上,可是其它语言的编程经验告诉咱们这会是一件很是痛苦的事,所以咱们将使用依赖注入的方式较松散地添加路由模块
来扩展一下服务器的start()函数,以便将路由函数做为参数传递过去:

var http = require("http");
var url = require("url");

function start(route) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");

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

同时,咱们会相应扩展index.js,使得路由函数能够被注入到服务器中:

var server = require("./server");
var router = require("./router");

server.start(router.route);
bash$ node index.js
Request for /foo received.
About to route a request for /foo

你将会看到应用输出相应的信息,这代表咱们的HTTP服务器已经在使用路由模块了,并会将请求的路径传递给路由

  • 处理函数requestHandlers的模块

并对于每个请求处理程序,添加一个占位用函数,随后将这些函数做为模块的方法导出:

function start() {
  console.log("Request handler 'start' was called.");
}

function upload() {
  console.log("Request handler 'upload' was called.");
}

exports.start = start;
exports.upload = upload;

这样咱们就能够把请求处理程序和路由模块链接起来,让路由“有路可寻”。
仔细想一想,有一大堆东西,每一个都要映射到一个字符串(就是请求的URL)上?彷佛关联数组(associative array)能完美胜任。

引用:不过结果有点使人失望,JavaScript没提供关联数组 -- 也能够说它提供了?事实上,在JavaScript中,真正能提供此类功能的是它的对象。
在C++或C#中,当咱们谈到对象,指的是类或者结构体的实例。对象根据他们实例化的模板(就是所谓的类),会拥有不一样的属性和方法。但在JavaScript里对象不是这个概念。在JavaScript中,对象就是一个键/值对的集合 -- 你能够把JavaScript的对象想象成一个键为字符串类型的字典。

好了,最后再回到代码上来。如今咱们已经肯定将一系列请求处理程序经过一个对象来传递,而且须要使用松耦合的方式将这个对象注入到route()函数中。

咱们先将这个对象引入到主文件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);

正如所见,将不一样的URL映射到相同的请求处理程序上是很容易的:只要在对象中添加一个键为"/"的属性,对应requestHandlers.start便可,这样咱们就能够干净简洁地配置/start和/的请求都交由start这一处理程序处理。

在完成了对象的定义后,咱们把它做为额外的参数传递给服务器,为此将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;

这样咱们就在start()函数里添加了handle参数,而且把handle对象做为第一个参数传递给了route()回调函数。
而后咱们相应地在route.js文件中修改route()函数:

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[pathname]();的表达式

有了这些,咱们就把服务器、路由和请求处理程序在一块儿了

  • 处理程序与服务器
    咱们采用以下这种新的实现方式:相对采用将内容传递给服务器的方式,咱们此次采用将服务器“传递”给内容的方式。 从实践角度来讲,就是将response对象(从服务器的回调函数onRequest()获取)经过请求路由传递给请求处理程序。 随后,处理程序就能够采用该对象上的函数来对请求做出响应。

原理就是如此,接下来让咱们来一步步实现这种方案。

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

相对此前从route()函数获取返回值的作法,此次咱们将response对象做为第三个参数传递给route()函数,而且,咱们将onRequest()处理程序中全部有关response的函数调都移除,由于咱们但愿这部分工做让route()函数来完成。

下面就来看看咱们的router.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);
    response.writeHead(404, {"Content-Type": "text/plain"});
    response.write("404 Not found");
    response.end();
  }
}

exports.route = route;

一样的模式:相对此前从请求处理程序中获取返回值,此次取而代之的是直接传递response对象。

若是没有对应的请求处理器处理,咱们就直接返回“404”错误。

最后,咱们将requestHandler.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;

咱们的处理程序函数须要接收response参数,为了对请求做出直接的响应。

start处理程序在exec()的匿名回调函数中作请求响应的操做,而upload处理程序仍然是简单的回复“Hello World”,只是此次是使用response对象而已。

这时再次咱们启动应用(node index.js),一切都会工做的很好。

  • 处理POST请求
    考虑这样一个简单的例子:咱们显示一个文本区(textarea)供用户输入内容,而后经过POST请求提交给服务器。最后,服务器接受到请求,经过处理程序将输入的内容展现到浏览器中。

/start请求处理程序用于生成带文本区的表单,所以,咱们将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;

这里采用非阻塞方式处理是明智的,由于POST请求通常都比较“重” —— 用户可能会输入大量的内容。用阻塞的方式处理大数据量的请求必然会致使用户操做的阻塞。

为了使整个过程非阻塞,Node.js会将POST数据拆分红不少小的数据块,而后经过触发特定的事件,将这些小数据块传递给回调函数。这里的特定的事件有data事件(表示新的小数据块到达了)以及end事件(表示全部的数据都已经接收完毕)。

咱们须要告诉Node.js当这些事件触发的时候,回调哪些函数。怎么告诉呢? 咱们经过在request对象上注册监听器(listener) 来实现。这里的request对象是每次接收到HTTP请求时候,都会把该对象传递给onRequest回调函数。
以下所示:

request.addListener("data", function(chunk) {
  // called when a new chunk of data was received
});

request.addListener("end", function() {
  // called when all chunks of data have been received
});

问题来了,这部分逻辑写在哪里呢? 咱们如今只是在服务器中获取到了request对象 —— 咱们并无像以前response对象那样,把 request 对象传递给请求路由和请求处理程序。

在我看来,获取全部来自请求的数据,而后将这些数据给应用层处理,应该是HTTP服务器要作的事情。所以,我建议,咱们直接在服务器中处理POST数据,而后将最终的数据传递给请求路由和请求处理器,让他们来进行进一步的处理。

所以,实现思路就是: 将data和end事件的回调函数直接放在服务器中,在data事件回调中收集全部的POST数据,当接收到全部数据,触发end事件后,其回调函数调用请求路由,并将数据传递给它,而后,请求路由再将该数据传递给请求处理程序。

还等什么,立刻来实现。先从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”事件的监听器,用于收集每次接收到的新数据块,并将其赋值给postData 变量,最后,咱们将请求路由的调用移到end事件处理程序中,以确保它只会当全部数据接收完毕后才触发,而且只触发一次。咱们同时还把POST数据传递给请求路由,由于这些数据,请求处理程序会用到。

上述代码在每一个数据块到达的时候输出了日志,这对于最终生产环境来讲,是很很差的(数据量可能会很大,还记得吧?),可是,在开发阶段是颇有用的,有助于让咱们看到发生了什么。

我建议能够尝试下,尝试着去输入一小段文本,以及大段内容,当大段内容的时候,就会发现data事件会触发屡次。
再来点酷的。咱们接下来在/upload页面,展现用户输入的内容。要实现该功能,咱们须要将postData传递给请求处理程序,修改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中,咱们将数据包含在对upload请求的响应中:

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数据并在请求处理程序中处理该数据了。

咱们最后要作的是: 当前咱们是把请求的整个消息体传递给了请求路由和请求处理程序。咱们应该只把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 the text: "+
  querystring.parse(postData).text);
  response.end();
}

exports.start = start;
exports.upload = upload;
  • 处理文件上传
    这里咱们要用到的外部模块是Felix Geisendörfer开发的node-formidable模块。它对解析上传的文件数据作了很好的抽象。 其实说白了,处理文件上传“就是”处理POST数据 —— 可是,麻烦的是在具体的处理细节,因此,这里采用现成的方案更合适点。

使用该模块,首先须要安装该模块。Node.js有它本身的包管理器,叫NPM。它可让安装Node.js的外部模块变得很是方便。经过以下一条命令就能够完成该模块的安装:

npm install formidable
npm info build Success: formidable@1.0.9
npm ok
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;

咱们还须要将这新的请求处理程序,添加到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;
handle["/show"] = requestHandlers.show;

server.start(router.route, handle);

好,最后咱们要的就是:

在/start表单中添加一个文件上传元素
将node-formidable整合到咱们的upload请求处理程序中,用于将上传的图片保存到/tmp/test.png
将上传的图片内嵌到/uploadURL输出的HTML中
须要在upload处理程序中对上传的文件进行处理,这样的话,咱们就须要将request对象传递给node-formidable的form.parse函数。

可是,咱们有的只是response对象和postData数组。看样子,咱们只能不得不将request对象从服务器开始一路经过请求路由,再传递给请求处理程序。 或许还有更好的方案,可是,无论怎么说,目前这样作能够知足咱们的需求。

到这里,咱们能够将postData从服务器以及请求处理程序中移除了 —— 一方面,对于咱们处理文件上传来讲已经不须要了,另一方面,它甚至可能会引起这样一个问题: 咱们已经“消耗”了request对象中的数据,这意味着,对于form.parse来讲,当它想要获取数据的时候就什么也获取不到了。(由于Node.js不会对数据作缓存)

咱们从server.js开始 —— 移除对postData的处理以及request.setEncoding (这部分node-formidable自身会处理),转而采用将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 —— 咱们再也不须要传递postData了,此次要传递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/html"});
    response.write("404 Not found");
    response.end();
  }
}

exports.route = route;
还没写完,有空继续更新,也能够看下载个
 》
相关文章
相关标签/搜索