React-列表组件(通知列表、私信列表、虚拟列表)

极客逐浪客
• 阅读 3353

引言

最近在做社交网站开发,过程中需要用到三种组件:通知列表组件、聊天列表组件和虚拟列表组件。这三种组件都是社交网站必备的,现在把我在开发中遇到的问题以及代码全部分享给大家,希望对大家有所帮助。

通知列表

我们在使用社交网站的过程中会发现他们通常会使用下拉通知展现通知列表数据,这种就是现在要介绍的通知列表,如图所示:
React-列表组件(通知列表、私信列表、虚拟列表)

首先我们先来介绍下通知下拉列表的工作原理:
1、点击通知按钮
2、查询列表数据并展现
3、向下拉滚动条过程中分页加载数据,并把数据合并到列表中,直到数据全部加载完

现在看看我们如何制作吧。首先,我们需要使用一个React插件:InfiniteScroll,废话不多说直接上代码:

<div id="scrollableDiv" className={styles.noticeBody}>
              {
                loading&&page.pageNum === 1? <Loading />:<InfiniteScroll
                  //注意:dataLength={remindList.length}要写remindList.length不能写成remindListTotal,切记!
                    dataLength={remindList.length}
                    next={loadMoreData}
                    height={413}
                    hasMore={remindList.length < remindListTotal}
                    loader={<Loading/>}
                    scrollableTarget="scrollableDiv">
                  <List
                      split={false}
                      itemLayout="horizontal"
                      dataSource={remindList}
                      renderItem={item => (your list item in here)}
                  />
                </InfiniteScroll>
              }
            </div>

我们看一下上面的代码,首先我们要定义一个id=scrollableDiv的div,接着判断如果当前页码是1的话,则显示loading加载组件。

注意: 因为InfiniteScroll组件,默认如果没有数据是不主动触发next对应的loadMoreData获取下一页数据的方法,所以最好我们打开下拉框的时候就主动去获取列表第一页的数据,在获取过程中我们可以先用loading效果展示给用户,目的是为了提升更好的体验,当然你也可以不用加!
loading&&page.pageNum === 1? <Loading />

现在,来看看这段代码:

<InfiniteScroll
                  //注意:dataLength={remindList.length}要写remindList.length不能写成remindListTotal,切记!
                    dataLength={remindList.length}
                    next={loadMoreData}
                    height={413}
                    hasMore={remindList.length < remindListTotal}
                    loader={<Loading/>}
                    scrollableTarget="scrollableDiv">
                  <List
                      split={false}
                      itemLayout="horizontal"
                      dataSource={remindList}
                      renderItem={item => (your list item in here)}
                  />
                </InfiniteScroll>

不知道大家有没有看InfiniteScroll文档上面的代码,如果没有的话,我这里就简单介绍一下,并且把开发过程中遇到的问题给大家点明一下防止入坑。
dataLength={remindList.length}表示当前数据长度
next={loadMoreData}表示获取下一页数据的方法,它会随着滚动条的滚动自动触发的
hasMore={remindList.length < remindListTotal}表示什么时候显示loading效果
loader={<Loading/>}表示loading效果组件
scrollableTarget表示它是依赖于id=scrollableTarget的div的

注意:1、这里需要注意的是dataLength应该是当前列表的长度,否则滚动条滚动到列表底部的时候不会触发获取下一页数据的方法loadMoreData
2、因为下拉列表滚动加载过程中,列表数据源remindList是一直增加的,它是把每页的数据源merge在一起的。

私信列表

React-列表组件(通知列表、私信列表、虚拟列表)
私信列表就比较特殊了,大家都用过微信,QQ的,它的聊天记录是向上滚动加载,跟我们的通知下拉列表刚好相反,庆幸的是InfiniteScroll组件也提供该功能,直接上代码:

<div className={styles.chatList} id="scrollableDiv" ref={dom}>
            <InfiniteScroll
              //注意:dataLength={remindList.length}要写remindList.length不能写成remindListTotal,切记!
              dataLength={chatList.length}
              next={getCurrentData}
              hasMore={showChatListLoading}
              loader={<Loading/>}
              style={{
                display: "flex",
                flexDirection: "column-reverse"
              }}
              scrollableTarget="scrollableDiv"
              inverse={true}>
                your list item in here
            </InfiniteScroll>
          </div>

跟之前一样,我们来分析下这段代码,因为是滚动条向上滚动加载,所以我们要把loading放置在顶部,所以要加上inverse={true},同时还要设置两个样式:
样式一、

