React-列表元件(通知列表、私信列表、虛擬列表)

Awbeci發表於2023-05-04

引言

最近在做社交網站開發,過程中需要用到三種元件:通知列表元件、聊天列表元件和虛擬列表元件。這三種元件都是社交網站必備的,現在把我在開發中遇到的問題以及程式碼全部分享給大家,希望對大家有所幫助。

通知列表

我們在使用社交網站的過程中會發現他們通常會使用下拉通知展現通知列表資料,這種就是現在要介紹的通知列表,如圖所示:
image.png

首先我們先來介紹下通知下拉選單的工作原理:
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在一起的。

私信列表

image.png
私信列表就比較特殊了,大家都用過微信,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元素也是斷累加的,如下所示:
image.png

而虛擬列表的意思是不管你有多少條資料,每次html中的列表元素只展示固定的條數,雖然資料來源仍然是合併的,但是html的dom元素只顯示固定的幾條,如下所示:
image.png

比如上面的列表,不管你是向上滾動還是向下滾動都只顯示這6條,這就是虛擬列表,目的也是為了減少dom元素的渲染尤其是大量資料的場景下尤其有效,比如:微博、facebook、twitter等等。
image.png

如何使用虛擬列表?

首先,我們來認識一個外掛: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 固定滾動到底部,隨著頁面內容增多捲軸自己滾動展示最新的內容

相關文章