文章详情
基于Node搭建一个聊天室
标签:
  • node
  • vue
  • mongodb
日期:2018-2-07 17:57
摘要:这篇文章适合已有前端基础,但是还没接触过服务端的小伙伴,可以作为node入门教程看一看。由于作者也只是自学前端一年左右,刚接触node,难免会有不对的地方,欢迎指正,拒绝人身攻击,谢谢~

这篇文章适合已有前端基础,但是还没接触过服务端的小伙伴,可以作为node入门教程看一看。由于作者也只是自学前端一年左右,刚接触node,难免会有不对的地方,欢迎指正,拒绝人身攻击,谢谢~ 

项目截图:

查看DEMO

首先我们看看要用到哪些东西
前端:vue+vue-router
后端:node+express+socket.io+mongodb

vue是一个基于数据驱动的轻量级前端框架,拥有虚拟DOM,聊天室里的聊天记录会有很多,用vue挺合适的,当然要用jquery拼接字符串来操作DOM也是没问题的啦。

node是一个用c++编写的可以运行javascript的平台,和java一样属于应用层的平台。node是单线程异步非阻塞IO,非常善于处理高迸发IO,但是不擅长处理计算任务,由于是单线程,编写代码需要一定鲁棒性,一个用户报错就会让整个程序终止。

express可以看作是node的拓展包,自带类似阿帕奇的静态服务,且可以更加方便的组织路由和中间件。

socket.io是一个同步通讯的解决方案,基于websocket协议,内部封装了很多种解决方案,可以兼容所有浏览器,区别于ajax,ajax是客户端向服务端请求一次就返回一次数据,没有请求服务端就没法向客户端发送数据,而socket.io是双向的,可以看成是管道。

mongodb是一个非关系型的数据库,没有表的概念,都是一个个的集合,可以往集合里直接存入一条条的json数据,没有字段的约束,有时非常方便。

服务端的搭建

首先得安装node这个东西,去官网http://nodejs.cn/download/下一个就好了,32位和64位的要看清楚,下载.msi的版本,直接安装完,连环境变量也帮我们配好了,打开命令行输入node -v,再输入npm -v,我们发现node已经安装成功了,并且还顺带把npm也安装了,npm是个包管理器。

现在我们来创建服务端,先建一个文件夹,在这个文件夹里打开命令行输入npm init
之后命令行会问你很多东西,第一个是项目名字,其它可以先全部默认,不明白的可以自行百度,一路按回车下去,然后这个文件夹里就多了一个package.json的配置文件。现在我们继续敲命令行:
npm install express --save
npm install socket.io --save
npm install mongodb@2.2.33 --save

这个时候,服务端需要用到的依赖已经全部安装完了,接下来我们在根目录创建一个server.js文件,先引入必须的模块:

// 引入express
var express = require('express');
// 获得express的实例
var app = express();
// 引入http模块
var http = require('http');
// 用http模块创建一个服务并把express的实例挂载上去
var server = http.Server(app);
// 引入socket.io并立即实例化,把server挂载上去
var io = require('socket.io')(server);

这个时候,我们的服务已经创建好了,什么,你不信?我们来监听一下:

var server = server.listen(4001, function () {
console.log('服务端启动成功!端口4001');
});

保存一下代码,在当前文件夹打开命令行,输入node server.js(.js可以省略),不出意外,你会看到:

前端代码在这里就不做过多介绍了,下一步开始默认已经把静态页面写好了。
我们继续来写服务端,先用socket.io的实例来监听客户端的连接:

var onlieCount=0;
// 新用户连接进来时
io.on('connection', function (socket) {
onlieCount++;
console.log('有一个人进来了,'+'现在有'+onlieCount+'人在线!');
});

现在我们来写前端,前端需要引入一个socket.io.js的文件,这个文件在后端文件夹下的node_modules\socket.io-client\dist文件夹下,我们拷贝出来丢到前端文件夹里,并引用到html里去,同时打开socket的连接通道,连到本机的4001端口:

<script src="static/js/socket.io.js"></script>
<script type="text/javascript">
var socket=io.connect('http://localhost:4001');
</script>

这时,我们来重启一下服务端,并把前端页面打开两个,不出意外,你会看到:

继续转到服务端,我们来监听一下用户断开连接:

// 新用户连接进来时
io.on('connection', function (socket) {
onlieCount++;
console.log('有一个人进来了,'+'现在有'+onlieCount+'人在线!');
// 当有用户断开时
socket.on('disconnect', function () {
onlieCount--;
console.log(socket.id+'离开了,'+'现在有'+onlieCount+'人在线!');
});
});

注意了,断开连接的监听需要写到新用户连接的回调里,不然你怎么知道是谁断开了,新用户连接的回调有一个参数,那是这个连接的实例对象,它有个id的属性并且是唯一的,之后做单人聊天要用到,我们试着打印一下:

接下来服务端的代码默认都是写在新用户连接的回调里,现在给客户端广播发消息:

// 给所有客户端发送消息
io.emit('notice','大家新年快乐!');

