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

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

前言:最近在学习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通信模型 

    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)}
    >
    

    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 

 

二、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)
        })
    })

 

三、聊天页面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中:设置聊天信息相关的reduceraction 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})
            }
        })
    })
    

 

四、聊天功能实现  

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

  • 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)
        })
    })
    

  • 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))
            })
        })
    })

      

  • 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中

    从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>
                        )
                    })}
    

      

       

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

  • 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 : ''}
    

     

六、聊天头像显示  

  • 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})
                }
            })
        })
    })
    

  • 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}/>}
    

 

七、修正未读消息数量  

  • 消息详情页中:【只显示与当前选择用户的信息】
  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 = '? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
                   ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
                   ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
                   ? ? ? ? ? ? '
                   .split(" ")
                   .filter(v=>v)
                   .map(v=>({text:v}));
  • 点击【提交】时:设置state中的showEmoji为false,控制emoji列表隐藏

    this.setState({
           text: '',
           showEmoji: false
    })
  • 通过Grid组件显示emoji列表:判断state中的showEmoji,为true时显示

    {this.state.showEmoji ? <Grid 
                data={emoji}
                columnNum={9}
                carouselMaxRow={4}
                isCarousel={true}
                onClick={el=>{
                     this.setState({
                           text: this.state.text+el.text
                     })
                }}
    > : null} 
  • 坑:Grid组件官方Bug,滚动列表的初始状态只显示一行,只有手动触发一个事件才能显示全部

  • 解决:官方推荐解决方法是,定义一个方法,手动发送一个事件

    fixCarousel(){
            setTimeout(function(){ 
                window.dispatchEvent(new Event('resize'))
            }, 0)
    }
  • 点击【?】时:控制emoji列表的显示隐藏,同时调用fixCarousel方法修正bug

    <span
           style={{marginRight:15}}
           onClick={()=>{
                 this.setState({
                      showEmoji: !this.state.showEmoji
                 })
                 this.fixCarousel()
            }}
    >?</span>
    

      


注:项目来自慕课网

posted @ 2019-01-23 10:14 柳洁琼Elena 阅读( ...) 评论( ...) 编辑 收藏
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值