最近作了聊天,功能点大概以下:
- 群聊
- @全员
- 群主禁言
- 聊天表情😊
技术选择
- 基于
Socket.io
作技术基础
- 基于
node-redis
作聊天消息缓存
- 基于
node-schedule
定时存储Redis
中的聊天消息
服务器端
const server = require('http').createServer();
const io = require('socket.io')(server, {
path: '/socket/chat',
serveClient: false
});
server.listen(3000);
复制代码
客户端
- 若有须要代理
/socket/chat/
与path
设置一致
- 不设置
path
,就是默认的socket.io
proxy: {
"/socket/chat/*": {
"target": "ws://localhost:8000/socket/chat",
"ws": true
},
}
复制代码
const ws = io('localhost:4000', {
path: '/socket/chat',
transports: ['websocket'],
reconnection: true,
});
ws.on('connect', () => {});
ws.on('error', error => {
console.log(error);
});
ws.on('disconnect', reason => {
console.log('disconnect', reason);
});
window.addEventListener('beforeunload', () => {
ws.emit('client:disconnect');
});
复制代码
- Web端加入房间
// Web端
socket.emit('join', id);
// Server端
socket.on('join', roomId => {
socket.join(roomId, error => {
if (error) {
server.log([...errorTags, 'join'], { roomId, error });
}
socket.on('user-send', listener);
});
});
复制代码
- 进入房间后,监听Server端的消息
// Web端
socket.on('chat:room:server-send', this.handleSocket);
// Server端
socket.to(msg.roomId).emit('server-send', newMsg);
复制代码
- 聊天消息存储到Redis
const key = `CHAT:ROOM:${id}`;
await redis.ZADDAsync(key, msg.time, JSON.stringify(msg));
复制代码
- 分页获取Redis消息
// Web端
// 获取历史消息
socket.emit(
'room.msg:list',
{ roomId: id, ps, pn, isBottom: true },
this.handleHistoryMessage
);
// Server端
socket.on(
'room.msg:list',
async ({ roomId, ps = 20, pn = 1 }, fn) => {
const key = getKey('CHAT:MSG', roomId);
const data = await redis.zrevrangebyscoreAsync(
key,
'+inf',
'-inf',
'LIMIT',
(pn - 1) * ps,
ps
);
const list = data.map(it => JSON.parse(it));
fn({ list, pn, isBottom });
}
);
复制代码
- 定时存储到Mongo
- 定时存储,保持Redis中一直存储1e3条之内的数据
- 超过1e3条的数据,会定时同步到Mongo,而后在Redis中删除
- 每次同步1e3条
const start = 1e3;
const count = 1e3;
const chat2DB = async () => {
const list = await redis.keysAsync('CHAT:ROOM:*');
list.forEach(async key => {
const msgList = await redis.zrevrangebyscoreAsync(
key,
'+inf',
'-inf',
'LIMIT',
start,
count
);
if (msgList.length) {
const bulk = msgList.map(it => JSON.parse(it));
await chatModel.create(...bulk, (err) => {
if (err) {
// looger('打印错误平常,同步失败');
}
redis.ZREMAsync(key, ...msgList);
});
}
});
}
var schedule = require('node-schedule');
const rule = '0 */1 * * * *'; // 每分钟执行一次
schedule.scheduleJob(rule, chat2DB);
复制代码
- 表情未完待续...
- socket创建太多带来的单机使用问题...
- @全员,单独创建一个监听事件
// Web端
socket.on('chat:@all:server:send', () => {
message.info('有人@你');
});
// Server
socket.to(msg.roomId).emit('chat:@all:server:send', msg);
复制代码
相关方法
- 初始化历史消息或者每次接受到聊天消息时,自动滚动到底部,查看最新消息
// 滚动到底部
scrollTop = () => {
setTimeout(() => {
this.newsCon.scrollTop = 100000000;
}, 200);
};
复制代码
- 上滑加载更多历史消息
_onScrollEvent = () => {
const { id } = this.props;
let { pn } = this.state;
if (this.newsCon.scrollTop === 0) {
pn += 1;
socket.emit(
'room.msg:list',
{ roomId: id, ps, pn },
this.handleHistoryMessage
);
}
};
// React中div
<div
className="news-con"
ref={e => {
this.newsCon = e;
}}
onScrollCapture={debounce(this._onScrollEvent, 100)}
>
</div>
复制代码
相关技术学习文章
End