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

react

前言:最近在学习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中:设置聊天信息相关的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})

    }

    })

    })

四、聊天功能实现  

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

      


注:项目来自慕课网

以上是 【招聘App】—— React/Nodejs/MongoDB全栈项目:socket.io&聊天实现 的全部内容, 来源链接: utcz.com/z/381595.html

回到顶部