React 实践 - 构建聊天界面

上一篇: React 体验开箱即用

React 实践 - 构建聊天界面

实现步骤 - 目录定义

基于 antd.design-pro 脚手架

  • layouts 添加聊天界面布局
  • pages 目录添加聊天界面

文件结构

├── src
│   ├── layouts
│   │   ├── layouts
│   │   ├── ChatLayout.jsx  # 聊天布局
│   │   ├── ChatLayout.less # 聊天布局样式
│   ├── pages
│   │   ├── Im
│   │   │   ├── chat
│   │   │   │   ├── index.jsx # 聊天界面
│   │   │   │   ├── idnex.less # 聊天界面样式
│   │   │   ├── index.jsx # 聊天初始化界面

配置访问路由 config/routes.js

{
  path: '/im',
  name: 'im',
  icon: 'wechat', // 显示图标
  routes: [
    {
      path: '/im', // 访问路由地址
      component: '../layouts/ChatLayout', // 聊天界面使用聊天布局
      routes: [
        {
          path: '/im', // 初始化界面
          component: './Im',
        },
        {
          path: '/im/chat', // 具体聊天界面显示
          name: 'chat',
          component: './Im/chat', // 对应组件
        },
      ],
    },
  ],
},

添加国际化 locales/zh-CN/menu.js

  'menu.im': '即时通讯',
  'menu.chat': '聊天',

实现步骤 - Mock 数据准备

使用 dva 状态数据管理

dva 不熟悉可以查看介绍使用 React 常用开发框架

models 目录下创建 chat.js

const ChatModel = {
  namespace: "chat",

  state: {
    // 当前聊天对象
    chatUserInfo: {},
    // 输入聊天信息
    content: "",
    // 聊天详情
    messageList: [
      {
        msgId: "7751131831315698898",
        sendId: "775113183131074580",
        content: "你吃饭了吗?",
      },
      {
        msgId: "7751133655565656565",
        sendId: "801901013480247300",
        content: "黄河之水天上来。",
      },
    ],
    // 最近聊天列表
    chatRecordList: [
      {
        msgId: "775113183131069599",
        sendId: "801901013480247300",
        receiveId: "775113183131074580",
        type: "1",
        chatType: "1",
        sendTime: "2021-03-09",
        content: "黄河之水天上来。",
      },
    ],
  },

  effects: {},

  reducers: {
    // 当前和那个聊天
    chatCurrentUser(state, { payload: { chatUserInfo } }) {
      return {
        ...state,
        chatUserInfo,
      };
    },

    // 刷新聊天列表
    refreshChatList(state, { payload: { messageList } }) {
      return {
        ...state,
        messageList: messageList,
      };
    },

    // 输入消息改变时
    chatInputMessageChange(state, { payload: { message } }) {
      return {
        ...state,
        content: message,
      };
    },
  },
  subscriptions: {
    setup({ history }) {
      history.listen(({ pathname, search }) => {});
    },
  },
};
export default ChatModel;

实现步骤 - 布局

ChatLayout 代码

import { Link } from "umi";
import { connect } from "dva";

import ChatRecordItem from "@/components/ChatRecordItem";

import styles from "./ChatLayout.less";

const ChatLayout = ({ dispatch, children, chat }) => {
  const { chatUserInfo, chatRecordList } = chat;

  // 点击切换当前聊天对象
  // model/chat 中定义 `chat/chatCurrentUser`
  const onChangeChatCurrentUser = (item) => {
    dispatch({
      type: "chat/chatCurrentUser",
      payload: {
        chatUserInfo: item,
      },
    });
  };
  return (
    <div className={styles["chat-layout-container"]}>
      <div className={styles["chat-message-list"]}>
        {chatRecordList.map((item) => {
          return (
            <Link to="/im/chat" key={item.msgId}>
              <ChatRecordItem
                {...item}
                // 选中聊天对象
                selected={item.sendId === chatUserInfo.sendId}
                onClick={() => onChangeChatCurrentUser(item)}
              />
            </Link>
          );
        })}
      </div>
      <div className={styles["chat-message-content"]}>{children}</div>
    </div>
  );
};

export default connect(({ chat }) => ({
  chat,
}))(ChatLayout);

ChatLayout 代码说明

没有选择聊天对象显示
import { Card, Empty } from "antd";

export default () => (
  <Card>
    <Empty description="请选择聊天对象" />
  </Card>
);
  • antd.design-pro Layout 定义
  • 单独创建聊天布局
  • route.js 中使用
    • 配置访问路由 -> config/routes.js
聊天布局
  • 分为左右,左边显示最近聊天记录,点击最近聊天对象, 右边显示聊天对象详细信息

React 实践 - 构建聊天界面

ChatRecordItem 为封装的组件,显示每一天最近聊天记录

React 实践 - 构建聊天界面

  • initUserList 为 mock 数据,存放 mock 用户信息(用户名称、头像)

    export const initUserList = [
      {
        userId: "801901013480247300",
        userName: "李白",
        avatar:
          "https://gitee.com/shizidada/moose-resource/raw/master/blog/default-avatar.png",
      },
    ];
    
  • 后期对接 SpringBoot 集成 netty-socketio

classnames 模块可以构建使用 className 添加样式

import { Avatar, Divider } from "antd";
import { initUserList } from "@/mock/userList";

import cls from "classnames";

import styles from "./index.less";