前端里写一下监听接收,如果是vue就写在mounted钩子函数里:

socket.on('notice', function (info) {
console.log('这是来自服务端的消息:'+info);
});

我们重启一下服务器和前端页面:

服务端已经把消息发给客户端并且打印出来了,这里要注意这个notice可以自己命名,但是两边都要一样,相当于一个标记;还有客户端向服务端发送消息也是一样的代码,只是需要反过来,这里就不演示了,小伙伴们自己尝试一下。
现在已经可以把聊天室的公共聊天基础功能给写出来了,客户端点发送的时候,把textarea里的文本传到后台,后台接收之后发一次广播给所有人:

客户端点击发送 → emit给服务端 → 服务端emit给所有在线客户端

多人聊天的思路

这里主要讲一下思路,不会写太多的代码。
我们在前端定义一个数组变量,然后把收到的广播消息一条条push追加到这个数组里,根据这个数组是不是就能渲染出聊天消息列表啦:

小伙伴们可能会问,你这列表里怎么除了消息,还有头像、昵称和时间呢,没错,socket.io支持发送{}对象,在对象里你想装多少属性都OK的啦,看需要自己添加。
我们现在来看看在线列表的基础功能怎么实现,首先需要显示所有的在线用户,每个用户需要显示头像、用户名,这个可以在登录页面用vue的动态路由传递到聊天页面,然后立即传给服务端,服务端建一个数组push进去,再把这个列表emit广播给所有人,这样就同步了客户端的在线列表:

this.$router.push({ name: 'chat', params: userinfo});

在聊天页面的mounted钩子里拿到userinfo存起来:

this.userinfo = this.$route.params;
socket.emit('userinfo', this.userinfo);

服务端这样写,建一个全局变量存放用户信息:

var onlineList = {list: []};
// 监听还是要写在连接用户的回调里
socket.on('userinfo', function (userinfo) {
// 给用户信息添加socketid属性,值为此管道的唯一id
userinfo.socketid=socket.id;
onlineList.list.push(userinfo);
// 广播在线列表
io.emit('onlineList', onlineList);
});

这样,只要在客户端监听一下‘onlineList’,只要有人上线,都能收到一个列表的数据,然后根据数据再渲染页面。下线更新所有客户端列表也是一个原理,当用户下线,服务端能拿到这个用户的socket.id,根据这个id去遍历一下onlineList.list这个数组,有相等的就把这个数组项删除,之后再把这个onlineList广播给所有客户端,这就实现了用户上下线保持列表一致的功能。

至此,我们已经可以实现简单的在线列表和聊天窗口的对话框显示了,但是如何在聊天窗口区分自己和别人的聊天消息了,我想让别人的都显示白色,自己的显示绿色,这个其实很简单,首先我们定义一个变量来装自己的名字,然后再给对话框写一个绿色的class样式,聊天窗口的数据是一个对象数组,通过v-for渲染出来的,我们可以这么写:

<div v-for="(item, index) in msgList" :class="['whiteBox', {'greenBox':myName == msgList[index].username}]">

这样只要是匹配到msgList里面的对象的username属性和我的名字一致,那就会加上greenBox这个样式了。

单人聊天的思路

我们先来分析一下,多人聊天的流程是这样:

用户A → 服务端 → 所有用户

那么单人聊天就是A发给B,只有AB两个人能看到,流程是这样:

用户A → 服务端 →  用户B  

那么服务端怎么才能指定某个用户单独发送消息呢,很简单,每一个客户端与服务端连接的这根管道都是独立的,且是一个对象,我们只要找到这个对象,然后调用他的emit方法,就能单独给此用户发送数据了。
之前提到了,在新用户连接的时候,回调里会有一个参数,这个参数正是此用户的管道对象,只要有新用户连接,我们就把这个管道对象存起来,有用户下线我们就把这个管道对象删除,我在服务端是这么做的:

var allsocket = {};
// 新用户连接进来时
io.on('connection', function (socket) {
var the_id = socket.id;
// 把每一个用户的管道对象存到一个总对象里,key就是socket.id
allsocket[the_id] = socket;
});

之后,我们可以通过allsocket[key].emit()来向指定客户端发送消息。
前端只需要在向客户端发送消息的时候带上目标用户的socketid属性就可以了,后端拿到数据后直接在回调里把消息allsocket[key].emit()转发。
现在来说说前端怎么显示个人聊天的信息吧,这个时候vue数据驱动的好处就凸显出来了,和公共聊天一样,也是存一个数组,但是会有很多不同的人,我们再建一个对象allMsgList,把这些个人聊天记录的数组存进去,key设为此人的名字,value就是消息的数组。
我们再写一个vue的computed计算属性取名curChatMsg,用这个计算属性去v-for渲染聊天消息列表,计算属性怎么写呢,我们先给在线用户列表里的列表项添加一个点击事件,data里再设一个变量curChat,点哪个用户就把此用户的名字赋值给curChat,然后这个计算属性里判断一下,根据不同的key用户名字返回不同的value消息数组:

