【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

时间:2022-06-05 05:22:16

前言:最近在学习Redux+react+Router+Nodejs全栈开发高级课程,这里对实践过程作个记录,方便自己和大家翻阅。最终成果github地址:https://github.com/66Web/react-antd-zhaoping,欢迎star。


一、Socket.io基础知识

    基于事件的实时双向通信库 

  • 基于websocket协议
  • 前后端通过事件进行双向通信
  • 配合express,快速开发实时应用

Socket.io和Ajax区别

  • 基于不同的网络协议
  • 注意:Socket.io是实现websocket协议的一个库
  • Ajax基于http协议,单向,实时获取数据只能轮询
  • socket.io基于websocket双向通信协议,后端可以主动推送数据

    Socket.io通信模型 

【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

Socket.io后端API

  • 配合express
  • 前端:import io from 'socket.io-client'
    npm install socket.io-client --save  
  • 后端:Io = require('socket.io')(http)
    npm install socket.io --save  
  • io.on 监听事件
  • io.emit 触发事件

    聊天页面的跳转 

  • componet目录下:创建chat聊天组件目录,获取this.props.match(命中).params(参数) .user(用户名)
    import React from 'react'
    
    class Chat extends React.Component{
    render(){
    //console.log(this.props)
    return (
    <h2>chat with user: {this.props.match.params.user}</h2>
    )
    }
    } export default Chat
  • usercard.js中:通过withRouter,this.props.history.push(`拼接后的路由地址`),实现聊天页面的跳转

    import {withRouter} from 'react-router-dom'
    
    @withRouter
    
    handleClick(v){
    this.props.history.push(`/chat/${v.user}`)
    } <Card
    key={v._id}
    onClick={() => this.handleClick(v)}
    >

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

    Socket.io前后端联通

  • 后端server.js中:绑定socket与express
    const app = express()
    
    //work with express
    const server = require('http').Server(app) //express server 用http包裹
    const io = require('socket.io')(server) //再传给socket.io对象,使其与express关联起来 //监听connection事件
    io.on('connection', function(socket){
    console.log('user login')
    }) //改app.listen 为server.listen:使socket.io 与express成功绑定
    server.listen(9093, function(){
    console.log('Node app start at port 9093')
    })
  • 前端chat.js中:发起socket连接,这里因为前后端端口号不一致,需要跨域手动连接socket

    import io from 'socket.io-client'
    
    const socket = io('ws://localhost:9093') //前后端端口不一,跨域需要手动连接socket 

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

二、socket.io前后端实时显示消息

  • chat.js中:实现信息输入且广播到全局
  1. 点击【发送】时通过socket.emit触发sendmsg事件:传递state中存储的输入信息,同时将state中信息清空
    handleSubmit(){
    // console.log(this.state.text)
    socket.emit('sendmsg',{text: this.state.text}) //触发事件
    this.setState({text: ''})
    }  
  2. 在componetDidMount中通过socket.on监听recvmsg事件:接收后端传来的全局信息,扩展到当前state中存储的msg数组中
    componentDidMount(){
    socket.on('recvmsg', (data) => {
    // console.log(data)
    this.setState({
    msg: [...this.state.msg, data.text]
    })
    })
    }
  3. 完整代码 ↓

    import React from 'react'
    import {List, InputItem} from 'antd-mobile'
    import io from 'socket.io-client'
    const socket = io('ws://localhost:9093') //前后端端口不一,跨域需要手动连接socket class Chat extends React.Component{
    constructor(props){
    super(props)
    this.state={text: '', msg:[]}
    }
    componentDidMount(){
    socket.on('recvmsg', (data) => {
    // console.log(data)
    this.setState({
    msg: [...this.state.msg, data.text]
    })
    })
    }
    handleSubmit(){
    // console.log(this.state.text)
    socket.emit('sendmsg',{text: this.state.text}) //触发事件
    this.setState({text: ''})
    }
    render(){
    // console.log(this.props)
    return (
    <div>
    {this.state.msg.map(v=>{
    return <p key={v}>{v}</p>
    })}
    <div className="stick-footer">
    <List >
    <InputItem
    placeholder='请输入'
    value={this.state.text}
    onChange={v =>{
    this.setState({
    text:v
    })
    }}
    extra={<span onClick={()=>this.handleSubmit()}>发送</span>}
    >信息</InputItem>
    </List>
    </div>
    </div>
    )
    }
    } export default Chat
  • server.js中:获取并发送全局信息

  1. 通过socket.on监听sendmsg事件,获取前端传来的信息

  2. 再通过socket.emit触发resvmsg事件,将信息传递给前端

    //监听connection事件
    io.on('connection', function(socket){ //io全局的请求
    console.log('user login')
    socket.on('sendmsg', function(data){ //socket当前连接的请求
    console.log(data)
    io.emit('recvmsg', data)
    })
    })

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