const ChatRecordItem = ({ sendId, sendTime, content, selected, onClick }) => {
  const [userInfo] = initUserList.filter((item) => item.userId === sendId);
  if (!userInfo) return null;
  let selectedClassName = styles["chat-record-item-selected"];
  return (
    <div>
      <div
        className={cls(styles["chat-record-item"], {
          [selectedClassName]: selected,
        })}
        onClick={onClick}
      >
        <div className={styles["chat-user-avatar"]}>
          <Avatar src={userInfo.avatar} />
        </div>
        <div className={styles["chat-user-info"]}>
          <div className={styles.top}>
            <p className={styles["chat-user-name"]}>
              {userInfo.userName || ""}
            </p>
            <span className={styles.time}>{sendTime}</span>
          </div>
          <div className={styles["chat-message-detail-item"]}>
            <p className={styles["chat-message-detail"]}> {content}</p>
          </div>
        </div>
      </div>
      <Divider style={{ margin: 0 }} />
    </div>
  );
};

export default ChatRecordItem;

聊天

选择聊天对象显示

  • Im -> chat -> index.jsx

    代码地址
    https://gitee.com/shizidada/moose-react-learn/blob/master/src/pages/Im/chat/index.jsx

详情显示

React 实践 - 构建聊天界面

{
  messageList.map((item, index) => {
    return item.sendId !== userId ? (
      <div className={styles["chat-item"]} key={item.msgId || index}>
        <div className={styles["chat-receiver"]}>
          {/* receiver */}
          <div className={styles["avatar-wrap"]}>
            <div className={styles.avatar}>
              <Avatar
                size="large"
                style={{ backgroundColor: "#005EFF", verticalAlign: "middle" }}
              >
                {toUserName}
              </Avatar>
            </div>
          </div>
          <div className={styles.content}>{item.content}</div>
        </div>
      </div>
    ) : (
      <div className={styles["chat-item"]} key={item.messageId || index}>
        <div className={styles["chat-sender"]}>
          {/* sender */}
          <div className={styles.content}>{item.content}</div>
          <div className={styles["avatar-wrap"]}>
            <div className={styles.avatar}>
              <Avatar
                size="large"
                style={{ backgroundColor: "#005EFF", verticalAlign: "middle" }}
              >
                {userName}
              </Avatar>
            </div>
          </div>
        </div>
      </div>
    );
  });
}

可以对显示发送的消息和接收的消息进行组件化封装

聊天操作

React 实践 - 构建聊天界面

<div className={styles["chat-input-area"]}>
  <Row>
    <Input.TextArea
      placeholder="请输入消息"
      autoSize={{ minRows: 4, maxRows: 5 }}
      value={content}
      onChange={(e) =>
        dispatch({
          type: "chat/chatInputMessageChange",
          payload: {
            message: e.target.value,
          },
        })
      }
    />
  </Row>
  <Row type="flex" justify="end" style={{ marginTop: 10, marginRight: 10 }}>
    <Col>
      <Button type="primary" onClick={onSendMessage} disabled={!content}>
        发送
      </Button>
      {/* for test */}
      {/* <Button type="primary" onClick={onMessageScroll}>滚动</Button> */}
    </Col>
  </Row>
</div>

发送消息

  • 必须选择聊天对象
  • 内容必须输入
  • 触发 ChatModel 定义 reducers;添加消息,重新刷新当前视图
const onSendMessage = () => {
  if (!receiveId) {
    message.error("请选择聊天对象");
    return;
  }

  if (!content) {
    return;
  }

  let messageTemplate = {
    type: "MS:TEXT",
    chatType: "CT:SINGLE",
    content,
    sendId: userId,
    receiveId: receiveId,
  };
  const temp = messageList.concat();
  temp.push(messageTemplate);
  dispatch({
    type: "chat/refreshChatList",
    payload: {
      messageList: temp,
    },
  });
  dispatch({
    type: "chat/chatInputMessageChange",
    payload: {
      message: null,
    },
  });
};

超过消息容器,自动滚动至底部

  • 可以使用原生 js 实现
  • 当前使用 react-scroll 模块实现

使用 react-scroll

  • npm install react-scroll --save

  • 添加 react-scroll 代码

import { Element } from "react-scroll";
  • 在遍历完消息后面添加
<Element name="bottomElement"></Element>

React 实践 - 构建聊天界面

  • 调用 react-scroll API
import { scroller } from "react-scroll";

// scrollId 为 Element name 定义
scroller.scrollTo(`scrollId`, {
  duration: 800,
  delay: 0,
  smooth: true,
  containerId: `containerId`,
  offset: 50,
});

封装 react-scroll API

  • utils/scroller.js
import { scroller } from "react-scroll";

export const scrollToBottom = (scrollId, containerId) => {
  scroller.scrollTo(scrollId, {
    duration: 800,
    delay: 0,
    smooth: true,
    containerId: containerId,
    offset: 50,
  });
};

更多 react-scroll API 参考 react-scroll 模块文档 https://www.npmjs.com/package/react-scroll

使用

  • 选中聊天对象调用 scrollToBottom
  • 在发送消息完成之后调用 scrollToBottom
const onMessageScroll = () => {
  scrollToBottom("bottomElement", "chatItems");
};

// 发送消息
...
 dispatch({
    type: 'chat/chatInputMessageChange',
    payload: {
      message: null,
    },
  });
  onMessageScroll(); // 调用 scrollToBottom
};
....

// 进入页面
useEffect(() => {
  // 没有选中聊天对象,返回 聊天初始化界面
if (!receiveId) {
    history.replace({ pathname: '/im' });
    return;
  }
  onMessageScroll();
  return () => {};
}, []);

React 实践 - 构建聊天界面
关注公众号 「全栈技术部」,不断学习更多有趣的技术知识。

React 实践 - 构建聊天界面
上一篇:并发的一些好项目


下一篇:实时监听输入框值变化的完美方案:oninput & onpropertychange