网页聊天室之Socket.io实战
从10月1号到13号,用了大概80%的时间基本实现了一个聊天室应该具备的基本功能:
- 用户登录注册
- 添加好友 (点击添加双方可同时添加对方)
- 选择好友聊天和聊天信息的存储
- 若对方离线则提示用户对方不在线
- 若对方正在与其他用户聊天,则提示对方忙碌,对方提示收到其他聊天请求
- 存在未读消息可显示在用户名旁,且可在聊天页面直接显示未读消息
- 查看与某个好友的聊天记录
好了,基本就是这些功能。总的实现过程来讲,会操作socket.io和mongodb即可。
先来看下界面吧。
聊天记录界面还很粗糙
登录 聊天界面 1实现过程
1. 登录注册就不用讲了,直接把建博客时的拿过来了。
2. 数据结构的定义
//用户表
var userSchema = new Schema({
username: String,
password: String,
email: String,
address: String,
userImg: String,
meta: {
updateAt: {type:Date, default: Date.now()},
createAt: {type:Date, default: Date.now()}
}
});
/* 好友表 */
var friendSchema = new Schema({
uid: {type: ObjectId, ref: 'User'},
fid: {type: ObjectId, ref: 'User'},
meta: {
updateAt: {type:Date, default: Date.now()},
createAt: {type:Date, default: Date.now()}
}
});
/*/聊天信息表 */
var messageSchema = new Schema({
from: {type: ObjectId, ref: 'User'},
to: {type: ObjectId, ref: 'User'},
content: String,
status: String,
meta: {
updateAt: {type:Date, default: Date.now()},
createAt: {type:Date, default: Date.now()}
}
});
用户表:增加
friends
数组,包含friendId
和unread
。unread
是后来考虑到要显示未读消息时加进去的,一开始friends
只是个保持好友_id的数组,加了unread后就要重写取出用户好友等相关函数,很麻烦,这是个教训,所以以后加数组的时候应该多定义一个,不管之后有没有用。
聊天信息表很简单,from
发送方id
,to
接收方id
,status
是消息读取状态,已读为1
,未读为0
重新定义好用户表后就可以进行拉取全部用户数组和用户好友数组了。
//取出用户
exports.searchAllUsers = function (req, cb) {
var $user = {};
User.find({}, function (err, data) {
var userList = new Array();
for (var i = 0; i < data.length; i++) {
userList.push(data[i].toObject());
}
// console.log(categoryList);
$user.results = userList;
$user.count = userList.length;
cb(true, $user);
})
};
//取出用户好友
exports.matchUser = function (id, cb) {
User.findById(id, function (err, data) {
if(err) {
console.log(err);
} else {
var user = (data !== null) ? data.toObject() : '';
cb(true, user);
}
}).populate('friends.friendId', 'username');
};
侧边栏的html内容
<div class="sidebar" id="chat-sidebar">
<div class="sidebar-wrapper" id="sidebar-wrapper">
<div class="panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#admin-box" href="#friend">好友列表</a>
</h4>
</div>
<div id="friend" class="panel-collapse collapse">
<div class="panel-body">
<ul class="nav">
{{#if user.friends}}
{{#each user.friends}}
<li class="users-list">
<div class="username">
<a href="/p/chatRoom/{{_id}}">{{friendId.username}} {{unread}}</a>
</div>
</li>
{{/each}}
{{/if}}
</ul>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#admin-box" href="#stranger">用户列表 ({{userCount}})</a>
</h4>
</div>
<div id="stranger" class="panel-collapse collapse">
<div class="panel-body">
<ul class="nav">
{{#each entries}}
<li data-toggle="select" data-id="{{_id}}" class="users-list">
<div class="username">{{username}}</div>
<div class="users-list-r">
<a href="#" class="add-user"><i class="fa fa-plus"></i>添加</a>
</div>
</li>
{{/each}}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
ChatRoom的html内容
<div class="wrapper">
<div class="chat-room" data-id="{{user._id}}">
<div class="chat-top">
<div class="username-to" data-id="{{friend._id}}">{{friend.username}}</div>
<div class="back-btn">
<a href="/p/historyMessages/{{friend._id}}" class="btn btn-primary btn-md">查看聊天记录</a>
</div>
</div>
<div class="chat-ctn">
<ul id="messages">
{{#each message}}
<li class="chat-box-l"><img class="chat-user-img" src="/images/mb2.jpg"><p class="chat-p">{{content}}</p></li>
{{/each}}
</ul>
</div>
<form action="">
<div class="send-box">
<div class="msg-input">
<input id="m" autocomplete="off" class="input-ctn" placeholder="请输入...">
</div>
<div class="send-btn">
<button type="submit" class="btn btn-primary pull-right">发送</button>
</div>
</div>
</form>
</div>
</div>
<script src="/chat/chatRoom.js"></script>
让我们来看下router的内容
//进入index后读取全部用户和重新读取好友列表,以便添加好友后即可在好友列表看到该好友
var express = require('express');
var router = express.Router();
var config = require('../config');
var dbHelper = require('../db/dbHelper');
var formidable = require('formidable');
var entries = require('../db/jsonRes');
var fs = require('fs');
/* GET home page. */
router.get('/index', function(req, res, next) {
var id = req.session.user._id;
dbHelper.searchAllUsers(req, function (success, data) {
dbHelper.getFriends(id, function (success, doc) {
res.render('new', {
entries: data.results,
userCount: data.count,
user: req.session.user,
friendList: doc
});
});
});
});
//获取历史聊天记录
router.post('/getHistoryMsg', function (req, res, next) {
dbHelper.findHistoryMsg(req.body, function (success, doc) {
res.send(doc);
})
});
//添加好友
router.post('/addFriend', function(req, res, next) {
dbHelper.addFriend(req.body, function (success, doc) {
res.send(doc);
});
});
//
router.post('/addMessage', function(req, res, next) {
dbHelper.addMessage(req.body, function (success, doc) {
res.send(doc);
});
});
router.post('/getUnreadMsg', function(req, res, next) {
dbHelper.getUnreadMsg(req.body, function (success, doc) {
res.send(doc);
});
});
router.post('/updateMsgStatus', function (req, res, next) {
dbHelper.updateMsgStatus(req.body, function (success, doc) {
res.send(doc);
});
});
//上传图片
router.post('/uploadImg', function(req, res, next) {
console.log("开始上传");
// var io = global.io;
var form = new formidable.IncomingForm();
var path = "";
var fields = [];
form.encoding = 'utf-8'; //上传文件编码格式
form.uploadDir = "public/uploadFile"; //上传文件保存路径(必须在public下新建)
form.keepExtensions = true; //保持上传文件后缀
form.maxFieldsSize = 30000 * 1024 * 1024; //上传文件格式最大值
var uploadprogress = 0;
console.log("start:upload----"+uploadprogress);
form.parse(req);
form.on('field', function(field, value) {
console.log(field + ":" + value); //上传的参数数据
})
.on('file', function(field, file) {
path = '\\' + file.path; //上传的文件数据
})
.on('progress', function(bytesReceived, bytesExpected) {
uploadprogress = (bytesReceived / bytesExpected * 100).toFixed(0); //计算进度
console.log("upload----"+ uploadprogress);
// io.sockets.in('sessionId').emit('uploadProgress', uploadprogress);
})
.on('end', function() {
//上传完发送成功的json数据
console.log('-> upload done\n');
entries.code = 0;
entries.data = path;
res.writeHead(200, {
'content-type': 'text/json'
});
res.end(JSON.stringify(entries));
})
.on("err",function(err){
var callback="<script>alert('"+err+"');</script>";
res.end(callback);//这段文本发回前端就会被同名的函数执行
}).on("abort",function(){
var callback="<script>alert('"+ttt+"');</script>";
res.end(callback);
});
});
//上传截图
router.post('/upload', function(req, res, next){
//接收前台POST过来的base64
var imgData = req.body.img;
//过滤data:URL
var base64Data = imgData.replace(/^data:image\/\w+;base64,/, "");
var dataBuffer = new Buffer(base64Data, 'base64');
var fileName = req.body.fileName;
console.log(dataBuffer);
fs.writeFile("./public/uploadFile/upload_" + fileName +".jpg", dataBuffer, function(err) {
if(err){
res.send(err);
}else{
var path = "\\public\\uploadFile\\upload_" + fileName +".jpg";
entries.code = 0;
entries.data = path;
res.end(JSON.stringify(entries));
}
});
});
module.exports = router;
好了,接下去就是聊天相关操作了
3. 添加好友
参数data
包含userId
和friendId
,查找到_id: userId
后把friendId
push进去,对friendId
做同样处理,从而实现两端同时添加好友
exports.addFriend = function (data, cb) {
entries.code = 0;
console.log(data);
User.findById(data.user, function (err, user) {
User.findOne({"_id": data.user, "friends.friendId": data.friend}, function (err, doc) {
if(err) {
console.log(err);
} else if(doc != null) {
entries.code = 98;
entries.msg = '该好友已添加!';
cb(false, entries);
} else if(doc == null) {
user.friends.push({
friendId: data.friend
});
user.save(function (err, doc) {
if(err) {
entries.code = 99;
console.log(err);
} else {
console.log("好友添加成功");
cb(true, entries);
}
})
}
});
});
User.findById(data.friend, function (err, user) {
User.findOne({"_id": data.friend, "friends.friendId": data.user}, function (err, doc) {
if(err) {
console.log(err);
} else if(doc != null) {
console.log("对方已添加!");
} else if(doc == null) {
user.friends.push({
friendId: data.user
});
user.save(function (err, doc) {
if(err) {
console.log(err);
} else {
console.log("both add!")
}
})
}
});
});
};
4 Socket.io聊天
根据之前写的那篇Socket.io API,来看下实际环境该如何操作。
服务端代码
var io = require('socket.io').listen(server);
var userId = {};
var toChat = {};
io.on('connection', function(socket){
socket.on('add user', function (data) {
socket.name = data.from;
toChat[data.from] = data.to;//保存用户聊天对象
userId[data.from] = socket;//每次进入都要socket.id都要变一次
});
socket.on('chat message', function(msg){
if(toChat[toChat[socket.name]] == socket.name && userId[msg.to]) {
var data = {
msg: msg.content,
ctn: msg.content,
status: 1
};
userId[msg.to].emit('chat message', data);
} else if(userId[msg.to] && toChat[toChat[socket.name]] != socket.name) {
var data = {
msg: "系统:你收到其他用户的聊天信息!",
_msg: "对方正在和其他人聊天!",
ctn: msg.content,
status: 2
};
userId[msg.from].emit('chat busy', data);
userId[msg.to].emit('chat message', data);
} else {
var data = {
msg: "抱歉,当前用户不在线!",
ctn: msg.content,
status: 0
};
userId[msg.from].emit('chat message', data);
}
});
socket.on('disconnect', function () {
//用户退出提醒对方
if(userId[toChat[socket.name]] && toChat[toChat[socket.name]] == socket.name) {//判断对方聊天的对象是否是自己
userId[toChat[socket.name]].emit('user left', "对方退出!");
}
delete userId[socket.name];
delete toChat[socket.name];
});
});
客户端js代码
var socket = io();
//传递给服务器用户Id
var data = {
from: $('.chat-room').attr("data-id"),
to: $('.username-to').attr("data-id")
};
var msg = {
from: $('.chat-room').attr("data-id"),
to: $('.username-to').attr("data-id"),
content: ''
};
socket.emit('add user', data);
updateMsgStatus();//消息标记为已读
$('form').submit(function(){
var ctn = $('#m').val();
if(ctn != '') {
var html = '<li class="chat-box-r"><p class="chat-p">'+ ctn +'</p><img class="chat-user-img" src="/images/mb2.jpg"></li>';
$('#messages').append(html);
msg.content = ctn;
socket.emit('chat message', msg);
// doAddMsg();
}
$('#m').val('');
return false;
});
socket.on('chat busy', function (data) {
var html = '<li class="chat-box-l"><img class="chat-user-img" src="/images/mb2.jpg"><p class="chat-p">' + data._msg +'</p></li>';
$('#messages').append(html);
doAddMsg(0, data.ctn, msg.from, msg.to);
});
socket.on('chat message', function(data){
var html = '<li class="chat-box-l"><img class="chat-user-img" src="/images/mb2.jpg"><p class="chat-p">' + data.msg +'</p></li>';
$('#messages').append(html);
// doAddMsg(data.status, data.ctn);
if(data.status == 1) {
doAddMsg(1, data.ctn, msg.to, msg.from);
} else if(data.status == 0) {
doAddMsg(0, data.ctn, msg.from, msg.to);
}
});
socket.on('user left', function (msg) {
var html = '<li class="chat-box-l"><img class="chat-user-img" src="/images/mb2.jpg"><p class="chat-p">' + msg +'</p></li>';
$('#messages').append(html);
});
function doAddMsg(status, ctn, from, to) {
$.ajax({
type: "POST",
url: "/p/addMessage",
contentType: "application/json",
dataType: "json",
data: JSON.stringify({
'from': from,
'to': to,
'content': ctn,
'status': status
}),
success: function(result) {
if (result.code == 99) {
alert("发送失败");
}
}
})
}
function updateMsgStatus() {
$.ajax({
type: "POST",
url: "/p/updateMsgStatus",
contentType: "application/json",
dataType: "json",
data: JSON.stringify({
'from': msg.to,
'to': msg.from
}),
success: function(result) {
if (result.code == 99) {
alert(result.msg);
} else {
console.log("消息已全部阅读!");
}
}
})
}
注释:A客户端主要就是
emit()
消息,当emit(add user)
后服务器就会保存该用户socket.name
为该用户ID
,保存到userList
数组里,相当于userList [ ID ]
代表当前用户,之后发送接受就有这个来处理。接着服务器接收到客户端出来的消息后,有 3 种情况要处理:
- 对方不在线,即
userList [ 对方ID ]
为NULL
,则emit给用户对方不在线,并且用户本地保存消息,标记消息为未读,status为 0
- 对方B在线,但正在与其他用户C聊天,chatTo数组保存聊天对象的Id,判断对方的聊天对象是否是自己,写成代码即为
chatTo[chatTo[A]] == A
,其中chatTo [ A ]就是B
,如果false则emit回本地告诉用户对方正忙,status为0,保存消息进数据库
3.对方在线且聊天对象正是自己,则emit消息给对方,status为 1
,保存消息进数据库
5. 对未读消息的处理(未读消息显示数量和打开聊天页面直接把未读消息取出显示)
- 当保存的一条消息
status为0
时,则相应的用户下的好友属性unread + 1
- 进入聊天页面时如果对方的
unread不为 0
,则从message中找到对方发给自己的status为 0
的消息,放进数组返回
//保存消息
exports.addMessage = function (data, cb) {
console.log(data);
var message= new Message({
from: data.from,
to: data.to,
content: data.content,
status: data.status
});
message.save(function (err, doc) {
if(err) {
entries.code = 99;
cb(false, entries);
console.log("add message fail !");
}
});
//如果是未读消息,则
if(data.status == 0) {
User.findById(data.to, function (err, user) {
for(var i = 0; i < user.friends.length; i++) {
var item = user.friends[i];
if(item.friendId.toString() == data.from) {
item.unread++;
break;
}
}
user.save(function (err, doc) {
if(err) {
console.log("add unread fail !");
}
})
})
}
};
//进入聊天页面时获取对方名字,存在未读消息则取出显示
exports.findFriend = function (userId, friendId, cb) {
User.findOne({"_id": userId, "friends._id": friendId})
.populate('friends.friendId', 'username')
.exec(function (err, data) {
var user = (data !== null) ? data.toObject() : '';
for(var i =0; i < data.friends.length; i++) {
var item = data.friends[i];
if(item._id.toString() == friendId){
user = item;
break;
}
}
// console.log(user.friendId);
var $unreadMsg = {};
var messageList = new Array();
$unreadMsg.results = messageList;
$unreadMsg.friend = user.friendId;
if(user.unread != 0) {
// var $unreadMsg = {};
Message.find({"from": user.friendId._id, "to": userId, "status": 0}, function (err, data) {
// var messageList = new Array();
for (var i = 0; i < data.length; i++) {
messageList.push(data[i].toObject());
}
$unreadMsg.results = messageList;
// $unreadMsg.friend = user.friendId;
// console.log(messageList);
})
}
cb(true, $unreadMsg);
// console.log($unreadMsg);
// else {
// cb(true, user.friendId);
// }
})
};
取出未读消息显示后,就该把未读改为已读了
//将对应消息的status改为1,对应好友的unread改为0
exports.updateMsgStatus = function (data, cb) {
Message.find({"from": data.from, "to": data.to, "status": 0}, function (err, message) {
for(var i =0; i < message.length; i++) {
message[i].status = 1;
message[i].save(function (err, doc) {
if(err) {
console.log(err);
} else {
console.log("成功修改消息status为1");
}
})
}
});
//在用户对应的好友未处理消息改为0
User.findById(data.to, function (err, user) {
for(var i = 0; i < user.friends.length; i++) {
var item = user.friends[i];
if(item.friendId.toString() == data.from) {
item.unread = 0;
break;
}
}
user.save(function (err, doc) {
if(err) {
console.log("update unread fail !");
}
})
})
};
由于
update
只能对一条数据进行操作,在更新一块我将status为 0
的放进数组,利用for循环
更改每一条符合条件的message,批量处理。
6. 查找聊天记录
由于message
定义的时候只有简单的from
,to
,content
,现在只能取出要么是对方发给自己的聊天记录,要么是自己发给对方的聊天记录,无法实现对话式的完整聊天记录提取。我想到的是同时取出双方的记录,再按照时间排序,依次存入数组返回。所以提取聊天记录要改一下。
//查看历史消息记录
exports.findHistoryMsg = function (from, to, cb) {
Message.find({"from": from, "to": to})
.populate("from to", "username username")
.exec(function (err, data) {
var $message = {};
var messageList = new Array();
for(var i =0; i < data.length; i++) {
messageList.push(data[i].toObject());
}
// console.log(messageList);
var name = messageList[0].from.username;
$message.results = messageList;
$message.name = name;
cb(true, $message);
})
};