socket.io前后端联通,建立聊天室

15 篇文章 0 订阅

node.js 提供了高效的服务端运行环境,但是由于浏览器端对HTML5的支持不一,为了兼容所有浏览器,提供卓越的实时的用户体验,并且为程序员提供客户端与服务端一致的编程体验,于是socket.io诞生。Socket.io将Websocket和轮询 (Polling)机制以及其它的实时通信方式封装成了通用的接口,并且在服务端实现了这些实时机制的相应代码。

Socket.IO 实现了实时双向的基于事件的通讯机制。旨在让各种浏览器与移动设备上实现实时app功能,模糊化各种传输机制。Socket.IO 是跨平台,多种连接方式自动切换,做即时通讯方面的开发很方便,而且能和 expressjs 提供的传统请求方式很好的结合。


代码运行环境

前端后端数据库
ReactExpressMongoDB

主要依赖版本

"express": "^4.17.1",
"socket.io": "^3.1.0",
"socket.io-client": "^3.1.0",

在这里插入图片描述

后端

(聊天室是由用户发起的,所以在建立聊天室模型的同时,也应该建立用户模型)

1.server.js

(项目入口文件,socket.io 配置)

const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const model = require('./model')
const Chat = model.getModel('chat')
const app = express()

const server = require('http').Server(app)

// { cors: true },使得 socket.io 支持跨域
const io = require('socket.io')(server, { cors: true })

// 监听用户连接
io.on('connection', function(socket) {
  // 数据连接
  socket.on('sendmsg', function(data) {
    const { from, to, msg } = data
    // 每个聊天都有唯一的 id
    const chatid = [from, to].sort().join('_')
    // 每一条聊天记录在数据库中创建一个记录
    Chat.create({chatid, from, to, content: msg}, function(err, doc) {
    // 广播(全局)事件
      io.emit('recvmsg', Object.assign({}, doc._doc))
    })
  })
})

const userRouter = require('./user')

app.use(cookieParser())
app.use(bodyParser.json())
app.use('/user', userRouter)
server.listen(9093, function() {
  console.log('server port is 9093')
})
2.model.js
const mongoose = require('mongoose')
const DB_URL = 'mongodb://localhost:27017/goodwork'
mongoose.connect(DB_URL)

const models = {
	user:{
		'user':{type:String, 'require':true},
		'pwd':{type:String, 'require':true},
		'type':{'type':String, 'require':true},
		//头像
		'avatar':{'type':String},
		// 个人简介或者职位简介
		'desc':{'type':String},
		'title':{'type':String},
		'company':{'type':String},
		'money':{'type':String}
	},
	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, deafult: ''},
		'create_time': {type: Number, deafult: new Date().getTime()}
	}
}

for(let m in models){
	mongoose.model(m, new mongoose.Schema(models[m]))
}

module.exports = {
	getModel:function(name){
		return mongoose.model(name)
	}
}
3.user.js

(路由文件,编写消息列表路由和已读消息列表路由)

const { json } = require('body-parser')
const express = require('express')
const utils = require('utility')
const Router = express.Router()
const model = require('./model')
const User = model.getModel('user')
const Chat = model.getModel('chat')

const _filter = { 'pwd': 0, '__v': 0 }

/**
 * 获取聊天信息
 */
Router.get('/getmsglist', function(req, res) {
  // 使用 cookies 进行登录验证
  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: 0,
          msgs: doc,
          users: users
        })
      }
    })
  })
})
/**
 * 已读消息
 */
Router.post('/readmsg', function (req, res) {
  const userid = req.cookies.userid
  const { from } = req.body
  console.log(userid, from)
  Chat.updateMany(
    { from, to: userid },
    { '$set': { read: true } },
    { 'multi': true },
    function (err, doc) {
      console.log(doc)
      if (!err) {
        return res.json({
          code: 0,
          num: doc.nModified
        })
      }
      return res.json({
        code: 1,
        msg: '出错了'
      })
    })
})

module.exports = Router

前端

(前端编写较为复杂,涉及各个页面之间的跳转,redux 之间的数据管理,这里笔者无法将所有细节描述清楚,请各位见谅)

前端页面使用 redux 进行数据管理

1.redux / chat.redux.js
import axios from 'axios'
import io from 'socket.io-client'
const socket = io('ws://localhost:9093')

// 聊天列表
const MSG_LIST = 'MSG_LIST'
// 读取信息
const MSG_RECV = 'MSG_RECV'
// 标识已读
const MSG_READ = 'MSG_READ'

const initState = {
  chatmsg: [],
  users: {},
  unread: 0
}

export function chat(state = initState, action) {
  switch (action.type) {
  // 聊天列表
    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
      }
    // 标识已读
    case MSG_READ:
      const { from } = action.payload
      return {
        ...state, 
        chatmsg: state.chatmsg.map(v => ({...v, read: from === v.from ? true : v.read})),
        unread: state.unread.num ? state.unread.num : 0}
    default:
      return state
  }
}

function msgRecv(msg, userid) {
  return { userid, type: MSG_RECV, payload: msg }
}

