React 16.x折腾记 - (7) 基于React+Antd封装聊天记录(用到React的memo,lazy, Suspense这些)

本文涉及的产品
对象存储 OSS,20GB 3个月
对象存储 OSS,恶意文件检测 1000次 1年
对象存储 OSS,内容安全 1000次 1年
简介: 在重构的路上,总能写点什么东西出来 , 这组件并不复杂,放出来的总觉得有点用处一方面当做笔记,一方面可以给有需要的人; 有兴趣的小伙伴可以瞅瞅。


前言


在重构的路上,总能写点什么东西出来 , 这组件并不复杂,放出来的总觉得有点用处

一方面当做笔记,一方面可以给有需要的人; 有兴趣的小伙伴可以瞅瞅。


效果图




实现的功能


  • 渲染支持图片,文字,图文
  • 支持删除条目(并给予父回调)


用到技术点:


  • css module: 包括内置的继承特性,类似less的嵌套写法那种
  • 用到的react 16.6特性
  • lazy, Suspense来实现子组件的懒加载
  • memo让函数式组件有PureComponent的特性(浅比较)
  • flexbox来布局
  • 用了lodashisEqual来深度比较对象,用于getDerivedStateFromProps(避免每次都更新state)


代码实现


index.js : 组件的主入口


import React, { PureComponent, lazy, Suspense } from 'react';
import { Avatar, Icon, Popover } from 'antd';
import style from './index.css';
// lodash 深比较
import isEqual from 'lodash/isEqual';
// 渲染不同内容的组件
const LazyComponent = lazy(() => import('./RenderContent'));
export default class index extends PureComponent {
  state = {
    deleteBtnSpin: false,
    loading: true,
    list: [
      {
        time: '2018-11-12 15:35:15',
        avatar:
          'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90',
        nickname: '用户甲',
        pos: 1,
        voice:
          'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-chat/3_508340417_c84f79407f5bc16b9e7ee0373631cf35.aac',
        text: '',
      },
      {
        time: '2018-11-12 15:36:15',
        avatar:
          'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90',
        nickname: '用户甲',
        pos: 1,
        voice:
          'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-chat/3_508340417_c84f79407f5bc16b9e7ee0373631cf35.aac',
        text: '',
      },
      {
        time: '2018-11-12 15:37:15',
        avatar:
          'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90',
        nickname: '卡布奇诺',
        pos: 2,
        voice: '',
        text:
          '该词语多用于讽刺和揶揄调侃。也有送快递、查水电气、社区送温暖等引申说法。例如:(1)有人在网络上发表了不合乎相关法律法规或者破坏社会稳定和谐等消息而被警方捕;(2)在贴吧或论坛里拥有删帖权限的大小吧主,检查贴吧里是否存在灌水的帖子或跟帖,遇到就进行删除的行为。',
      },
      {
        time: '2018-11-12 15:38:15',
        avatar:
          'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90',
        nickname: '卡布奇诺',
        pos: 2,
        voice: '',
        img:
          'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3040115650,4147729993&fm=26&gp=0.jpg',
        text:
          '该词语多用于讽刺和揶揄调侃。也有送快递、查水电气、社区送温暖等引申说法。例如:(1)有人在网络上发表了不合乎相关法律法规或者破坏社会稳定和谐等消息而被警方捕;(2)在贴吧或论坛里拥有删帖权限的大小吧主,检查贴吧里是否存在灌水的帖子或跟帖,遇到就进行删除的行为。',
      },
      {
        time: '2018-11-12 15:39:15',
        avatar:
          'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90',
        nickname: '卡布奇诺',
        pos: 2,
        voice: '',
        img:
          'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3040115650,4147729993&fm=26&gp=0.jpg',
      },
    ],
  };
  static getDerivedStateFromProps(nextProps, prevState) {
    const { data } = nextProps;
    // 若是props和缓存state一致,则不更新state
    if (isEqual(prevState.prevData, nextProps.data)) {
      return null;
    }
    // 若是没有传入props也是
    if (!data || !Array.isArray(data) || data.length <= 0) {
      return null;
    }
    return {
      list: data,
      prevData: nextProps.data,
    };
  }
  // 唤醒子组件的回调过程
  wakeUpLazyComponent = () => {
    return <div>loading.....</div>;
  };
  // 悬浮条目显示删除按钮
  showOperatBtn = index => {
    let tmpList = [...this.state.list];
    tmpList = tmpList.map((item, innerIndex) => {
      if (index === innerIndex) {
        item.operatBtn = true;
      } else {
        item.operatBtn = false;
      }
      return item;
    });
    this.setState({ list: tmpList });
  };
  // 关闭操作按钮
  hideOperatBtn = index => {
    let tmpList = [...this.state.list];
    tmpList = tmpList.map((item, innerIndex) => {
      item.operatBtn = false;
      return item;
    });
    this.setState({ list: tmpList });
  };
  // 删除这条回复
  deleteCurrentReplay = (index, itemInfo) => {
    let tmpList = [...this.state.list];
    tmpList.splice(index, 1);
    this.setState({ list: tmpList });
    // 给父的回调,把该item的所有信息返回,外部再去执行接口操作什么的
    if (this.props.deleteItem) {
      this.props.deleteItem(itemInfo);
    }
  };
  render() {
    const { list, deleteBtnSpin } = this.state;
    // 是否显示操作区域
    const { operate } = this.props;
    // 渲染组件的前置条件
    const isRender = list && list.length > 0;
    return (
      <ul className={style['list-wrapper']}>
        {isRender &&
          list.map((item, listIndex) => {
            return (
              <Suspense fallback={this.wakeUpLazyComponent()} key={listIndex}>
                <li
                  className={style['list-item']}
                  onMouseOver={() => this.showOperatBtn(listIndex)}
                  onMouseLeave={() => this.hideOperatBtn(listIndex)}
                >
                  <span className={style['time']}>{item.time ? item.time : '时间占位符'}</span>
                  <div
                    className={
                      item.pos === 1
                        ? style['list-item-horizontal']
                        : style['list-item-horizontal-reverse']
                    }
                  >
                    <Avatar
                      shape="square"
                      src={
                        item.avatar
                          ? item.avatar
                          : 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png'
                      }
                    />
                    <div
                      className={
                        item.pos === 1 ? style['content-wrapper-flex'] : style['content-wrapper']
                      }
                    >
                      <p className={item.pos === 1 ? style.nickname : style['nickname-right']}>
                        {item.nickname ? item.nickname : '用户昵称占位符'}
                      </p>
                      <div className={style.content}>
                        <LazyComponent {...item} />
                      </div>
                    </div>
                    {!!operate && item.operatBtn ? (
                      <Popover
                        content={'此操作会删除该记录'}
                        title="谨慎操作!"
                        onMouseEnter={() => {
                          this.setState({ deleteBtnSpin: true });
                        }}
                        onMouseLeave={() => {
                          this.setState({ deleteBtnSpin: false });
                        }}
                      >
                        <Icon
                          type="delete"
                          spin={deleteBtnSpin}
                          style={{
                            fontSize: 24,
                            alignSelf: 'flex-end',
                            color: `${this.state.deleteBtnSpin ? '#ec1414' : '#1890ff'}`,
                          }}
                          onClick={() => this.deleteCurrentReplay(listIndex, item)}
                        />
                      </Popover>
                    ) : null}
                  </div>
                </li>
              </Suspense>
            );
          })}
      </ul>
    );
  }
}