style={{
    display: "flex",
    flexDirection: "column-reverse"
  }}

样式二、

.chatList{
  height: calc(100vh - 186px);
  overflow-y: auto;
  display: flex;
  flex-direction: column-reverse;
  overflow-anchor: none;
}

这样就完成了反向上拉加载分页数据了,其它属性跟上面大同小异这里就不过多描述了,不过要注意几个问题:

注意:1、我在向上滚动的时候分页也成功了,也合并到列表中,可是滚动条一直在顶部,看过qq和微信的同学应该都知道,向上滚动加载的时候,滚动条应该在当前聊天记录上,而不是在最顶部,然后在网上搜索才知道,只要在父节点上加上这个代码就可以了:overflow-anchor: none;
2、聊天记录的列表跟下拉通知列表数据也是相似的,每次向上滚动的时候我们都会合并到chatList(暂定为聊天记录列表名)中,但是有一点不一样,我可以通过输入聊天内容并展现到列表中,通知下拉可是没有输入展现功能,这点非常重要,因为我们会遇到一个非常棘手的问题:如何正确合并数据?以及合并数据之后分页查询重复问题?
正确合并数据:估计好多小伙伴已经想到了,后台把数据推送给前端之后,直接concat到chatList中(注意不是分页查询,因为那样页面会有闪动的不好体验),这也没问题
分页查询重复问题:我们来看看什么是分页查询重复问题,这里有篇文章大家可以看看,于是我们使用了上面第2种解决方案。

解决分页查询重复问题

解决思路2
请求第1页时记录第1条数据(即最新的那条)的写入时间, 然后后面查询第2,3,4...页数据, 把记录的写入时间作为参数, 然后在sql语句中做限制
例如查询第2页, 设置写入时间小于等于2019-05-15 19:31:59, 这样即使有新数据插入, 也不在我们本次分页查询的范围内.
select * from table1 where write_time <=1557919919000 order by write_time desc limit 5,5

既然我们已经知道了如何解决,下面给出具体步骤以及代码:

1、当后台推送数据给前端的时候,我们先把数据合并到chatList中,并给个标识type=websocket
2、当用户向上滚动的时候,我们可以通过findIndex拿到这个type=websocket的数据的创建时间,通过分页接口传递给后台
3、后台返回数据之后我们再合并到chatList中

分页代码:

export const getMessages = createAsyncThunk('notify/getMessages', async (params, thunkAPI) => {
  try {
    const notify = thunkAPI.getState().notify
    if (notify.chatListPage.pageNum > 1) {
      // 找到第1个type=websocket的数据,然后赋值给flagCreatedTime即可
      // 为什么找到第1个?因为list中新加的websocket数据是从尾部开始加的,所以只要从索引0找到到最近一个type=websocket就是从这个时间开始算的,而不是最后一个
      const i = notify.chatList.findIndex(item => item.type === 'websocket')
      if (i > -1) {
        params.flagCreatedTime = notify.chatList[i].createdDt
      }
    }


    const res = await axios.post(`/notify/crud/messages/getMessages`, {
      dialogId: params.dialogId,
      pageNum: params.pageNum,
      pageSize: params.pageSize,
      flagCreatedTime: params.flagCreatedTime
    });
    return res.data
  } catch (error) {
    return thunkAPI.rejectWithValue({errorMsg: error.message});
  }
});

分页方法的回调:

[getMessages.fulfilled]:(state, action) => {
    if (action.payload.data) {
      // 获取总数
      const total = action.payload.data.total
      state.chatListPage = {
        pageSize: action.payload.data.pageSize,
        pageNum: action.payload.data.pageNum,
        total: action.payload.data.total
      }

      const rows = action.payload.data.rows.reverse()

      // 这里做这个判断是因为react18 useeffect会执行两次,所以我根据Pagenum判断是否要合并以避免重复合并问题
      if (action.payload.data.pageNum > 2) {
        state.chatList = [
          ...rows,
          ...state.chatList
        ]
      } else {
        state.chatList = rows
      }
      // 设置是否要分页加载,InfiniteScroll组件的hasMore属性会用到

      state.showChatListLoading = state.chatList.length < total
        // 当pageNum是第1页并且总数据还不到pageSize,说明根本没分页的必要
        && !(total<state.chatListPage.pageSize && state.chatListPage.pageNum === 1)
      // state.loadingMsg = false;
    }
  },

合并的代码:

