资讯专栏INFORMATION COLUMN

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

Cristic / 2202人阅读

摘要:处理程序在的匿名回调函数中做请求响应的操作,而处理程序仍然是简单的回复,只是这次是使用对象而已。为了使整个过程非阻塞,会将数据拆分成很多小的数据块,然后通过触发特定的事件,将这些小数据块传递给回调函数。

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

先看效果图:

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

输入好了之后进入,然后随便说点什么,


可以多个人在线聊天,


也可以上传图片:


更改字体颜色:

制作过程

文件分布:入口文件server.js

本项目的全代码 https://github.com/FengNianya...(我的github仓库)

讲解:

服务器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() 中做这件事情。不过就像我说过的,我们应该加入一些抽象的元素,让我们的例子变得更有趣一点儿。

处理不同的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 = ""+
    ""+
    ""+
    ""+
    ""+
    "
"+ ""+ ""+ "
"+ ""+ ""; 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 = ""+
    ""+
    ""+
    ""+
    ""+
    "
"+ ""+ ""+ "
"+ ""+ ""; 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 = ""+
    ""+
    ""+
    ""+
    ""+
    "
"+ ""+ ""+ "
"+ ""+ ""; 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 = ""+
    ""+
    ""+
    ""+
    ""+
    "
"+ ""+ ""+ "
"+ ""+ ""; 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 + " "); 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;
还没写完,有空继续更新,也可以看下载个
 》

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89579.html

相关文章

  • nodejs练手爬虫+API测试

    摘要:知乎日报代理首先感谢提供的分析使用详情请参考他提供的参数和地址代理转发的使用为前缀进入代理路由启动界面图像获取后为图像分辨率,接受任意的格式,为任意非负整数,返回值均相同返回值示例最新消息等具体参考提供的分析中的使用方式以及参数含义。 项目说明 这是一个基于express的node后端API服务,当时只是想抓取字幕组网站的下载资源,以备以后通过nas的方式去自动下载关注的美剧。不过后来...

    bigdevil_s 评论0 收藏0
  • 基于 Vue2+Node+mongoDB 的前后端分离全栈练手项目

    摘要:本文源码简介之前刚入门并做好了一个简而全的纯全家桶的项目,数据都是本地模拟请求的详情请移步这里为了真正做到数据库的真实存取,于是又开始入门了并以此来为之前的页面写后台数据接口。 本文源码:Github 简介: 之前刚入门vue并做好了一个简而全的纯vue2全家桶的项目,数据都是本地 json 模拟请求的;详情请移步这里:vue-proj-demo 为了真正做到数据库的真实存取,于是又...

    jay_tian 评论0 收藏0
  • 全栈最后一公里 - Node.js 项目的线上服务器部署与发布

    摘要:没有耐心阅读的同学,可以直接前往学习全栈最后一公里。我下面会罗列一些,我自己录制过的一些项目,或者其他的我觉得可以按照这个路线继续深入学习的项目资源。 showImg(https://segmentfault.com/img/bVMlke?w=833&h=410); 本文技术软文,阅读需谨慎,长约 7000 字,通读需 5 分钟 大家好,我是 Scott,本文通过提供给大家学习的方法,...

    Nosee 评论0 收藏0
  • vue+socket.io+express+mongodb 实现简易多房间在线群聊

    摘要:项目简介主要是通过做一个多人在线多房间群聊的小项目来练手全栈技术的结合运用。编译运行开启服务,新建命令行窗口启动服务端,新建命令行窗口启动前端页面然后在浏览器多个窗口打开,注册不同账号并登录即可进行多用户多房间在线聊天。 项目简介 主要是通过做一个多人在线多房间群聊的小项目、来练手全栈技术的结合运用。 项目源码:chat-vue-node 主要技术: vue2全家桶 + socket....

    android_c 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<