Webpack 4.X + React + Node + Mongodb 从零搭建聊天室(二)

上篇文章我们把框基本搭建起来,本篇文章,我们具体实现功能逻辑

已完成功能:

  • 用户注册、登录
  • 用户进入/离开聊天室,通知当前聊天室内所有用户
  • 单个用户新增群聊,所有用户可以看到
  • 用户可实时与所有人聊天
  • 用户离线保留聊天列表、聊天记录
  • 点击用户头像,新增私聊
  • 用户可实时单人私聊
  • 聊天室记录用户未读消息
  • 用户正在输入功能

资源链接https://github.com/zhangyongwnag/chat_room



一、建立socket连接

上篇文章,我们把后台服务基本搭建起来,并且利用socket.io服务端启动socket服务在这里插入图片描述
用过socket.io的大家基本都了解,他必须与客户端的socket.io-client搭配使用

1、下载

npm i socket.io-client -S

2、客户端创建连接
import React, {
    
    Component} from 'react'

let socket = require('socket.io-client')('http://127.0.0.1:3001')

我们可以看到已经创建连接成功了
在这里插入图片描述

3、测试交互

接下来,我们客户端与服务端进行交互

// 客户端
socket.emit('init','客户端发送了消息')

// 服务端
io.on('connection', socket => {
    
    
  socket.on('init',data => {
    
    
    console.log(data)
  })
})

测试效果:我们可以看到控制打印出客户端发送的消息
在这里插入图片描述

二、客户端添加状态管理

客户端:我们用到了reduxreact-redux来实现状态管理

1、下载

npm i redux react-redux -S // redux状态管理
npm i redux-logger redux-thunk -S // redux中间件

2、使用

index.js根目录

import React from 'react'
import ReactDOM from 'react-dom'
import App from './pages/App'
import './assets/css/index.css'
import {
    
    Provider} from 'react-redux' // Provider组件用来注入store对象
import {
    
    createStore,applyMiddleware,compose} from 'redux' // 挂在中间件
import reducer from './store' // 引入reducer
import thunk from "redux-thunk"; // 改造store.dispatch
import logger from 'redux-logger' // 控制台打印reducer日志

// 创建store对象
const store = createStore(
  reducer,
  compose(
    applyMiddleware(thunk),
    applyMiddleware(logger)
  )
)

ReactDOM.render(
  <Provider store={
    
    store}>
    <App/>
  </Provider>,
  document.getElementById('app'))

if (module.hot){
    
    
  module.hot.accept(() => {
    
    

  })
}

创建store目录,并且连接多个reducer

store/index.js连接多个reducer

import {
    
    combineReducers} from 'redux' // 引入中间件

import room from './reducer/room' // 引入聊天室列表 reducer
import records from "./reducer/records"; // 引入聊天记录 reducer

export default combineReducers({
    
    
  room, // 聊天室列表
  records, // 聊天记录
})

store/module/room.js聊天室列表reducer

/**
 * @description 聊天术列表
 * @param {
 *   {Object} state {
 *     {Object} room:当前所在的聊天室信息
 *     {Array} room_list:聊天室列表
 *   }
 * }
 */
export default function (state = {
    
    }, action) {
    
    
  switch (action.type) {
    
    
    case 'get':
      return [...state]
    case 'add':
      return [...state, action.data]
    case 'set':
      let result = Object.assign(state, action.data)
      return {
    
    
        room: result.room, // 当前所处的聊天室
        room_list: [...result.room_list] // 聊天室列表
      }
    default:
   		return state
  }
}

records同理如此

3、注入store
import React, {
    
    Component} from 'react'
import {
    
    connect} from 'react-redux' // connect中间件,用来绑定store对象于props

class App extends Component {
    
    
  constructor(props) {
    
    
    super(props)
  }
  render(){
    
    
    return (<div>init</div>) 
  }
}

function mapStateToProps(state) {
    
    
  // 注册state
  return {
    
    
    room: state.room.room,
    records: state.records
  }
}

