前言:最近在学习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中:实现信息输入且广播到全局
- 点击【发送】时通过socket.emit触发sendmsg事件:传递state中存储的输入信息,同时将state中信息清空
handleSubmit(){
// console.log(this.state.text)
socket.emit('sendmsg',{text: this.state.text}) //触发事件
this.setState({text: ''})
} - 在componetDidMount中通过socket.on监听recvmsg事件:接收后端传来的全局信息,扩展到当前state中存储的msg数组中
componentDidMount(){
socket.on('recvmsg', (data) => {
// console.log(data)
this.setState({
msg: [...this.state.msg, data.text]
})
})
} -
完整代码 ↓
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中:获取并发送全局信息
通过socket.on监听sendmsg事件,获取前端传来的信息
-
再通过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方法
通过axios.get调用接口,获取聊天信息列表数据
-
通过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: ''})
}
-
chat.redux.js中:定义sendMsg方法调用socket.emit触发sendmsg事件,发送数据
export function sendMsg({from, to, msg}){
return dispatch=>{
socket.emit('sendmsg', {from, to, msg})
}
} -
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,作为聊天数据的标识
重新创建Chat数据库:将数据存入mongodb数据库
-
同时通过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以及操作数据的方法
通过socket.on监听recvmsg事件,获取后端传来的Chat数据
-
调用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))
})
}
} -
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中:修改获取聊天信息列表的方法
- 查找到所有用户:将每个用户的信息以id:{name, avatar}的形式,创建user实例
- 获取到当前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}/>}
七、修正未读消息数量
- 消息详情页中:【只显示与当前选择用户的信息】
-
util.js中:定义工具函数getChatId,将当前用户userid、选择用户targetid连接在一起
export function getChatId(userId, targetId){
return [userId, targetId].sort().join('_')
} -
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时,计算为一条未读的消息
知识点:redux中使用其它地方的数据,通过getState获取全部的状态
- 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))
}
})
}
} - 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 = '