node.js 提供了高效的服务端运行环境,但是由于浏览器端对HTML5的支持不一,为了兼容所有浏览器,提供卓越的实时的用户体验,并且为程序员提供客户端与服务端一致的编程体验,于是socket.io诞生。Socket.io将Websocket和轮询 (Polling)机制以及其它的实时通信方式封装成了通用的接口,并且在服务端实现了这些实时机制的相应代码。
Socket.IO 实现了实时双向的基于事件的通讯机制。旨在让各种浏览器与移动设备上实现实时app功能,模糊化各种传输机制。Socket.IO 是跨平台,多种连接方式自动切换,做即时通讯方面的开发很方便,而且能和 expressjs 提供的传统请求方式很好的结合。
代码运行环境
前端 | 后端 | 数据库 |
---|---|---|
React | Express | MongoDB |
主要依赖版本
"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)获取学习资料