// 接收消息并合并到chatList消息列表中
concatMessageInChatList: (state, action) => {
  if (action.payload.length > 0) {
    action.payload.forEach(item => {
      item.type = 'websocket';
    })
  }
  state.chatList = [
    ...state.chatList,
    ...action.payload
  ]
}

这样就能解决分页数据重复问题以及合并问题了。

虚拟列表

什么是虚拟列表?

我们上面谈到了通知下拉列表,聊天向上拉列表,他们都有一个共同点,就是随着滚动加载我们往列表中合并的数据越来越多,html中的dom元素也是断累加的,如下所示:
React-列表组件(通知列表、私信列表、虚拟列表)

而虚拟列表的意思是不管你有多少条数据,每次html中的列表元素只展示固定的条数,虽然数据源仍然是合并的,但是html的dom元素只显示固定的几条,如下所示:
React-列表组件(通知列表、私信列表、虚拟列表)

比如上面的列表,不管你是向上滚动还是向下滚动都只显示这6条,这就是虚拟列表,目的也是为了减少dom元素的渲染尤其是大量数据的场景下尤其有效,比如:微博、facebook、twitter等等。
React-列表组件(通知列表、私信列表、虚拟列表)

如何使用虚拟列表?

首先,我们来认识一个插件:react-virtuoso,这个插件非常厉害,提供的功能也非常全面,感兴趣的小伙伴可以看看,废话不多说,直接上代码:

<Virtuoso
  useWindowScroll
  ref={virtuosoRef}
  data={latestNews}
  context={{ abc: loadingData }}
  components={{Footer: ({ height, index, context: { abc }}) => {
      return <>
        {
          !abc&&latestNews.length===0?<div className={styles.empty}>
            <span className={styles.tip}>暂无数据</span>
            <p className={styles.desc}>快去添加好友吧</p>
          </div>:null
        }
        <div style={{
          height: '400px',
          display: 'flex',
          alignItems: 'flex-start',
          justifyContent: 'center',
          paddingTop: '100px'
        }}>
          {abc ? <Spin indicator={<LoadingOutlined
            style={{
              fontSize: 36,
            }}
            spin
          />}/> : null}
        </div>
      </>
    }}}
  overscan={20}
  endReached={loadMore}
  itemContent={(index, item) => (
    your item in here 
  )}
/>

还是先带大家看看这个组件的相关属性吧。