/**
 * 监听广播
 */
export function recvMsg() {
  return (dispatch, getState) => {
  // 监听广播
    socket.on('recvmsg', function(data) {
      const userid = getState().user._id
      dispatch(msgRecv(data, userid))
    })
  }
}

function msgList(msgs, users, userid) {
  return { type: MSG_LIST, payload: {msgs, users, userid} }
}

/**
 * 获取消息列表
 */
export function getMsgList() {
  return (dispatch, getState) => {
    axios.get('/user/getmsglist').then(res => {
      console.log(res)
      if (res.status === 200 && res.data.code === 0) {
        const userid = getState().user._id
        dispatch(msgList(res.data.msgs, res.data.users, userid))
      }
    })
  }
}

/**
 * 发送消息
 * @param {*} from 发送者, to 接收者
 * 
 */
export function sendMsg({ from, to, msg }) {
  return dispatch => {
    socket.emit('sendmsg', { from, to, msg })
  }
}

function msgRead({ from, userid, num }) {
  return { type: MSG_READ, payload: { from, userid, num } }
}

export function readMsg(from) {
  return (dispatch, getState) => {
    axios.post('/user/readmsg', {from}).then(res => {
      const userid = getState().user._id
      if (res.status === 200 && res.data.code === 0) {
        const num = res.data.num
        dispatch(msgRead({ userid, from, num }))
      }
    })
  }
}

2.chat.js

import { Grid, Icon, InputItem, List, NavBar } from 'antd-mobile';
import React, { Component } from 'react';
import { connect } from 'react-redux'
import { getMsgList, sendMsg, recvMsg, readMsg } from '../../redux/chat.redux'
import { getChatId } from '../../util'

@connect(
  state => state,
  { getMsgList, sendMsg, recvMsg, readMsg }
)
class Chat extends Component {
  constructor(props) {
    super(props);
    this.state = {
      text: '',
      msg: []
    }
  }
  componentDidMount() {
  // 获取消息
    if (!this.props.chat.chatmsg.length) {
      this.props.getMsgList()
      this.props.recvMsg()
    }
  }
  // 将消息标为已读
  componentWillUnmount() {
    const to = this.props.match.params.user
    this.props.readMsg(to)
  }
  fixCarousel() {
    setTimeout(function () {
      window.dispatchEvent(new Event('resize'))
    }, 0)
  }
  handleSubmit() {
    // 发送者
    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: '',
      showEmoji: false
    })
  }
  render() {
    const emoji = '😀 😁 😂 😍 😘 😝 😬 🤮 🤟 🤏 👌 🤜 🙏'
      .split(' ')
      .filter(v => v)
      .map(v => ({ text: v }))

    const userid = this.props.match.params.user
    const Item = List.Item
    const users = this.props.chat.users
    if (!users[userid]) {
      return null
    }
    const chatid = getChatId(userid, this.props.user._id)
    const chatmsgs = this.props.chat.chatmsg.filter(v => v.chatid === chatid)
    return (
      <div id="chat-page">
        <div style={{ position: "fixed", zIndex: 999, width: '100%' }}>
          <NavBar
            mode="dark"
            icon={<Icon type="left"></Icon>}
            onLeftClick={() => {
              this.props.history.goBack()
            }}
          >
            {users[userid].name}
          </NavBar>
        </div>
        {/* 聊天列表 */}
        {chatmsgs.map(v => {
          const avatar = require(`../img/${users[v.from].avatar}.png`).default
          return v.from === userid ? (
            <List key={v._id}>
              <Item
                thumb={avatar}
                multipleLine={true}
                wrap={true}
                platform="android"
              >{v.content}</Item>
            </List>
          ) : (
              <List key={v._id}>
                <Item
                  multipleLine={true}
                  wrap={true}
                  platform="android"
                  extra={<img src={avatar} alt="" />
                  }
                  className="chat-me"
                >{v.content}</Item>
              </List>
            )
        })}
        {/* 聊天列表 */}
        <div className="stick-fooetr">
          <List>
            <InputItem
              placeholder='请输入'
              value={this.state.text}
              onChange={v => {
                this.setState({ text: v })
              }}
              extra={
                <div>
                  <span
                    style={{ marginRight: 15 }}
                    onClick={() => {
                      this.setState({
                        showEmoji: !this.state.showEmoji
                      })
                      this.fixCarousel()
                    }}
                  >😀</span>
                  <span onClick={() => this.handleSubmit()}>发送</span>
                </div>
              }
            ></InputItem>
            {this.state.showEmoji ? <Grid
              data={emoji}
              columnNum={9}
              carouselMaxRow={2}
              isCarousel={true}
              onClick={el => {
                this.setState({
                  text: this.state.text + el.text
                })
                console.log(el)
              }}
            /> : null}
          </List>
        </div>
      </div>
    )
  }
}

export default Chat

在这里插入图片描述


具体代码可以访问笔者的 gitee
也可以关注笔者的公众号:大明贵妇,(回复:socket.io)获取学习资料

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值