RenderContent.js:渲染对话条目


import React, { memo } from 'react';
import style from './index.css';
// antd 图文组件
import { Card } from 'antd';
const { Meta } = Card;
const RenderContent = memo(props => {
  if (props.img && props.text) {
    return (
      <Card
        hoverable
        style={{ width: 300 }}
        cover={<img alt="example" src={props.img ? props.img : ''} />}
      >
        <Meta description={props.text ? props.text : ''} />
      </Card>
    );
  }
  if (props.img) {
    return (
      <div className={style['img-wrapper']}>
        <img className={style['img-preview']} src={props.img ? props.img : ''} alt="photos" />
      </div>
    );
  }
  if (props.text) {
    return <div className={style['bubble']}>{props.text}</div>;
  }
  if (props.voice) {
    return <audio src={props.voice ? props.voice : ''} controls />;
  }
  return null;
});
export default RenderContent;


index.css : 样式


composescss module能识别的特殊字段,用于继承其他样式的


/* 列表全局样式 */
.list-wrapper {
  list-style-type: none;
  list-style: none;
  padding-left: 0;
}
/* 列表基本样式 */
.list-item {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-content: flex-start;
  margin: 15px 0;
}
/* 水平展开 */
.list-item-horizontal {
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
}
/* 右对齐方式变化 */
.list-item-horizontal-reverse {
  composes: list-item-horizontal;
  flex-direction: row-reverse;
}
/* 用户名 */
.nickname {
  font-size: 12px;
  padding:0 10px;
  color: #8a8484;
  margin-bottom: 5px;
}
/* 用户名右对齐 */
.nickname-right {
  composes: nickname;
  text-align: right;
}
/* 时间样式 */
.time {
  text-align: center;
  background-color: #cecece;
  color: #fff;
  border-radius: 3px;
  align-self: center;
  font-size: 12px;
  padding: 5px;
  margin:5px;
}
/* 内容区域 */
.content-wrapper {
  margin: 0 15px;
}
/* 弹性伸缩 */
.content-wrapper-responsive {
  flex: 1;
}
/* 气泡文字 */
.bubble {
  padding: 8px;
  color: #333;
  max-width: 300px;
  line-height: 1.5;
  background-color: #a7e544;
  border-radius: 3px;
  text-align: left;
  text-indent: 10px;
  margin:0 3px;
}
/* 图片预览 */
.img-wrapper {
  box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14),
    0px 2px 1px -1px rgba(0, 0, 0, 0.12);
  border-radius: 3px;
  padding: 5px;
}
.img-preview {
  max-width: 200px;
}