curChatMsg: function(){
// 如果存在就返回
if(allMsgList[this.curChat]){
return allMsgList[this.curChat];
}
},

现在只要点击用户列表里的不同用户,就会显示与不同用户单独聊天的消息啦。现在一个简单的支持多人聊天和单人聊天的聊天室就实现了,其它功能根据需要再慢慢添砖加瓦,例如新消息提示、新消息置顶、列表用户搜索、表情发送与接收等等,在vue里基于数据驱动实现起来都不难。

查看聊天历史记录

实现这个功能,我们需要用到数据库,这里选择用MongoDB,首先我们得去官网下载安装文件https://www.mongodb.com/download-center?jmp=nav#community

一直往下点,安装完成后,我们需要配置一下,在某个地方新建一个文件夹,里面再建三个子文件夹,用来存放数据库数据和日志等:

在oth文件夹里,再建一个conf文件,txt改后缀即可:

#数据库路径
dbpath=d:\mongo\data\
#日志输出文件路径
logpath=d:\mongo\logs\mongodb.log
#错误日志采用追加模式
logappend=true
#启用日志文件,默认启用
journal=true
#端口号 默认为27017
port=27017
#http 访问配置 端口为28017
httpinterface=true

现在我们来启动一下数据库试试,找到MongoDB的安装路径,进入到里面的bin文件夹,在此处打开命令行,执行:

mongod --config d:\mongo\oth\mongo.conf

命令行没有任何反应,但是数据库已经启动了,我们打开浏览器进到localhost:28017,可以看到数据库已经在运行了。
我们需要把聊天室多人聊天的每一句对话都存在数据库里,先在服务端引入之前安装的mongodb驱动,拿到MongoClient的实例:

var MongoClient = require('mongodb').MongoClient;

接着我们存一下数据库地址:

// mychat是数据库的名字,连接的时候如果找不到会自动建立
var DB_CONN_STR = 'mongodb://localhost:27017/mychat';

我们在接收多人聊天的回调里,把客户端传过来的消息存进数据库:

MongoClient.connect(DB_CONN_STR, function(err, db) {
if (err) throw err;
// data是客户端传过来的数据
var data = {};
// chat_msg是要存入的集合的名字,找不到会自动建立
db.collection('chat_msg').insert(data , function(err, res) {
if (err) throw err;
console.log("插入成功");
db.close();
});
});

现在,每一条多人聊天的消息都已经入库了,我们需要在前端显示,可以在聊天消息框顶部加一个按钮,如下:

只要点这个按钮,服务端就需要去数据库里查询并把相应的列表数据发送到前端,这里的查询条件需要好好的琢磨一下,首先存入数据库的消息肯定要设计一个时间戳的属性,我们按时间戳倒序查询,时间靠后的先查,且不能直接分页,最开始我写了一个分页查询,点一下往后查10条,但是如果在你翻看聊天记录的时候,刚好有其他人发了消息,是不是分页就对不上了,会出现重复对话显示。这里需要用到skip(num)来跳过查询,参数是一个数字,表示跳过多少条往后查询,前端要把这个参数传过来,就是msgList的长度,比如msgList当前显示的聊天消息有8条,那么就跳过8条往后查,假设查10条,我们把这10条消息拿到后在前端reverse().concat(msgList)连接到旧msgList的前面,这时msgList一共18条,我们正准备再点一次“查看更多消息”之前刚好另一个人发了1条消息,这时msgList就是19条,我们点“查看更多消息”的时候顺带就把19这个数字传到服务端,服务端skip(19).limit(10),这样就能对上了:

MongoClient.connect(DB_CONN_STR, function(err, db) {
if (err) throw err
// find()是查询所有,.sort({'time':-1})是按time字段倒序排列,skip(num).limit(10)是跳过num条往后查10条
db.collection('chat_msg').find({}).sort({'time':-1}).skip(num).limit(10).toArray(function(err, result) {
if (err) throw err;
console.log("查询成功");
// 查到数据后直接根据socketid单发给客户端
allsocket[socketid].emit('chat_record', result);
db.close();
});
});

客户端拿到后reverse()颠倒一下,再concat(msgList),拼接到旧msgList前面,v-for根据数据自动就渲染了页面。

到这里,聊天室的基础核心功能就已经完成了,支持多人聊天,支持单人聊天,支持查看多人聊天的历史记录,其它零碎的功能大部分需要在前端去实现了,我就先写到这里,之后有时间再补充其它的零碎功能。

表情的实现

表情本质上就是图片,可以是一整张精灵图,也可以每个表情单独一张图片,实现略有不同。我们可以创建一个json对象保存图片的路径和名字(做精灵图的话是背景位置的坐标),点击某个表情的时候,给消息发送框加上一个特定的词语,例如〖微笑〗,然后渲染消息的时候匹配一次(可用计算属性),如果检测到〖微笑〗的字符串,则删除掉,并操作DOM插入相对应的图片。

发送
评论(0)