每条消息都要设置一个是否已读的属性
消息分组:
后台返回和当前用户相关的所有消息,根据当前用户和不同用户直接的聊天标识,
返回对应的分组消息列表
未读消息:
后台返回所有的消息后,根据和不同用户的聊天标识,获取相应的聊天内容,
遍历每一条聊天内容,读取每一条的是否已读属性,来确定未读的总消息
已读消息:
当进入聊天界面后,就通过后台修改所有的读取状态属性为已读的消息,
然后返回修改的条数,前台根据修改的条数和总条数来计算剩余未读的总消息
代码示例:
reducer.js:
//聊天状态
const initChat={
users:{},
chatMsgs:[],
unReadCount:0 //总未读数量
}
function chat(state=initChat,action)
{
switch(action.type)
{
case RECEIVE_MSG_LIST: // data: {users, chatMsgs}
const {users, chatMsgs, userid} = action.data
console.log(userid)
return {
users,
chatMsgs,
unReadCount: chatMsgs.reduce((preTotal, msg) => preTotal+(!msg.read&&msg.to===userid?1:0),0)
}
case RECEIVE_MSG: // data: chatMsg
const {chatMsg} = action.data
return {
users: state.users,
chatMsgs: [...state.chatMsgs, chatMsg],
unReadCount: state.unReadCount + (!chatMsg.read&&chatMsg.to===action.data.userid?1:0)
}
default:
return state
}
}
actions.js:
import {reqRegister,reqLogin,reqUpdateUser,reqUser,reqUserList,reqChatMsgList,reqReadMsg} from '../api/index'
import {AUTH_SUCCESS,ERROR_MSG,RECEIVE_USER,RESET_USER,RECEIVE_USERLIST,RECEIVE_MSG_LIST,RECEIVE_MSG,MSG_READ} from './action-types'
import io from 'socket.io-client'
//授权成功更改状态
const authSuccess=(user)=>({type:AUTH_SUCCESS,data:user})
//失败更改状态
const errorMsg=(msg)=>({type:ERROR_MSG,data:msg})
//完善信息后更新
const receiveUser=(user)=>({type:RECEIVE_USER,data:user})
//重置用户信息
export const resetUser=(msg)=>({type:RESET_USER,data:msg})
//接收用户列表
const receiveUserList=(userlist)=>({type:RECEIVE_USERLIST,data:userlist})
//接收消息列表
const receiveMsgList=({users,chatMsgs,userid})=>({type:RECEIVE_MSG_LIST,data:{users,chatMsgs,userid}})
//接收单个消息
const receiveMsg=(chatMsg,userid)=>({type:RECEIVE_MSG,data:{chatMsg,userid}})
//注册
export const register=(user)=>{
const {username,password,password2,type} =user;
//表单前台验证,返回对应的同步action
if(!username||!password||!password2)
{
return errorMsg('用户名或密码不为空');
}
if(password!==password2)
{
return errorMsg('两次密码不一致');
}
return async (dispatch)=>{
const response=await reqRegister({username,password,type})
const res=response.data;
if(res.code===0) //成功
{
getMsgList(dispatch,res.data._id);
dispatch(authSuccess(res.data));
}else{
dispatch(errorMsg(res.msg));
}
}
}
//登录
export const login=(user)=>{
const{username,password}=user
//表单前台验证,返回对应的action
if(!username||!password)
{
return errorMsg('用户名或密码不为空');
}
return async (dispatch)=>{
const response=await reqLogin(user)
const res=response.data;
if(res.code===0) //成功
{
getMsgList(dispatch,res.data.user._id);
dispatch(authSuccess(res.data.user));
}else{
dispatch(errorMsg(res.msg));
}
}
}
//用户完善头像等信息后更新用户
export const updateUser=(user)=>{
return async (dispatch)=>{
const response=await reqUpdateUser(user)
const res=response.data;
if(res.code===0)
{
dispatch(receiveUser(res.data));
}else{
dispatch(resetUser(res.msg));
}
}
}
//获取用户异步action
export const getUser=()=>{
return async(dispatch)=>{
const response =await reqUser();
const res=response.data;
if(res.code===0){
getMsgList(dispatch,res.data._id);
dispatch(receiveUser(res.data))
}else{
dispatch(resetUser(res.msg));
}
}
}
//获取用户列表
export const getUserList=(type)=>{
return async dispatch=>{
const response =await reqUserList(type);
const res=response.data;
if(res.code===0)
{
dispatch(receiveUserList(res.data))
}
}
}
/**
* 单例模式创建一个socket,利用全局对象io新增属性来实现
*/
function initIO(dispatch,userid)
{
if(!io.socket){
io.socket=io('ws://localhost:4000')
io.socket.on('receiveMsg', function (chatMsg) {
console.log('浏览器端接收到消息:', chatMsg)
//只有当前会话才分发action保存
if(userid==chatMsg.from||userid==chatMsg.to)
{
dispatch(receiveMsg(chatMsg,userid))
}
})
}
}
//异步发送消息
export const sendMsg=({from,to,content})=>{
return dispatch=>{
initIO();
console.log('浏览器发送消息',{from,to,content})
io.socket.emit('sendMsg',{from,to,content})
}
}
//异步获取消息函数
async function getMsgList(dispatch,userid)
{
initIO(dispatch,userid);
const response =await reqChatMsgList();
const res=response.data
if(res.code===0)
{
const{users,chatMsgs}=res.data
dispatch(receiveMsgList({users,chatMsgs,userid}))
}
}
action-type.js:
//注册/登录成功
export const AUTH_SUCCESS='auth_success'
//错误提示信息
export const ERROR_MSG='error_msg'
//用户信息完善
export const RECEIVE_USER='receive_user'
//重置用户
export const RESET_USER='reset_user'
//接收用户列表
export const RECEIVE_USERLIST='receive_userlist'
//接收消息列表
export const RECEIVE_MSG_LIST='receive_msg_list'
//接收到新的一条消息
export const RECEIVE_MSG='receive_msg'
//读取消息
export const MSG_READ='msg_read'
api接口:
/*
包含n个接口请求的函数
*/
import ajax from './ajax'
//注册接口
export const reqRegister=(user)=>ajax('/register',user,'POST');
//登录接口
export const reqLogin=({username,password})=>ajax('/login',{username,password},'POST');
//更新用户接口
export const reqUpdateUser=(user)=>ajax('/update',user,'POST');
//获取用户信息
export const reqUser=()=>ajax('/user')
//获取用户列表
export const reqUserList=(type)=>ajax('/userlist',{type:type})
//获取聊天消息
export const reqChatMsgList=()=>ajax('/msglist')
//修改消息为已读
export const reqReadMsg=(from)=>ajax('/readMsg',{from},'POST')
api封装:
/**
ajax模块,返回值为promise对象
*/
import axios from 'axios'
import qs from 'qs'
export default function ajax(url,data={},type='GET')
{
// url='http://127.0.0.1:4000'+url;
if(type==='GET')
{
let str=''
//将对象拼成url参数对
Object.keys(data).forEach(function(item,index){
str+=item+'='+data[item]+'&'
})
//去掉最后一个&或根本无参数
if(str)
{
str=str.substring(0,str.length-1)
str='?'+str;
}
return axios.get(url+str)
}else{
return axios.post(url,qs.stringify(data))
}
}
后台消息有关路由:
let express = require('express')
var bodyParser = require('body-parser');
var md5=require('blueimp-md5');
var cookieParser = require('cookie-parser');
var urlencodedParser = bodyParser.urlencoded({ extended: false });
const {UserModel,ChatModel} =require('./db/models');
const filter={password:0,__v:0};
var app=express();
//socket.io
var server = require('http').Server(app);
require('./socketio/test')(server);
server.listen(4000,function(){
console.log('this express server is running at http://127.0.0.1:4000 ');
});
app.use(cookieParser());
/*
获取当前用户所有相关聊天信息列表
*/
app.get('/msglist', (req, res)=>{
// 获取 cookie 中的 userid
const userid = req.cookies.userid
console.log(userid);
// 查询得到所有 user 文档数组
UserModel.find(function (err, userDocs) {
// 用对象存储所有 user 信息: key 为 user 的_id, val 为 name 和 header 组成的 user 对象
const users = {} // 对象容器
//将数据库的所有用户,按照以user_id为key,存入容器中
userDocs.forEach(doc => {
users[doc._id] = {username: doc.username, header: doc.header}
})
/*
查询 userid 相关的所有聊天信息
参数 1: 查询条件
参数 2: 过滤条件
参数 3: 回调函数
*/
//根据两个用户其中一个userid,返回所有聊天内容
ChatModel.find({'$or': [{from: userid}, {to: userid}]}, filter, function (err,chatMsgs) {
// 返回包含所有用户和当前用户相关的所有聊天消息的数据
res.send({code: 0, data: {users, chatMsgs}})
})
})
})
/*
修改指定消息为已读
*/
app.post('/readmsg',urlencodedParser, function (req, res) {
// 得到请求中的 from 和 to
const from = req.body.from
const to = req.cookies.userid
console.log(from);
/*
更新数据库中的 chat 数据
参数 1: 查询条件
参数 2: 更新为指定的数据对象
参数 3: 是否 1 次更新多条, 默认只更新一条
参数 4: 更新完成的回调函数
*/
ChatModel.update({from, to, read: false}, {read: true}, {multi: true}, function (err,doc) {
console.log('/readmsg', doc)
res.send({code: 0, data: doc.nModified}) // 更新的数量
})
})
socket.io后台模块:
const {ChatModel}=require('../db/models')
module.exports = function (server) {
// 得到 IO 对象
const io = require('socket.io')(server)
//监视连接(当有一个客户连接上时回调)
io.on('connection', function (socket) {
console.log('soketio connected')
// 绑定 sendMsg 监听, 接收客户端发送的消息
socket.on('sendMsg', function ({from,to,content}) {
console.log('服务器接收客户端发送',{from,to,content})
//保存消息进数据库,再将保存信息返回给前台
const chat_id=[from,to].sort().join('-');
const create_time=Date.now();
new ChatModel({from,to,content,chat_id,create_time}).save(function(err,chatMsg){
//向客户端发送数据
io.emit('receiveMsg',chatMsg);
})
})
})
}