useWindowScroll表示使用window级别滚动条,而不是某个dom里面的滚动条
ref={virtuosoRef}这个不用多说了吧,react自带的功能
data={latestNews}表示列表数据源,注意虽然是虚拟列表,但是数据源仍然是合并的
context={{ abc: loadingData }}表示定义全局变量,如果你需要把useState或者useRef值传递到Virtuoso组件中需要定义变量值,否则你在组件中是不能使用的,这个要注意一下
components={{Footer: ({ height, index, context: { abc }}) =>表示自定义底部组件ReactNode,里面的context: { abc }就是我们上面定义的在这里就用到了
overscan={20}表示设置该属性以使组件“chunk”在滚动上呈现新项目。该属性会导致组件渲染的项目多于所需的项目,但会减少滚动时的重新渲染
endReached={loadMore}表示滚动到页面底部的时候触发获取下一页数据的方法
itemContent={(index, item) =>表示列表项渲染的ReactNode

注意:1、使用Virtuoso组件,并且设置useWindowScroll,目的是为了在整个页面上面滚动
2、使用context,传递数据,否则不能直接获取到context数据
3、使用virtuosoRef可以控制滚动条位置

这样就完成了虚拟列表的功能了,这里再次提醒一下react-virtuoso组件提供了好多丰富的功能,像上面的通知列表、聊天列表其实都可以使用react-virtuoso组件,只不过感觉数据量不大的场景下使用react-infinite-scroll-component组件还是满方便的,主要还是看大家使用场景和需求了。

总结

1、要理解向上滚动和向下滚动加载的原理
2、其实还有一种移动端解决方案:当滚动条在顶部的时候向下拉之后也会重新加载数据,跟滚动条滚动到顶部重新加载数据的区别在于有个向下拉的动作,同样的使用react-infinite-scroll-component组件可以完成这个功能。
3、虚拟列表的使用场景是在大批数量级的情况下使用,极大减少了dom的渲染减轻浏览器压力。

引用

react滚动加载组件,超级好用
React hooks实现聊天室
React18的useEffect会执行两次
CSS: overflow-anchor 固定滚动到底部,随着页面内容增多滚动条自己滚动展示最新的内容

点赞
收藏
评论区
推荐文章
Python进阶者 Python进阶者
3年前
盘点一道Python列表合并的基础题目(列表推导式)
大家好,我是我是皮皮。一、前言前几天Python青铜交流群有个叫【猎影】的粉丝问了一个关于时间转换的问题,这里拿出来给大家分享下,可以看到报错如下图所示。题目:两个列表:运行之后,可以得到答案。如果不加那个判断的话,得到的答案是下图这样的:如果列表中的1和2都是int数据类型的话,直接一个列表推导式可以搞定,如下图所示:方法二:列表推导式使用列表推导式一
Wesley13 Wesley13
3年前
java编码优化10技巧
最近,我给Java项目做了一次代码清理工作。经过清理后,我发现一组常见的违规代码(指不规范的代码并不表示代码错误)重复出现在代码中。因此,我把常见的这些违规编码总结成一份列表,分享给大家以帮助Java爱好者提高代码的质量和可维护性。这份列表没有依据任何规则或顺序,所有的这些都是通过代码质量工具包括CheckStyle(https://www.osch
Jacquelyn38 Jacquelyn38
4年前
关于Vue在面试中常常被提到的几点
❝现在Vue几乎公司里都用,所以掌握Vue至关重要,这里我总结了几点,希望对大家有用❞1、Vue项目中为什么要在列表组件中写key,作用是什么?我们在业务组件中,会经常使用循环列表,当时用vfor命令时,会在后面写上:key,那么为什么建议写呢?key的作用是更新组件时判断两个节点是否相同。相同则复用,不相同就删除旧的创建新的。正是因为带唯一key时
Stella981 Stella981
3年前
Notification使用详解之一:基础应用
在消息通知时,我们经常用到两个组件Toast和Notification。特别是重要的和需要长时间显示的信息,用Notification就最合适不过了。当有消息通知时,状态栏会显示通知的图标和文字,通过下拉状态栏,就可以看到通知信息了,Android这一创新性的UI组件赢得了用户的一致好评,就连苹果也开始模仿了。今天我们就结合实例,探讨一下Notifica
Stella981 Stella981
3年前
Egret之egret.gui.List的使用教程
工具:EgretWing说明:List         列表组件ItemRender   列表Item组件这里只讲解一下如何绑定数据到List以及对应到ItemRender展示。/  Created by haocao on 15/6/25. /class 
Wesley13 Wesley13
3年前
OPMS 1.2 版本更新发布
主要新增消息通知及考勤管理,其他功能优化,样式优化1、修订用户登录后跳转2、修订审批请假,天数不能输入小数问题3、增加考勤管理  1、增加上下班考勤打卡  2、个人考勤列表、小计、搜索  3、全部员工考勤列表,小计  4、员工管理添加个人考勤快捷链接4、增加消息通知功能  1、消息顶部红点显示
Stella981 Stella981
3年前
React Native 开发豆瓣评分(七)首页组件开发
首页内容拆分看效果图,首页由热门影院、豆瓣热门、热门影视等列表组成,每个列表又由头加横向滑动的电影海报列表构成。所以可以先把页面的电影海报、评分、列表头做成组件,然后在使用ScrollView将内容包裹即可构成首页。<divaligncenter<imgsrc"https://img2018.cnblogs.co
Python进阶者 Python进阶者
2年前
这种数组 能不能设置为每隔多少个数是一个小数组 ?
大家好,我是皮皮。一、前言前几天在Python白银群【E】问了一个Python列表基础的问题,这里拿出来给大家分享下。他的列表如下图所示:想要的效果就是下图这种的相对规整一些的。二、实现过程这里【巭孬嫑勥烎】给了一个思路,把一个列表,分割为n个小列表。de
达里尔 达里尔
1年前
vant2下拉列表组件封装
vue2vant2下拉列表组件封装
融云IM即时通讯 融云IM即时通讯
7个月前
融云IM干货丨uni-app中的uni-list 插件具体怎么用?
unilist是uniapp中用于构建列表的组件,以下是具体的使用方法:1.基本用法导入组件:首先,你需要在你的页面或组件中导入unilist和unilistitem组件。例如:javascriptimportuniListfrom'@/component
布局王 布局王
1个月前
鸿蒙Next仓颉语言开发实战教程:下拉刷新和上拉加载更多
在移动应用中,各种列表页面离不开下拉刷新和上拉加载更多,我们的商城应用也是如此。今天介绍一下在仓颉开发语言中如何实现这一功能。下拉刷新仓颉开发语言直接提供了下拉刷新的组件,叫做Refresh,使用起来也非常方便:@StatevarheaderLoading