Webpack 4.X + React + Node + Mongodb チャット ルームをゼロから構築する (2)

前回の記事では基本的なフレームの構築を行いましたが、今回は関数ロジックを詳細に実装していきます。

完成した機能:

  • ユーザー登録、ログイン
  • ユーザーがチャット ルームに出入りすると、現在のチャット ルームにいるすべてのユーザーに通知されます。
  • 1 人のユーザーがグループ チャットを追加すると、すべてのユーザーがそれを見ることができます。
  • ユーザーは誰とでもリアルタイムでチャットできます
  • ユーザーはチャット リストとチャット記録をオフラインで保存できます
  • ユーザーのアバターをクリックしてプライベート チャットを追加します
  • ユーザーはリアルタイムでお互いにプライベートにチャットできます
  • チャット ルームはユーザーからの未読メッセージを記録します
  • ユーザーが関数を入力中です

リソースリンク: https://github.com/zhangyongwnag/chat_room



1. ソケット接続を確立する

前回の記事では、基本的にバックグラウンド サービスをセットアップしました。サーバーを使用してサービスを開始したことのある人は、socket.iosocket基本に、バックグラウンド サービスをクライアントと組み合わせて使用​​する必要があることを理解しています。ここに画像の説明を挿入します
socket.iosocket.io-client

1. ダウンロード

npm iソケット.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)
  })
})

テストの効果: コントロールがクライアントから送信されたメッセージを出力していることがわかります。
ここに画像の説明を挿入します

2. クライアント側でステータス管理を追加する

クライアント:状態管理を実装するためにreduxと を使用します。react-redux

1. ダウンロード

npm i redux act-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. 店内に注入
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]
      }
	})
  }
...

コンソールに操作のログが出力されていることがわかります。
ここに画像の説明を挿入します

3. 機能の実装

①:ユーザー登録とログイン
選別工程
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、データの戻り形式を均一に処理するために使用されるイベントがあることに気づくかもしれません。200100

/**
 * @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})))
  })

クライアント:正常に登録または再ログインした後、チャット ルーム リストを取得し、onceID に基づいてデフォルトの最初のチャット ルームに参加することを選択します。

  核心代码:
  
  // 获取聊天列表
  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 人のユーザーがグループ チャットを追加すると、すべてのユーザーが閲覧できます。
選別工程
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 とチャットを開始すると、チャット ルーム名は AB になり、それ以外の場合はチャット ルーム名は BA になります。

ここではクライアントについてはあまり紹介しません。アバターをクリックしてプライベート チャットを追加します。ここでは主にサーバーについて紹介します。

コアコード:

...
	/**
	 * @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 人とリアルタイムでプライベートチャットを行うことができます
選別工程
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、現在のソケット接続サービスを表し、次の形式Objectkey-value保存されます。

ユーザーの情報は接続するたびに異なるためsocketsocket_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)
	  })
	})
...

エフェクトデモンストレーション:
ここに画像の説明を挿入します
この時点で、すべての機能が完了しました。


4. 関連記事

おすすめ

転載: blog.csdn.net/Vue2018/article/details/107533478