使用姿势


接受的props


  • data, 格式是[Obj](数组对象);
  • operate : 布尔值(是否显示操作区域)


列表条目字段解释


  • time: 时间
  • avatar: 用户头像
  • nickname:用户昵称
  • pos: 1 (1在左侧渲染,2在右侧渲染)
  • voice(音频)/text(文本内容)/ img(图片内容) => voice(唯一)/text + img / text(唯一)


{
    time: '2018-11-12 15:35:15',
    avatar:
      'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90',
    nickname: '用户甲',
    pos: 1,
    voice:
      'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-chat/3_508340417_c84f79407f5bc16b9e7ee0373631cf35.aac',
    text: '',
    img:
          'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3040115650,4147729993&fm=26&gp=0.jpg'
  },
相关实践学习
借助OSS搭建在线教育视频课程分享网站
本教程介绍如何基于云服务器ECS和对象存储OSS,搭建一个在线教育视频课程分享网站。
目录
相关文章
|
2月前
|
前端开发 JavaScript 网络架构
react对antd中Select组件二次封装
本文介绍了如何在React中对Ant Design(antd)的Select组件进行二次封装,包括创建MSelect组件、定义默认属性、渲染Select组件,并展示了如何使用Less进行样式定义和如何在项目中使用封装后的Select组件。
89 2
react对antd中Select组件二次封装
|
2月前
|
前端开发
React添加路径别名alias、接受props默认值、并二次封装antd中Modal组件与使用
本文介绍了在React项目中如何添加路径别名alias以简化模块引入路径,设置组件props的默认值,以及如何二次封装Ant Design的Modal组件。文章还提供了具体的代码示例,包括配置Webpack的alias、设置defaultProps以及封装Modal组件的步骤和方法。
66 1
React添加路径别名alias、接受props默认值、并二次封装antd中Modal组件与使用
|
1月前
|
前端开发
react 封装防抖
react 封装防抖
32 4
|
2月前
封装react-antd-table组件参数以及方法如rowSelection、pageNum、pageSize、分页方法等等
文章介绍了如何封装React-Antd的Table组件,包括参数和方法,如行选择(rowSelection)、页码(pageNum)、页面大小(pageSize)、分页方法等,以简化在不同表格组件中的重复代码。
59 0
|
5月前
|
前端开发 API
|
4月前
|
前端开发
Vue3 【仿 react 的 hook】封装 useTitle
Vue3 【仿 react 的 hook】封装 useTitle
49 0
|
4月前
|
前端开发 API
Vue3 【仿 react 的 hook】封装 useLocation
Vue3 【仿 react 的 hook】封装 useLocation
40 0
|
8天前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
33 9
|
30天前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
1月前
|
前端开发 JavaScript API
探索React Hooks:前端开发的革命性工具
【10月更文挑战第5天】探索React Hooks:前端开发的革命性工具