export default connect(mapStateToProps)(App) // 注入props
4、测试效果
...
  componentDidMount() {
  	 this.props.dispatch('set', {
  	  data:{
 	      room: {
            room_id: '1',
            room_item: {}
          },
          room_list:[1,2,3,4]
      }
	})
  }
...

我们可以看到,控制台打印出我们操作的日志
在这里插入图片描述

三、实现功能

①:用户注册、登录
梳理流程
1. 用户进入客户端,如果本地用户信息存在,去登陆,反之去注册
2. 客户端发起注册申请,并携带用户注册名称,发送到服务端
3. 服务端判断用户是否已注册,如果未注册,将用户名称( 唯一约束 )插入到用户表,反之注册失败,返回失败信息,提示用户重新注册
4. 每个用户注册成功后,向聊天室列表里插入一条默认所有人的聊天室
5. 返回当前注册用户的聊天室列表

客户端:

如果本地用户信息不存在,前往注册,反之直接登录

...
 let socket = require('socket.io-client')('http://127.0.0.1:3001') // 创建socket连接

 class App extends Component {
    
    
  constructor(props) {
    
    
    super(props)
    this.state = {
    
    
      userInfo: {
    
    }, // 用户信息
    }
  }

  componentDidMount() {
    
    
    // 如果本地信息不存在,则去注册,反之获取聊天列表
    if (localStorage.getItem('userInfo')) {
    
    
      let userInfo = JSON.parse(localStorage.getItem('userInfo'))
      socket.emit('login', userInfo._id)
    } else {
    
    
      this.register()
    }
  }
  
  // 注册用户
  register = () => {
    
    
    let name = prompt('请输入用户名')
    // 如果输入去掉空格
    name != null ? name = name.replace(/\s+/g, "") : ''
    if (name == null || !name) {
    
    
      this.register()
    } else if (name.length > 6) {
    
    
      alert('用户名不得超过6位')
      this.register()
    } else if (name) {
    
    
      // 去注册
      socket.emit('chat_reg', name)
    }
  }
...

服务端:处理用户登录 | 用户注册 | 获取单个用户的聊天室列表

...
let User = require('./module/User') // 用户model
let Room = require('./module/Room') // 聊天室model
let Records = require('./module/Records') // 聊天记录model

io.on('connection', socket => {
    
    
  /**
   * @description 用户静默登录
   * @param {String | ObjectId} userId:登录的用户id
   */
  socket.on('login', userId => {
    
    
    // 更新用户列表socketId
    User.updateOne({
    
    _id: ObjectId(userId)}, {
    
    $set: {
    
    socket_id: socket.id}}, function (err, result) {
    
    
      socket.emit('login', socket.id)
    })
  })

  /**
   * @description 用户注册
   * @param {String} username:要注册的用户名称
   */
  socket.on('chat_reg', username => {
    
    
    let user = new User({
    
    
      user_name: username,
      current_room_id: '',
      socket_id: socket.id
    })
    // 注册用户插入数据库
    user.save()
      .then(res => {
    
    
        // 注册事件
        socket.emit('chat_reg', createResponse(true, res))
        let room = new Room({
    
    
          user_id: res._id.toString(),
          user_name: username,
          room_name: '所有人',
          status: 0,
          num: 0,
          badge_number: 0,
          current_status: false
        })
        // 默认所有人聊天室插入数据库
        room.save()
          .then(response => {
    
    
            // 首次发送用户聊天室列表
            socket.emit('get_room_list', createResponse(true, {
    
    once: true, data: [response]}))
          })
      })
      .catch(err => {
    
    
        // 注册失败
        socket.emit('chat_reg', createResponse(false, '注册失败,用户已注册'))
      })
  })

  /**
   * @description 请求聊天列表
   * @param {String | ObjectId} userId:用户ID
   */
  socket.on('get_room_list', userId => {
    
    
    Room.find({
    
    user_id: userId})
      .then(data => socket.emit('get_room_list', createResponse(true, {
    
    once: true, data})))
  })
...

这里大家可能会注意到,有一个createResponse事件,用来统一处理数据返回格式,200是成功,100是失败

/**
 * @description 创建响应体
 * @param {Boolean} status : 是否成功
 * @param {String | Array | Object | Boolean | Number | Symbol} data : 返回的数据
 */
function createResponse(status, data) {
    
    
  return {
    
    
    code: status ? 200 : 100,
    data,
    msg: status ? 'success' : 'error'
  }
}

客户端:

接下来,我们在客户端响应服务端的emit事件消息

我们写一个方法统一管理socket的事件回调

...
	componentDidMount () {
    
    
		...
	    // 开启监听socket事件回调
	    this.socketEvent()
	}
	
	socketEvent = () => {
    
    
	  // 获取注册结果,如果成功保存用户信息,获取聊天室列表,反之继续去注册
	  socket.on('chat_reg', apply => {
    
    
	     if (apply.code == 200) {
    
    
	       localStorage.setItem('userInfo', JSON.stringify(apply.data))
	       this.setState({
    
    
	         userInfo: apply.data
	       }, () => {
    
    
	         socket.emit('get_room_list', this.state.userInfo._id)
	       })
	     } else {
    
    
	       alert(apply.data)
	       this.register()
	     }
	   })
	  // 获取聊天列表
      socket.on('get_room_list', apply => {
    
    ...})
	}
...
②:用户进入/离开聊天室,通知当前聊天室内所有用户
梳理流程
1. 用户注册成功后,获取到聊天室列表,默认选择加入第一个聊天室
2. 用户加入聊天室后,通知聊天室内所有用户:xxx加入聊天。反之通知聊天室内所有用户:xxx离开聊天室

服务端:注册成功或者登录返回聊天室列表,携带特殊标识once,用来标识用户初始化连接,并默认加入聊天室

 核心代码:

  /**
   * @description 请求聊天列表
   * @param {String | ObjectId} userId:用户ID
   */
  socket.on('get_room_list', userId => {
    
    
    Room.find({
    
    user_id: userId})
      .then(data => socket.emit('get_room_list', createResponse(true, {
    
    once: true, data})))
  })

客户端:注册成功或者重新登录后,获取聊天室列表,并且根据once标识选择加入默认第一个聊天室

  核心代码:
  
  // 获取聊天列表
  socket.on('get_room_list', apply => {
    
    
     let room_list = apply.data.data.filter(item => item.user_id == this.state.userInfo._id)
     let room_id = room_list[0]._id.toString()
     let room_item = room_list[0]
     // 保存用户聊天室信息、列表
     this.props.dispatch({
    
    
       type: 'set',
       data: {
    
    
         room: {
    
    
           room_id,
           room_item,
         },
         room_list
       }
     })
     // 如果存在首次获取标识once,用户加入聊天室
     if (apply.data.once) {
    
    
       // 加入某个聊天室
       socket.emit('join', {
    
    
         roomName: this.props.room.room_item.room_name,
         roomId: this.props.room.room_id,
         userId: this.state.userInfo._id,
         userName: this.state.userInfo.user_name
       })
     }
   })

接下来,服务端处理用户加入聊天室逻辑

梳理逻辑

1. 用户join聊天室,创建一个room

2. 校验用户当前聊天室,判断是否重复加入

3. 如果非重复加入

  • 更新当前用户所处的聊天室ID (users表 - current_room_id 字段)
  • 更新当前用户聊天室的状态 (rooms表 - current_status 字段)
  • 清空当前用户当前聊天室的未读消息 (rooms表 - badge_number 字段)
  • 获取当前聊天室下的聊天记录 (records表 - room_name 字段)
  • 更新当前聊天室的在线人数 (rooms表 - num字段)
  • 给当前聊天室所有人推送服务消息:xxx加入聊天室

4. 加入聊天室成功

5. 这里的系统服务消息,不做入库处理

服务端核心代码:

  /**
   * @description 用户退出/加入聊天室
   * @param data {
   *   {String | ObjectId} userId:当前离线用户ID
   *   {String | ObjectId} roomId:当前用户所处聊天室ID
   *   {String} roomName:当前用户所处聊天室名称
   * }
   */
  socket.on('join', data => {
    
    
    // 创建room
    socket.join(data.roomName)
    // 找到用户的当前所在聊天室
    User.findOne({
    
    _id: ObjectId(data.userId)}, function (error, user_data) {
    
    
      // 如果用户的前后俩次聊天室一致,则不更新,反之加入成功
      if (user_data.current_room_id != data.roomId) {
    
    
      	...
      	// 对所有用户发送消息,这里
        io.sockets.in(data.roomName).emit('chat_message', createResponse(true, {
    
    
           action: 'add', // 添加聊天消息
           data: {
    
    
              user_id: data.userId,
              user_name: data.userName,
              room_name: data.roomName,
              chat_content: `${
      
      data.userName}加入了聊天室`,
              status: 0 // 0代表系统服务消息,1代表用户消息
            }
         }))
      }
  })

客户端处理服务端推送的消息,并渲染到页面上:

...
	// 获取聊天消息
	socket.on('chat_message', data => {
    
    
	  if (data.data.action == 'set') {
    
    
	    this.props.dispatch({
    
    
	      type: 'set_records',
	      data: data.data.data
	    })
	  } else if (data.data.action == 'add') {
    
    
	    this.props.dispatch({
    
    
	      type: 'add_record',
	      data: data.data.data
	    })
	 }
      // 聊天置底
	  this.updatePosition()
	  // 桌面消息通知有新的消息,这里因为安全问题,https可使用
	  // this.Notification()
	})
	
	// 聊天记录到达底部
    updatePosition = () => {
    
    
      let ele = document.getElementsByClassName('chat_body_room_content_scroll')[0]
      ele.scrollTop = ele.scrollHeight
    }
    
    // 新消息通知
    Notification = () => {
    
    
      let n = new Notification('会话服务提醒', {
    
    
        body: '您有新的消息哦,请查收',
        tag: 'linxin',
        icon: require('../assets/img/chat_head_img.jpg'),
        requireInteraction: true
      })
    }
...

我们看下基础实现效果:
在这里插入图片描述

③:单个用户新增群聊,所有用户可以看到
梳理流程
1. 添加群聊,名称唯一约束
2. 给当前所用用户聊天室列表添加此群聊
3. 添加成功后
  • 离开当前所在聊天室,给当前聊天室在线的用户发送离开消息(不包括自己),当前所在聊天室的人数- 1
  • 进入新添加的聊天室,新聊天的在线人数+ 1
4. 离开当前聊天室 (socket.leave)!!!

服务端核心代码

// 更新离开的聊天室在线人数
Room.updateMany({
    
    room_name: data.leaveRoom.roomName}, {
    
    $inc: {
    
    num: -1}}, function () {
    
    })
// 给当前聊天室用户发送离开信息,不包括自己
socket.broadcast.to(data.leaveRoom.roomName).emit('chat_message', createResponse(true, {
    
    
  action: 'add',
  data: {
    
    
    user_id: data.leaveRoom.userId,
    user_name: data.leaveRoom.userName,
    room_name: data.leaveRoom.roomName,
    chat_content: `${
      
      data.leaveRoom.userName}离开了聊天室`,
    status: 0
  }
}));
// 离开聊天室
socket.leave(data.leaveRoom.roomName)

我们看下实现的效果:
在这里插入图片描述

④:用户可实时与所有人聊天
梳理流程
1. 用户发送消息到服务端,向当前聊天室在线的用户推送消息
2. 当前聊天室不在线用户的未读消息数量 + 1
3. 聊天消息入库处理

客户端
Html

...
   <div className='chat_body_room_input'>
      <div className='chat_body_room_input_warp'>
        <input id='message' type="text" placeholder='请输入聊天内容'
               onKeyUp={() => event.keyCode == '13' ? this.sendMessage() : ''}/>
      </div>
      <div className='chat_body_room_input_button' onClick={this.sendMessage}>点击发送</div>
    </div>
...

App.js

// 发送消息
sendMessage = () => {
    
    
  let ele = document.getElementById('message')
  if (!ele.value) return
  socket.emit('chat_message', {
    
    
    roomName: this.props.room.room_item.room_name,
    userId: this.state.userInfo._id,
    userName: this.state.userInfo.user_name,
    chat_content: ele.value
  })
  ele.value = ''
}

服务端核心代码:

/**
 * @description 处理聊天信息
 * @param data {
 *   {String | ObjectId} userId:当前离线用户ID
 *   {String} username:当前用户名称
 *   {String} roomName:当前用户所处聊天室名称
 *   {String} chat_content:聊天内容
 * }
 */
socket.on('chat_message', data => {
    
    
  // 更新当前聊天室不在线用户的未读消息数量
  Room.updateMany({
    
    room_name: data.roomName, current_status: false}, {
    
    $inc: {
    
    badge_number: 1}})
    .then(res => {
    
    
      // 更新聊天列表
      updateRoomList()
      // 消息入库处理,并且发送消息至在线用户
      insertChatMessage({
    
    
        user_id: data.userId,
        user_name: data.userName,
        room_name: data.roomName,
        chat_content: data.chat_content,
        status: 1
      })
    })
})

/**
 * @description 消息入库
 * @param data {
 *   {String | ObjectId} userId:用户ID
 *   {String} username:用户名称
 *   {String} roomName:聊天室名称
 *   {String} chat_content:;聊天内容
 *   {Number} status:0是系统消息,其他代表用户消息
 * }
 */
function insertChatMessage(data) {
    
    
  let record = new Records(data)
  record.save()
    .then(res => {
    
    
      sendMessageRoom(data)
    })
    .catch(err => {
    
    
      console.log('插入失败')
    })
}

/**
 * @description 给当前聊天室用户发消息
 * @param {Object} data:插入的聊天记录
 */
function sendMessageRoom(data) {
    
    
  io.sockets.in(data.room_name).emit('chat_message', createResponse(true, {
    
    
    action: 'add',
    data,
  }))
}

我们看下实现的效果:
在这里插入图片描述

⑤:用户离线保留聊天列表、聊天记录

用户在关闭浏览器时,监听用户离开,对用户的离线状态进行设置
梳理流程
: 1. 更新用户当前所处的聊天室ID(users表 - current_room_id 字段)
: 2. 更新用户的所有聊天室的状态(rooms表 - current_status 字段)
: 3. 更新当前聊天室在线人数 - 1
: 4. 给当前聊天室在线用户推送用户离线消息(不包括自己)
: 5. 用户离开房间(socket.leave)!!!

客户端:

// 监听浏览器刷新/关闭事件
listenClose = () => {
    
    
  if (navigator.userAgent.indexOf('Firefox')) {
    
    
    window.onbeforeunload = () => {
    
    
      socket.emit('off_line', {
    
    
        userName: this.state.userInfo.user_name,
        userId: this.state.userInfo._id,
        roomName: this.props.room.room_item.room_name,
      })
    }
  } else {
    
    
    window.onunload = () => {
    
    
      socket.emit('off_line', {
    
    
        userName: this.state.userInfo.user_name,
        userId: this.state.userInfo._id,
        roomName: this.props.room.room_item.room_name,
      })
    }
  }
}

服务端:

/**
 * @description 用户离线
 * @param data {
 *   {String | ObjectId} userId:当前离线用户ID
 *   {String} roomName:当前用户所处聊天室名称
 * }
 */
socket.on('off_line', data => {
    
    
  // 更新当前离线用户所处的聊天室
  User.updateOne({
    
    _id: ObjectId(data.userId)}, {
    
    $set: {
    
    current_room_id: ''}})
    .then(res => {
    
    
       // 更新当前用户所有聊天室的所处状态
       Room.updateMany({
    
    user_id: data.userId}, {
    
    $set: {
    
    current_status: false}})
      	.then(res => {
    
    ...})
   })
})

效果演示
在这里插入图片描述

⑥:点击用户头像,新增私聊
梳理流程
1. 用户点击聊天记录非自己头像,新增私聊
2. 用户离开上一个聊天室,进入新的聊天室
  • 离开之前聊天室,如果是群聊,群聊在线人数 - 1,如果是私聊,用户离线
  • 用户进入新的聊天室,如果是群聊,在线人数 + 1,如果是私聊,不做任何操作
  • 如果是私聊,聊天室名称为 发起聊天的用户名-私聊的对方用户名,所以我们这里判断要注意,这里用 in 关键字
  • 例如:A 发起与 B 聊天,那么聊天室名称为 A-B,反之聊天室名称为 B-A

客户端这里就不多介绍了,点击头像新增私聊,这里主要介绍服务端:

核心代码:

...
	/**
	 * @description 新增私聊
	 * @param data {
	 *   {String | ObjectId} userId:当前用户ID
	 *   {String} username:当前用户名称
	 *   {String} userOtherId:与之聊天的用户ID
	 *   {String} userOtherName:与之聊天的用户名称
	 * }
	 */
	socket.on('add_private_chat', data => {
    
    
	  // 新增私聊聊天室
	  addPrivateRoom(socket, data)
	})

	/**
	 * @description 新增私聊用户
	 * @param {Object} socket:socket对象
	 * @param {Object} data:新增私聊用户信息
	 */
	function addPrivateRoom(socket, data) {
    
    
	  // 如果数据库不存在则添加,反之加入房间
	  Room.find({
    
    user_id: data.userId}).where('room_name').in([`${
      
      data.userName}-${
      
      data.userOtherName}`, `${
      
      data.userOtherName}-${
      
      data.userName}`]).exec((err, roomList) => {
    
    
	    if (err) return
	    if (roomList.length) {
    
    
	      socket.emit('add_private_chat', createResponse(true, roomList[0]))
	    } else {
    
    
	      let room = new Room({
    
    
	        user_id: data.userId.toString(),
	        user_name: data.userName,
	        room_name: `${
      
      data.userName}-${
      
      data.userOtherName}`,
	        status: 1,
	        num: 0,
	        badge_number: 0,
	        current_status: false
	      })
	      room.save()
	        .then(res => {
    
    
	          Room.find({
    
    user_id: data.userId})
	            .then(result => {
    
    
	              socket.emit('room_list_all', createResponse(true, result))
	              socket.emit('add_private_chat', createResponse(true, result.filter(item => item.room_name == `${
      
      data.userName}-${
      
      data.userOtherName}`)[0]))
	            })
	        })
	    }
	  })
	}
...
⑦:用户可实时单人私聊
梳理流程
1. 用户点击非自己头像,可新增/加入私聊
2. 判断该用户是否拥有此聊天室,如果拥有,加入聊天室,反之创建聊天室并加入
3. 如果用户未拥有聊天数,则添加聊天室
  • 离开当前所在聊天室,给当前聊天室在线的用户发送离开消息(不包括自己),当前所在聊天室的人数- 1
  • 加入新添加的私聊聊天室
  • 当前用户在新添加的聊天室发送聊天消息时,判断私聊的对方是否拥有聊天室
  • 如果未拥有,私聊的对方添加当前聊天室(属于2个人)并加入!!!
  • 如果拥有,但未在线(焦点不在聊天室),私聊的对方未读消息数量 +1
4. 如果用户拥有此聊天室,直接加入聊天室即可

服务端核心代码:

/**
 * @description 处理聊天信息
 * @param data {
 *   {String | ObjectId} userId:当前离线用户ID
 *   {String} username:当前用户名称
 *   {String} roomName:当前用户所处聊天室名称
 *   {String} chat_content:聊天内容
 *   {Number} status:0为群聊,其他为私聊
 * }
 */
socket.on('chat_message', data => {
    
    
  // 如果是群聊
  if (data.status == '0') {
    
    
  	// 更新当前聊天室不在线用户的未读消息数量
  	...
  	// 给所有在线用户发消息
  	...
  }else if (data.status == '1'){
    
    
  	// 如果当前用户不存在聊天室,添加聊天室并且加入聊天室
  	...
  	// 如果当前用户存在聊天室,判断当前用户是否在线,如果不在线,未读消息数量 + 1
  	...
  }

效果演示:
在这里插入图片描述

⑧:用户正在输入
梳理流程
1. 如果是私聊,监听用户输入
2. 客户端监听用户输入框输入,并告诉服务端用户开始输入
3. 如果接下来的一定时间内(这里500毫秒),用户继续输入,则不发送停止消息,否则告诉服务端用户停止输入
4. 这里主要通过函数防抖,来执行推送输入完毕消息到服务端,告知服务端用户输入完毕

客户端:

// html
...
<input id='message' type="text" placeholder='请输入聊天内容' onInput={
    
    this.inputting} 
onKeyUp={
    
    () => event.keyCode == '13' ? this.sendMessage() : ''}/>
...

---------------------------------------------------------------------------
// 正在输入
inputting = () => {
    
    
  // 如果是私聊,告诉服务端用户正在输入
  if (this.props.room.room_item.status == '1') {
    
    
    socket.emit('inputting', {
    
    
      userName: this.state.userInfo.user_name,
      roomName: this.props.room.room_item.room_name,
      status: true
    })
    // 500秒后,告诉用户输入完毕
    this.debounce(this.inputtingEnd, 500)
  }
}

// 用户结束输入
inputtingEnd = () => {
    
    
  socket.emit('inputting', {
    
    
    userName: this.state.userInfo.user_name,
    roomName: this.props.room.room_item.room_name,
    status: false
  })
}

// 函数防抖
debounce = (fun, delay) => {
    
    
  clearTimeout(fun.timer)
  fun.timer = setTimeout(() => {
    
    
    fun()
  }, delay)
}

服务端:

...
	/**
	 * @description 私聊监听用户输入
	 * @param {String} username:当前用户名称
	 * @param {String} roomName:当前聊天室名称
	 * @param {Boolean} status: 用户是否正在输入
	 */
	socket.on('inputting', data => {
    
    
	  User.findOne({
    
    user_name: data.roomName.replace(data.userName, '').replace('-', '')})
	    .then(res => {
    
    
	      // 如果用户存在
	      if (res != null) {
    
    
	        res.roomName = data.roomName
	        res.status = data.status
	        // 给某个用户发消息
	        sendMessageSingleUser(res)
	      }
	    })
	})

	/**
	 * @description 给某个用户发消息
	 * @param user:用户信息
	 */
	function sendMessageSingleUser(user) {
    
    
	  // 如果用户不在线的话,不推送(我们在用户离线时,把他的current_room_id置为空)
	  if (user.current_room_id) {
    
    
	    Room.find({
    
    user_id: user._id}, function (err, data) {
    
    
	      io.sockets.sockets[user.socket_id].emit('inputting', createResponse(user.status, user))
	    })
      }
	}
...

大家可以看到给某个用户发消息(这里可以把消息发送到当前聊天室,客户端自行判断)

这里用到了io.sockets.sockets,代表当前的socket连接服务,是一个Object,以key-value方式存储

因为用户每次连接socket后的socket_id都不一样,所以我们要在用户建立连接后,把他的socket_id更新

客户端:

...
	componentDidMount() {
    
    
	  // 如果本地信息不存在,则去注册,反之获取聊天列表
	  if (localStorage.getItem('userInfo')) {
    
    
	    let userInfo = JSON.parse(localStorage.getItem('userInfo'))
	    socket.emit('login', userInfo._id)
	  } else {
    
    
	    this.register()
	  }
	}
...
	// 获取登录结果
	socket.on('login', socket_id => {
    
    
	  userInfo.socket_id = socket_id
	  localStorage.setItem('userInfo', JSON.stringify(userInfo))
	  this.setState({
    
    
	    userInfo
	  }, () => {
    
    
	    socket.emit('get_room_list', userInfo._id)
	  })
	})
...

服务端:

...
	/**
	 * @description 用户静默登录
	 * @param {String | ObjectId} userId:登录的用户id
	 */
	socket.on('login', userId => {
    
    
	  // 更新用户列表socketId
	  User.updateOne({
    
    _id: ObjectId(userId)}, {
    
    $set: {
    
    socket_id: socket.id}}, function (err, result) {
    
    
	    socket.emit('login', socket.id)
	  })
	})
...

效果演示:
在这里插入图片描述
至此,我们就完成了所有功能!!!


四、相关文章

猜你喜欢

转载自blog.csdn.net/Vue2018/article/details/107533478