三、聊天页面redux链接 

  • model.js中:设置Chat聊天相关的mongodb数据字段
    chat: {
    'chatid':{'type':String, 'require':true},
    'from':{'type':String, 'require':true},
    'to':{'type':String, 'require':true},
    'read':{'type':Boolean, 'default':false},
    'content':{'type':String, 'require':true, 'default':''},
    'create_time':{'type':Number, 'default':new Date().getTime()}
    }
  • chat.redux.js中:设置聊天信息相关的reducer和action creator,首先获取当前用户的信息列表存储到redux中管理

    import axios from 'axios'
    import io from 'socket.io-client'
    const socket = io('ws://localhost:9093') //action type
    const MSG_LIST = 'MSG_LIST' //获取聊天列表 const initState = {
    chatmsg: [],
    unread: 0 //实时维护未读消息的数量
    } //reducer
    export function chat(state=initState, action){
    switch(action.type){
    case MSG_LIST:
    return {...state, chatmsg:action.payload, unread:action.payload.filter(v=>!v.read).length}
    default:
    return state
    }
    } //action creator
    function msgList(msgs){
    return {types:'MSG_LIST', payload:msgs}
    } //操作数据的方法
    export function getMsgList(){
    return dispatch=>{
    axios.get('/user/getmsglist')
    .then(res=>{
    if(res.status==200 && res.data.code==0){
    dispatch(msgList(res.data.msgs))
    }
    })
    }
    }
  • reducer.js中:合并入chat reducer

    import { chat } from './redux/chat.redux'
    
    export default combineReducers({user, chatuser, chat})
    
  • chat.js中: 连接组件和redux,调用getMsgList方法

  1. 通过axios.get调用接口,获取聊天信息列表数据

  2. 通过dispatch提交action修改后的数据存储到redux中

    import {connect} from 'react-redux'
    import {getMsgList} from '../../redux/chat.redux' @connect(
    state => state,
    {getMsgList}
    ) componentDidMount(){
    this.props.getMsgList()
    }
  • user.js中:通过Router.get方法,获取到当前cookies中的user,并查找出Chat中符合条件的所有数据

    //获取聊天信息列表 1.to user 2.from user
    Router.get('/getmsglist', function(req, res){
    const user = req.cookies.user
    //查询多个条件,用$or区分:'$or': [{from:user, to:user}]
    Chat.find({}, function(err, doc){
    if(err){
    return res.json({code:1, msg:'后端出错了'})
    }
    if(doc){
    return res.json({code:0, msgs:doc})
    }
    })
    })

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

