回顾
Websocket
WebSocket 协议在 2008 年诞生,2011 年成为国际标准。所有浏览器都已经支持了。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
- 参考:WebSocket 教程 www.ruanyifeng.com/blog/2017/0…
实现效果
思路
- 使用 socket 连接服务器
- 客户端发送消息给服务器
- 客户端监听 socket 事件
- 服务器把消息推送给客户端
聊天列表回显
service 定义
- 调用 /api/v1/message/record/list
export async function getMessageRecordList(params) {
return request("/api/v1/message/record/list", {
method: "POST",
requestType: "form",
data: params,
});
}
chat model 定义
effects: {
*getMessageRecordList({ payload }, { call, put }) {
const response = yield call(getMessageRecordList);
yield put({
type: 'updateMessageRecordList',
payload: {
messageRecordList: response.data,
},
});
},
建立连接
- 使用
socket.io-client模块进行 socket 连接
安装模块
npm install socket.io-client@1.7.4 --save
"socket.io-client": "^1.7.4"
TODO: 使用 1.7.4 版本, 2.x 版本可能存在问题
在 ChatLayout 中使用
封装方法
// 连接成功后,获取当前登录用户的聊天列表
const getMessageRecordList = () => {
dispatch({ type: "chat/getMessageRecordList" });
};
const connectImServer = () => {
try {
// 没有 token,调整登录界面
let accessToken =
localStorage.getItem(MOOSE_REACT_LEARN_ACCESS_TOKEN) || "";
if (!accessToken) {
redirectLogin();
return;
}
const socket = require("socket.io-client")(`http://localhost:9000`, {
transports: ["websocket"],
query: { userId, access_token: accessToken },
});
// 建立连接
socket.on("connect", () => {
console.log("connect:: ");
// 连接成功,调用回去聊天列表
getMessageRecordList();
});
// 发送错误
socket.on("error", (error) => {
console.log("error:: ", error);
});
// 连接错误
socket.on("connect_error", (error) => {
console.log("connect_error:: ", error);
});
// 断开连接
socket.on("disconnect", (reason) => {
console.log("disconnect:: ", reason);
});
// 监听接收服务端推送消息
socket.on("SINGLE_CHAT", (message) => {
console.log("客户端接收到消息: ", message);
// 触发 dva 数据保存
dispatch({ type: "chat/receiverMessage", payload: { message } });
// 滚动至底部
scrollToBottom("bottomElement", "chatItems");
});
// 保存连接 socket 信息
dispatch({ type: "chat/saveServerInfo", payload: { socket } });
} catch (error) {
console.log(error);
}
};
调用
useEffect(() => {
connectImServer();
return () => {};
}, []);
使用 dva 进行连接状态保存
models/chat.js
saveServerInfo(state, { payload: { socket } }) {
return { ...state, socket: socket };
},
/**
* 更新聊天记录列表
*/
updateMessageRecordList(state, { payload: { messageRecordList } }) {
return { ...state, messageRecordList };
},
/**
* 更新当前聊天列表
*/
updateMessageChatList(state, { payload: { messageChatList } }) {
return { ...state, messageChatList };
},
点击选择聊天对象
// 点击切换当前聊天对象
const onChangeChatCurrentUser = (item) => {
// 根据当前 sendId, receiveId, 判断当前聊天对象
let chatId = getDiffChatId(item, userId);
// 切换当前聊天对象
dispatch({ type: "chat/chatCurrentUser", payload: { chatUserInfo: item } });
// 获取当前聊天对象聊天记录
dispatch({ type: "chat/getMessageChatList", payload: { chatId } });
};
获取聊天详情
service 定义
export async function getMessageChatList(params) {
return request("/api/v1/message/chat/list", {
method: "POST",
requestType: "form",
data: params,
});
}
chat model 定义
/**
* 获取聊天请求
*/
*getMessageChatList({ payload }, { call, put }) {
const { chatId } = payload;
const response = yield call(getMessageChatList, { chatId });
console.log(response);
if (response.code === 200) {
yield put({
type: 'updateMessageChatList',
payload: {
messageChatList: response.data,
},
});
}
},
区分选择的聊天对象、发送者、接收者用户 Id
/**
* 区分聊天对象和当前登录用户对比
* @param {*} chatItem 聊天记录
* @param {*} userId 当前登录用户 Id
* @returns
*/
export const getDiffChatId = (chatItem, userId) => {
const { sendId, receiveId } = chatItem;
if (userId === sendId) {
return receiveId;
}
if (userId === receiveId) {
return sendId;
}
return "";
};
发送消息
- 点击发送按钮发送
- 拼装消息模板
- socket 发送
点击按钮逻辑
const onSendMessage = () => {
if (!receiveId) {
message.error("请选择聊天对象");
return;
}
if (!content) {
return;
}
let messageTemplate = {
type: "MS:TEXT",
chatType: "CT:SINGLE",
content,
sendId: userId,
receiveId: receiveId,
};
// chat model 发送消息
dispatch({ type: "chat/sendMessage", payload: { messageTemplate } });
// 消息滚动至底部
onMessageScroll();
};
chat model effects sendMessage
*sendMessage({ payload }, { call, put, select }) {
const { messageTemplate } = payload;
// dva select API 根据不同 命名空间获取对应状态
const chatState = yield select((state) => state['chat']);
// 从状态获取 socket 对象 --> 建立连接时保存过 socket 对象
const { messageChatList = [], socket } = chatState;
// 拼装消息
const temp = messageChatList.concat();
temp.push(messageTemplate);
// 刷新聊天列表
yield put({ type: 'refreshChatList', payload: { messageChatList: temp } });
// 情况输入框
yield put({ type: 'chatInputMessageChange', payload: { content: null } });
if (!socket) console.warn('socket 不存在,需要重新登录,请检查 Socket 连接。');
// 发送消息 (这个事件和服务端预先约定好)
if (socket) socket.emit('SINGLE_CHAT', messageTemplate);
console.log(chatState);
},
接收消息
- 在选择聊天对象后,监听聊天消息
- 解析消息,放入 messageChatList 刷新状态数据
chat model effects receiverMessage
*receiverMessage({ payload }, { call, put, select }) {
const { message } = payload;
const chatState = yield select((state) => state['chat']);
const { messageChatList = [] } = chatState;
const temp = messageChatList.concat();
// 服务端推送的消息结构为 JSON 格式,解析消息
temp.push(JSON.parse(message));
// 刷新聊天详情列表
yield put({ type: 'refreshChatList', payload: { messageChatList: temp } });
},
chat model reducers refreshChatList
// 刷新聊天列表
refreshChatList(state, { payload: { messageChatList } }) {
return { ...state, messageChatList: messageChatList };
},
服务端接收到消息,数据库存储
TODO:
- 发送、接收消息时
- 聊天记录列表没有更新
- 点击回去聊天对象的聊天记录一次只获取 10 条数据,需要滑动获取历史聊天记录数据
- 聊天列表头像还是从 Mock 数据中获取
- 未读消息
- ...
关注公众号全栈技术部,不断学习更多有趣的技术知识。