前后端分离、React 集成 Socket 实现消息接收

1,771 阅读4分钟

回顾

React 构建聊天界面

SpringBoot 聊天消息持久化

SpringBoot 集成 netty-socketio

Websocket

WebSocket 协议在 2008 年诞生,2011 年成为国际标准。所有浏览器都已经支持了。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

实现效果

思路

  • 使用 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 数据中获取
  • 未读消息
  • ...

关注公众号全栈技术部,不断学习更多有趣的技术知识。