四、聊天功能实现  

  • usercard.js中:修改路由的参数为v._id,因为_id是用户在mongodb中的唯一标识
    this.props.history.push(`/chat/${v._id}`)
    

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

  • chat.js中:将当前用户id,与选择的用户id,以及聊天信息msg发送给后端

    handleSubmit(){
    // socket.emit('sendmsg',{text: this.state.text}) //触发事件
    const from = this.props.user._id
    const to = this.props.match.params.user
    const msg = this.state.text
    this.props.sendMsg({from, to, msg})
    this.setState({text: ''})
    }
  1. chat.redux.js中:定义sendMsg方法调用socket.emit触发sendmsg事件,发送数据

    export function sendMsg({from, to, msg}){
    return dispatch=>{
    socket.emit('sendmsg', {from, to, msg})
    }
    }
  2. server.js中:通过socket.on监听sendmsg事件,获取数据

    //监听connection事件
    io.on('connection', function(socket){ //io全局的请求
    socket.on('sendmsg', function(data){ //socket当前连接的请求
    console.log(data)
    })
    })

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

  • server.js中:将from和to代表的用户id进行组合,定义为chatid,作为聊天数据的标识

  1. 重新创建Chat数据库:将数据存入mongodb数据库

  2. 同时通过io.emit触发recvmsg事件:将Chat数据库中的数据全部发送给前端

    //监听connection事件
    io.on('connection', function(socket){ //io全局的请求
    socket.on('sendmsg', function(data){ //socket当前连接的请求
    console.log(data)
    const {from, to, msg} = data
    const chatid = [from, to].sort().join('_')
    Chat.create({chatid, from, to, content:msg}, function(err, doc){ //数据库存入数据
    io.emit('recvmsg', Object.assign({}, doc._doc))
    })
    })
    })

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现  

  • chat.redux.js中:设置读取信息相关的reducer、action creator以及操作数据的方法

  1. 通过socket.on监听recvmsg事件,获取后端传来的Chat数据

  2. 调用dispatch触发action,将获取到的Chat数据存入redux中

    //action type
    const MSG_RECV = 'MST_RECV' //读取信息 //reducer中添加
    case MSG_RECV:
    return {...state, chatmsg:[...state.chatmsg, action.payload], unread:state.unread+1}
    } //action creator
    function msgRecv(msg){
    return {type:MSG_RECV, payload:msg}
    } //操作数据的方法
    export function recvMsg(){
    return dispatch=>{
    socket.on('recvmsg', function(data){
    console.log('recvmsg', data)
    dispatch(msgRecv(data))
    })
    }
    }  
  3. chat.js中:在componentDidMount时调用recvMsg方法,读取信息数据并存入redux中

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

    从props中获取到所有聊天数据,展示到页面中

    render(){
    const user = this.props.match.params.user
    const Item = List.Item
    return (
    <div id='chat-page'>
    <NavBar mode='dark'>
    {this.props.match.params.user}
    </NavBar>
    {this.props.chat.chatmsg.map(v=>{
    return v.from == user ? (
    <List key={v._id}>
    <Item
    >{v.content}</Item>
    </List>
    ) : (
    <List key={v._id}>
    <Item
    extra={'avatar'}
    className='chat-me'
    >{v.content}</Item>
    </List>
    )
    })}

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现  

      

 五、聊天未读消息实时展示

  • dashboard.js中:修改getMsgList和recvMsg的时机,在显示TabBar的页面中都可以获取聊天信息相关数据
    import {getMsgList, recvMsg} from '../../redux/chat.redux'
    
    @connect(
    state => state,
    {getMsgList, recvMsg}
    ) componentDidMount(){
    this.props.getMsgList()
    this.props.recvMsg()
    }
  • navlink.js中:获取redux中的chat数据,判断当v.path为‘/msg’时,通过badge徽标显示unread未读消息数

    import {connect} from 'react-redux'
    
    @connect(
    state => state.chat
    ) <TabBar.Item
    badge={v.path == '/msg' ? this.props.unread : ''}

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

六、聊天头像显示  

  • user.js中:修改获取聊天信息列表的方法
  1. 查找到所有用户:将每个用户的信息以id:{name, avatar}的形式,创建user实例
  2. 获取到当前cookie中的userid:通过$or查找到from或to为userid的聊天信息,将user实例插入返回的json中
    //获取聊天信息列表
    Router.get('/getmsglist', function(req, res){
    const user = req.cookies.userid
    User.find({}, function(e, userdoc){
    let users = {}
    userdoc.forEach(v=>{
    users[v._id] = {name:v.user, avatar:v.avatar}
    })
    Chat.find({'$or':[{from:user},{to:user}]}, function(err, doc){
    if(err){
    return res.json({code:1, msg:'后端出错了'})
    }
    if(doc){
    return res.json({code:0, msgs:doc, users:users})
    }
    })
    })
    })

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

  • chat.redux.js中:修改MSG_LIST相关reducer和action creator,以及getMsgList()将users数据存入redux

    //reducer中
    case MSG_LIST:
    return {...state, users:action.payload.users, chatmsg:action.payload.msgs,
    unread:action.payload.msgs.filter(v=>!v.read).length} //action creator
    function msgList(msgs, users){
    return {type:MSG_LIST, payload:{msgs, users}}
    } //操作数据的方法
    export function getMsgList(){
    return dispatch=>{
    axios.get('/user/getmsglist')
    .then(res=>{
    if(res.status==200 && res.data.code==0){
    dispatch(msgList(res.data.msgs, res.data.users))
    }
    })
    }
    }
  • chat.js中:通过路由参数获取选择用户的userid,从props.chat中获取users,实现导航用户名和头像的显示

    const userid = this.props.match.params.user
    const Item = List.Item
    const users = this.props.chat.users
    if(!users[userid]){
    return null
    } //header用户名显示
    <NavBar
    mode='dark'
    icon={<Icon type="left"/>}
    onLeftClick={() => {
    this.props.history.goBack()
    }}
    >
    {users[userid].name}
    </NavBar> const avatar = require(`../img/${users[v.from].avatar}.png`) //对方:左边显示
    thumb={avatar} //自己:右边显示
    extra={<img src={avatar}/>}

    【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现

七、修正未读消息数量  

  • 消息详情页中:【只显示与当前选择用户的信息】
  1. util.js中:定义工具函数getChatId,将当前用户userid、选择用户targetid连接在一起

    export function getChatId(userId, targetId){
    return [userId, targetId].sort().join('_')
    }
  2. chat.js中:过滤聊天信息,判断当连接好的id与chatid相同时显示

    import {getChatId} from '../../utils'
    
    const chatid = getChatId(userid, this.props.user._id)
    const chatmsgs = this.props.chat.chatmsg.filter(v=>v.chatid==chatid) {chatmsgs.map(v=>{ ……
  • 坑:【重复获取聊天信息并展示出多条相同信息】

  • 原因:dashboard.js多次切换页面时,getMsgList和recvMsg会不停的调用
  • 解决:添加判断,只有当获取到的chat.chatmsg为空时,才执行调用方法,获取消息列表和读取消息

    componentDidMount(){
    if(!this.props.chat.chatmsg.length){
    this.props.getMsgList()
    this.props.recvMsg()
    }
    }
  • 坑:【未读消息数中包含自己发出的信息】

  • 解决:chat.redux.js中在获取unread时添加过滤的判断条件,只有当to的值为当前用户id时,计算为一条未读的消息

  1. 知识点:redux中使用其它地方的数据,通过getState获取全部的状态

  2. getMsgList和recvMsg方法中:通过getState获取到全部状态,将userid传入action
    //action creator
    function msgList(msgs, users, userid){
    return {type:MSG_LIST, payload:{msgs, users, userid}}
    }
    function msgRecv(msg, userid){
    return {userid, type:MSG_RECV, payload:msg}
    } //操作数据的方法
    export function recvMsg(){
    return (dispatch, getState)=>{
    socket.on('recvmsg', function(data){
    // console.log('recvmsg', data)
    const userid = getState().user._id
    dispatch(msgRecv(data, userid))
    })
    }
    } export function getMsgList(){
    return (dispatch, getState)=>{
    axios.get('/user/getmsglist')
    .then(res=>{
    if(res.status==200 && res.data.code==0){
    //console.log('getState',getState())
    const userid = getState().user._id
    dispatch(msgList(res.data.msgs, res.data.users, userid))
    }
    })
    }
    }  
  3. reducer中:添加unread的过滤判断条件,比较action中的to与userid,若相同算为未读信息
    case MSG_LIST:
    return {...state, users:action.payload.users, chatmsg:action.payload.msgs, unread:action.payload.msgs.filter(v=>!v.read&&v.to==action.payload.userid).length}
    case MSG_RECV:
    const n = action.payload.to==action.userid ? 1 : 0
    return {...state, chatmsg:[...state.chatmsg, action.payload], unread:state.unread+n}

      

八、发送emoji表情

  • emoji表情:同样属于文本编码,可以使用输入法表情输入。【更多emoji选择
  • 定义为一串字符串:使用空格分隔开emoji表情,遍历后用空格符分隔,过滤掉有间隔两个空格的返回为text:v对象
    const emoji = '