demo演示地址:chat.lstmxx.cn
github地址:github.com/Lstmxx/chat…
1. 前言
服务端推送是一种服务器主动给客户端发送的技术,主要用于实时对客户端进行消息推送,如天气预报、聊天功能等。
1.1 HTTP 1.x
在websocket api出现之前,由于http1.x的缺陷,导致通信只能由客户端发起,用户想要获取到实时数据变化,就要不停的向服务器发送请求,这种方法我们一般称为轮询。这种方法在web端可以一用,但是在移动端就不行了,想一想你的app不停的消耗你的流量发请求到服务器,这会导致用户流量的大量浪费,体现极其差。
setInterval(() => {
axios()then((res) => {
···
}).catch(err => {
···
})
}, 3000)
1.2 HTTP 2.0
为了解决这一问题,终于在http2.0协议里面增加了一个新特性——服务器推送。而Html5根据这一特性提供了一种在单个TCP连接上进行全双工通讯的协议——WebSocket。
1.3 Socketio
1.3.1 描述
如果客户端想要使用websocket接受服务器推送的话,Socketio是一个不错的选择。Socket.io将Websocket、轮询机制以及其它的实时通信方式(ajax等)封装成了通用的接口,并且在服务端也实现了这些实时机制的相应代码。所以,使用Socket.io便不需要担心浏览器兼容问题。
1.3.2 namespace和room
socketio有两个重要的概念——namespace和room。两者关系是namespace包含room。举个例子,你要通知北小区的4座的所有用户交管理费,你先找到了北小区(namespace)然后再找到4座(room),最后给4座里面的业主发送交管理费消息。
2. Socketio的安装与使用
2.1 Vue中使用Socketio
在Vue中有两种方式使用Socketio
2.1.1 直接使用官方包
下载
npm install socket.io
引入
import io from 'socket.io-client'
使用
// 这里的namespace和后端设置的namespace是一样的
const socket = io.connect(`http://${域名}/${namespace}`)
// on函数是监听函数,接受两个参数,第一个是订阅名,第二个是接受订阅信息的回调
socket.on('chatMessage', res => {
console.log(res)
})
socket.on('response', res => {
console.log(res)
})
socket.on('connect', res => {
console.log(res)
})
···
// emit是发送函数,第一个参数是后端的订阅名,第二个是数据,可以是任意类型
socket.emit('user_input', 'wdnmd')
2.1.2 使用VueSocketio
相较于socket.io-client,VueSocketio自带支持在vuex中使用,这使得多组件共用消息更加便利。npm地址:www.npmjs.com/package/vue… 。
下载
npm install vue-socket.io
引入
// /fronted/src/main.js
import store from './store'
import VueSocketio from 'vue-socket.io'
···
Vue.use(new VueSocketio({
debug: true,
connection: `/${namespace}`,
/* 推荐使用vuex引入,方便多组件状态共享 */
vuex: {
store,
actionPrefix: 'SOCKET_' // 前缀,为了区分vuex文件中响应函数和普通函数
}
}))
单组件使用
// 在需要监听的vue引入
···
export default {
sockets: {
connect: function () {
console.log('socket connected')
},
received: function (res) {
console.log(res)
}
}
}
···
vuex中使用
// /store/module/room.js
···
// responseData 为响应数据
SOCKET_received ({ state, rootState, commit }, responseData) {
// do something
},
SOCKET_join_one ({}, responseData) {
// do something
}
2.2 flask中使用Socketio
flask中使用socketio主要用到Flask-SocketIO这个包,官网地址:flask-socketio.readthedocs.io/en/latest/ 。
下载
pip install flask-socketio
使用
···
# /backend/blueprint/socketio.py
from flask_cors import CORS # 跨域
from flask_socketio import SocketIO, emit, join_room, leave_room, close_room, rooms, disconnect
# 初始化socketio
socketio = SocketIO(app, cors_allowed_origins="*")
# 第一个参数为事件名,第二个为namespace
# 通过监听namespace下的事件做出响应,这里的namespace和前面前端定义的namespace要相同
# message为请求参数
@socketio.on('test_input', namespace='/chatroom')
def test_input(message):
# do someting
socketio.emit('test_received', '收到啦', namespace='/chatroom')
在app.py中引入
# /backend/app.py
from blueprint.socketio import app, socketio, db
···
if __name__ == "__main__":
···
socketio.run(app, host="0.0.0.0", port=4999, debug=True)
2.3 nginx配置
既然是前后端分离,那当然要使用nginx啦~
配置chatroom.conf
upstream chat_frontend {
server 127.0.0.1:8181; # 前端工程运行的地址
}
upstream chat_backend {
server 127.0.0.1:4999; # 后端工程运行的地址
}
server {
listen 80; # 监听端口
server_name www.chatroom.com; #域名
location ^~ /api { # 普通接口路由
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Server $host;
proxy_pass http://chat_backend;
}
location /socket.io { # socketio的路由
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://chat_backend;
}
location / { # 前端路由
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Server $host;
proxy_pass http://chat_frontend;
}
}
配置host
···
127.0.0.1 www.chatroom.com
2.4 通信
前面把flask和vue都配置好了,那么先测试一下。
整个流程非常简单,流程图如下:
2.4.1 vue
获取用户输入后,向目标事件发送数据。这里我自己实现了一个简陋的rich-text,如果不追求效果直接用input标签就完事了。
// /src/components/chat-room/message-box/message-box.vue
···
sendMessage (message) {
// 第一个参数为事件名,第二个参数为要发送的数据
this.$socket.emit('test_input', message)
}
在vuex中监听received事件获取服务器返回消息。
// /src/store/module/room.js
export default {
···
actions: {
···
SOCKET_test_received ({ state, rootState, commit }, responseData) {
console.log(responseData)
}
}
}
2.4.2 flask
后端这边就非常简单了,增加一个消息回调函数就好了。
from flask_socketio import SocketIO, emit
socketio = SocketIO(app, cors_allowed_origins="*")
···
@socketio.on('test_input', namespace='/chatroom')
def test_input(message):
# do someting
socketio.emit('test_received', '收到啦', namespace='/chatroom')
# 或者
# emit('test_received', '收到啦', namespace='/chatroom')
要注意的是,这个emit没有指定某一个room,所以会广播给在这个namespace下的所有人。
打开谷歌浏览器,效果如下:
3. 实现聊天室小demo
3.1 构思
一个简单的聊天室肯定会涉及到用户,房间和消息记录。
3.2 实现登录页面
首先解决一下用户,最核心的是登录。先建一个用户表。
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`create_time` datetime(0) DEFAULT NULL,
`avatar_image` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`room_id_set` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '每个用户所参加的房间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `ix_user_username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
封装一下登录接口,使用vuex保存登录状态。因为关闭页面后vuex会清掉token使用cookie来保存(axios的封装就不说了,不是重点)
// /fronted/src/libs/requestApi.js
···
export function baseLogin (config) {
const request = {
url: config.url,
method: 'POST',
data: config.data
}
return service.request(request)
}
// /fronted/src/libs/request.js
export function login (config) {
return new Promise((resolve, reject) => {
baseLogin(config).then((response) => {
resolve(response.data.data)
}).catch((err) => {
reject(err)
})
})
}
保存token
// /fronted/src/libs/utility/token.js
import Cookies from 'js-cookie'
const TOKEN_KEY = 'token'
export const setToken = (token) => {
Cookies.set(TOKEN_KEY, token, { expires: 1 })
}
export const getToken = () => {
const token = Cookies.get(TOKEN_KEY)
if (token !== 'null') return token
else return false
}
编写vuex的user模块
// /fronted/src/store/module/user.js
import { getToken, setToken } from '../../libs/utility/token'
import { login, getUserInfo, logout } from '@/libs/request'
export default {
state: {
token: getToken(),
userName: null,
userId: null,
avatarImage: null
},
getters: {
getToken (state) {
return state.token
},
getUserName (state) {
return state.userName
},
getUserId (state) {
return state.userId
},
getAvatarImage (state) {
return state.avatarImage
}
},
mutations: {
setToken (state, token) {
state.token = token
setToken(token)
},
setUserName (state, name) {
state.userName = name
},
setUserId (state, userId) {
state.userId = userId
},
setAvatarImage (state, avatarImage) {
state.avatarImage = avatarImage
}
},
actions: {
handleLogin ({ commit }, config) {
return new Promise((resolve, reject) => {
login(config).then((responseData) => {
commit('setToken', responseData.token)
resolve(responseData)
}).catch((err) => {
reject(err)
console.log(err)
})
})
},
loadUserInfo ({ commit }) {
return new Promise((resolve, reject) => {
getUserInfo().then((responseData) => {
commit('setToken', getToken())
commit('setUserName', responseData.userInfo.name)
commit('setUserId', responseData.userInfo.userId)
commit('setAvatarImage', responseData.userInfo.avatar_image)
resolve(responseData)
}).catch((err) => {
commit('setToken', null)
reject(err)
console.log(err)
})
})
}
}
}
在login页面中使用
// /fronted/src/views/login/login.vue
import { mapActions } from 'vuex'
export default {
···
methods: {
...mapActions([
'handleLogin',
'loadUserInfo'
]),
checkCapslock (e) {
const { key } = e
this.capsTooltip = key && key.length === 1 && (key >= 'A' && key <= 'Z')
},
// 登录,成功后跳转
onLogin () {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.$Loading.show()
const config = {
url: '/login',
data: this.loginForm
}
this.handleLogin(config).then(() => {
this.$Loading.hide()
this.$router.push({
name: 'ChatRoom'
})
}).catch((err) => {
this.$Loading.hide()
console.log(err)
})
}
})
},
// 注册,成功后回调登录
onRegister () {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.$Loading.show()
const config = {
url: '/register',
data: this.loginForm
}
this.handleLogin(config).then(() => {
this.$Loading.hide()
this.onLogin()
}).catch((err) => {
this.$Loading.hide()
console.log(err)
})
}
})
}
}
}
后端方面,可以看看/backend/blueprint/user.py。ui方面就不说了,不是重点。
3.3 实现房间的创建,展示和加入功能
对于房间来说,肯定要有创建和加入这两个功能的,下面先说说创建。
先建个表吧
DROP TABLE IF EXISTS `room`;
CREATE TABLE `room` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`owner` int(11) DEFAULT NULL COMMENT '房间创建人',
`user_set` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
`description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
`create_time` datetime(0) DEFAULT NULL,
`update_time` datetime(0) DEFAULT NULL,
`avatar_image` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`room_hash_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `ix_room_avatar_image`(`avatar_image`) USING BTREE,
INDEX `ix_room_create_time`(`create_time`) USING BTREE,
INDEX `ix_room_name`(`name`) USING BTREE,
INDEX `ix_room_owner`(`owner`) USING BTREE,
INDEX `ix_room_update_time`(`update_time`) USING BTREE,
CONSTRAINT `room_ibfk_1` FOREIGN KEY (`owner`) REFERENCES `user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
3.3.1 创建房间
首先先明确创建房间需要什么数据,我的想法是需要房间头像,房间名和房间描述。
前端主要是获取了房间头像、房间名和房间描述后发送请求到后端。这里的upLoadFile是自己模仿element来写的组件,有兴趣可以在 /fronted/src/components/base/up-load-file/up-load-file.vue 查看
// /fronted/src/components/chat-room/room-list/room-list.vue
<template>
···
<el-dialog title="创建房间" :visible.sync="createRoomDialog">
<el-form :model="createRoom" :rules="createRules" ref="createRoomForm">
<el-form-item label="房间名" prop="hashId">
<el-input v-model="createRoom.name" autocomplete="off" :maxlength='32' :minlength='32'></el-input>
</el-form-item>
<el-form-item label="房间描述" prop="description">
<el-input v-model="createRoom.description" autocomplete="off" :maxlength='32' :minlength='32'></el-input>
</el-form-item>
<el-form-item label="房间头像" prop="avatarImage">
<upLoadFile :maxImageNum="1" @on-change="getFilePath"/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="createRoomDialog = false">取 消</el-button>
<el-button type="primary" @click="handleCreateRoom">确 定</el-button>
</div>
</el-dialog>
···
</template>
<script>
import { post } from '@/libs/request'
import upLoadFile from '@/components/base/up-load-file'
export default {
name: 'RoomList',
props: {
roomList: {
default: () => [],
type: Array
}
},
components: {
···
upLoadFile
},
data () {
return {
···
createRoom: {
name: '',
description: '',
avatarImage: ''
}
}
},
method: {
getFilePath (imageList) {
this.createRoom.avatarImage = imageList[0].base64Path
},
handleCreateRoom () {
this.$refs.createRoomForm.validate(valid => {
if (valid) {
this.$Loading.show()
const config = {
url: '/room/create',
data: this.createRoom
}
post(config).then((responseData) => {
this.$Loading.hide()
this.createRoom.name = ''
this.createRoomDialog = false
this.createRoom = {
name: '',
description: '',
avatarImage: ''
}
this.$message({
message: '创建成功',
type: 'success'
})
this.$emit('create-room-success', responseData.room)
}).catch((err) => {
this.$Loading.hide()
this.createRoomDialog = false
console.log(err)
})
}
})
}
}
}
</script>
后端这边就简单了,直接插入数据库。插入时候使用base64来生成房间码,之后加入房间要用。
# /backend/blueprint/room.py
···
@room_bp.route('/api/room/create', methods=['POST'])
@verify_token
def room_create(tokenData):
values = request.get_json()
user = User.query.filter_by(id=tokenData['userId']).first()
if user:
room = Room(name=values['name'],
description=values['description'],
user_set=str(tokenData['userId']),
owner=user.id,
avatar_image='')
db.session.add(room)
db.session.flush()
room.room_hash_id = hashlib.md5(f'{room.id}{time.time()}'.encode('utf-8')).hexdigest()
room.user_set = f'{room.user_set},{user.id}' if room.user_set else user.id
if values['avatarImage']:
avatartImageList = values['avatarImage'].split(',')
suffix = avatartImageList[0].split('/')[1].split(';')[0]
filename = f'room_avatar/{room.room_hash_id}.{suffix}'
print(filename)
with open(f'media/{filename}', 'wb') as f:
f.write(base64.b64decode(avatartImageList[1]))
room.avatar_image = filename
user.room_id_set = f'{user.room_id_set},{room.id}' if user.room_id_set else room.id
db.session.commit()
return jsonify({
'data': {
'room': JSONHelper.model_to_json(room)
},
'message': '成功',
'status': 200
})
return jsonify({
'data': '',
'message': '失败失败',
'status': 500
})
3.3.2 展示房间
这个其实就是拉一个房间列表。要注意的是前端获取到房间列表后,要调用join_all这个事件监听这些房间的消息。
后端
@room_bp.route('/api/room/list', methods=['GET'])
@verify_token
def room_list(tokenData):
user = User.query.filter_by(id=tokenData['userId']).first()
if user:
roomlist = Room.query.filter(Room.id.in_(user.room_id_set.split(','))).all() if user.room_id_set else []
return jsonify({
'data': {
'roomList': JSONHelper.to_json_list(roomlist)
},
'message': '成功',
'status': 200
})
return jsonify({
'data': '',
'message': '失败失败',
'status': 500
})
前端这边先在room模块里编写加载房间列表函数。
// /fronted/src/store/module/room.js
···
loadRoomList ({ commit }) {
return new Promise((resolve, reject) => {
const config = {
url: '/room/list'
}
get(config).then((responseData) => {
commit('setRoomList', responseData.roomList)
resolve(responseData.roomList)
}).catch((err) => {
reject(err)
})
})
}
在chat-room页面调用。
// /fronted/src/views/chat-room/chat-room.vue
mounted () {
this.loadRoomList().then((roomList) => {
const request = {
roomList: roomList.map(room => room.id),
userId: this.userId
}
this.$socket.emit('join_all', request)
}).catch((err) => {
console.log(err)
})
}
后端响应join_all事件,调用join_room加入用户所在的所有房间。
# /backend/blueprint/socketio.py
@socketio.on('join_all', namespace='/chatroom')
def join_chats(message):
"""加入多个聊天室
"""
user = User.query.filter_by(id=message['userId']).first()
if user and len(message['roomList']) > 0:
for roomId in message['roomList']:
join_room(roomId)
emit('received', { # 发送加入消息
'user': {
'id': user.id,
'name': user.username,
'avatarImage': user.avatar_image,
},
'roomId': roomId,
'type': 'join'
}, namespace='/chatroom', room=roomId)
3.3.3 加入房间
获取对应的房间码后,输入加入就OK了。
// /fronted/src/components/chat-room/room-list/room-list.vue
<template>
···
<el-dialog title="加入房间" :visible.sync="joinRoomDialog">
<el-form :model="joinRoom" :rules="joinRules" ref="joinRoomForm">
<el-form-item label="房间号" prop="hashId">
<el-input v-model="joinRoom.hashId" autocomplete="off" :maxlength='32' :minlength='32'></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="joinRoomDialog = false">取 消</el-button>
<el-button type="primary" @click="handleJoinRoom">确 定</el-button>
</div>
</el-dialog>
···
</template>
<script>
import { post } from '@/libs/request'
export default {
name: 'RoomList',
props: {
roomList: {
default: () => [],
type: Array
}
},
data () {
return {
···
joinRoom: {
hashId: ''
}
}
},
method: {
handleJoinRoom () {
this.$refs.joinRoomForm.validate(valid => {
if (valid) {
this.$Loading.show()
const config = {
url: '/room/join',
data: {
roomIdHash: this.joinRoom.hashId
}
}
post(config).then((responseData) => {
this.$Loading.hide()
this.joinRoomDialog = false
this.$message({
message: '加入成功',
type: 'success'
})
this.$emit('create-room-success', responseData.room)
}).catch((err) => {
this.$Loading.hide()
console.log(err)
})
}
})
}
}
}
</script>
加入成功后,和创建一样,调用join_one_chat事件来加入房间。
// /fronted/src/views/chat-room/chat-room.vue
handleCreateJoinRoom (room) {
const roomList = this.roomList
roomList.push(room)
this.$store.commit('setRoomList', roomList)
const request = {
roomId: room.id,
userId: this.userId
}
this.$socket.emit('join_one_chat', request)
}
后端响应回调。
# /backend/blueprint/socketio.py
@socketio.on('join_one_chat', namespace='/chatroom')
def join_one_chat(join):
"""加入聊天室
"""
room = Room.query.filter_by(id=join['roomId']).first()
user = User.query.filter_by(id=join['userId']).first()
print(join)
if room and user:
join_room(room.id)
emit('received', {
'user': {
'id': user.id,
'name': user.username,
'avatarImage': user.avatar_image,
},
'roomId': room.id,
'type': 'join'
}, namespace='/chatroom', room=room)
3.4 消息记录的发送与保存
先建个表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-----------------------------
-Table structure for room_record
-----------------------------
DROP TABLE IF EXISTS `room_record`;
CREATE TABLE `room_record` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
`create_time` datetime(0) DEFAULT NULL,
`room_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `ix_room_record_create_time`(`create_time`) USING BTREE,
INDEX `ix_room_record_room_id`(`room_id`) USING BTREE,
CONSTRAINT `room_record_ibfk_1` FOREIGN KEY (`room_id`) REFERENCES `room` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
用户选择了对应房间,在对应房间中发送消息就OK了。
// /fronted/src/components/chat-room/message-box/message-box.vue
···
import { mapGetters, mapActions } from 'vuex'
import util from '@/libs/utility/util.js'
import UserMessage from '../user-message/index'
import JoinMessage from '../join-message/index'
import RichText from '@/components/base/rich-text/index'
export default {
name: 'MessageBox',
components: {
UserMessage,
JoinMessage,
RichText
},
computed: {
...mapGetters({
selectedRoom: 'getSelectedRoom',
userId: 'getUserId',
userName: 'getUserName',
messageList: 'getMessageList',
isUpdate: 'getUpdate',
avatarImage: 'getAvatarImage'
})
},
watch: {
selectedRoom () {
this.setMessageContentScroll()
},
isUpdate () {
if (this.isUpdate) {
this.$forceUpdate()
this.updateComplete()
this.setMessageContentScroll()
}
}
},
methods: {
...mapActions([
'updateComplete',
'userInput'
]),
setMessageContentScroll () {
this.$nextTick(() => {
const messageContent = document.getElementById('messageContent')
if (messageContent) {
if (messageContent.scrollHeight > messageContent.clientHeight) {
messageContent.scrollTop = messageContent.scrollHeight
}
}
})
},
sendMessage (message) {
const messageId = Number(new Date())
const messageContext = {
user: {
id: this.userId,
name: this.userName,
avatarImage: this.avatarImage
},
roomId: this.selectedRoom.id,
id: messageId,
message,
loading: true,
type: 'input'
}
const request = {
userId: this.userId,
roomId: this.selectedRoom.id,
id: messageId,
message,
type: 'input'
}
this.userInput(messageContext)
this.$socket.emit('user_send_message', request)
}
}
}
后端。接收到请求后,完成插入数据库处理并通过received事件返回给前端
# /backend/blueprint/socketio.py
@socketio.on('user_send_message', namespace='/chatroom')
def user_input(message):
"""获取用户输入
"""
userId = message['userId']
user = User.query.filter_by(id=message['userId']).first()
if user:
response = {
'user': {
'id': user.id,
'name': user.username,
'avatarImage': user.avatar_image,
},
'message': message['message'],
'roomId': message['roomId'],
'id': message['id'],
'type': message['type'],
'time': datetime.utcnow().isoformat(),
}
roomRecord = RoomRecord(content=message['message'], user_id=user.id, room_id=message['roomId'])
db.session.add(roomRecord)
db.session.commit()
socketio.emit('received', response,
namespace='/chatroom',
room=message['roomId'])
前端vuex的room模块接收
// /fronted/src/store/module/room.js
export default {
action: {
updateComplete ({ commit }) {
commit('setUpdate', false)
},
SOCKET_received ({ state, rootState, commit }, responseData) {
const messageList = state.messageList
const user = rootState.user
responseData.time = normalizeTimeDetail(responseData.time)
if (user.userId === responseData.user.id && responseData.type !== 'join') {
for (let i = messageList[responseData.roomId].length 1; i > 0; i--) {
if (messageList[responseData.roomId][i].user.id === user.userId && responseData.id === messageList[responseData.roomId][i].id) {
messageList[responseData.roomId][i].loading = false
messageList[responseData.roomId][i].time = responseData.time
break
}
}
if (!state.update) {
commit('setUpdate', true)
}
} else {
if (!messageList[responseData.roomId]) {
messageList[responseData.roomId] = []
}
messageList[responseData.roomId].push(responseData)
if (!state.update) {
commit('setUpdate', state.selectedRoom ? responseData.roomId === state.selectedRoom.id : false)
}
}
commit('setMessageList', messageList)
}
}
}
4.总结
说到这里其实也说完了重点的地方了,有兴趣可以看看源码。第一次写文章,有不足的地方请大佬们多多指点。