最近作了聊天,功能點大概以下:
- 羣聊
- @全員
- 羣主禁言
- 聊天表情😊
技術選擇
- 基於
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