React推动消息 解决长列表方案

react-virtualized

使用react-virtualized优化

在react生态中, react-virtualized作为长列表优化的存在已久, 社区一直在更新维护, 讨论不断, 同时也意味着这是一个长期存在的棘手问题! ?

解决以上问题的核心思想就是: 只加载可见区域的组件
react-virtualized将我们的滚动场景区分为了viewport内的局部滚动, 和基于viewport的滚动, 前者相当于在页面中开辟了一个独立的滚动区域,属于内部滚动, 这跟和iscroll的滚动很类似, 而后者则把滚动作为了window滚动的一部分(对于移动端而言,这种更为常见). 基于此计算出当前所需要显示的组件.

有教室1/2/3, 每间教室下有1000+个学生
学生组件为:

function Student({student}) {
    return <div>{student.name}</div>
}

如果我们直接把整个列表渲染出来, 仅仅学生列表就会生成1000+个div标签.

function Student({student, ...rest}) {
    return (
        <div>
            ...
                <div>{student.name} ....</div>
            ...
        </div>
    )
}

这个时候的DOM数量就会变得难以想象.

我们都知道, DOM结构如果过大, 网页就会出现用户操作体验上的问题, 比如滚动, 点击等常用操作. 同时, 对react的虚拟DOM计算以及虚拟DOM反映到真实DOM的压力也会很大. 当用户点击切换教室时, 就会出现秒级的卡顿.

使用react-virtualized优化

学生组件修改为:

function Student({student, style, ...rest}) {
    return (
        <div style={style}>
            ...
                <div>{student.name} ....</div>
            ...
        </div>
    )
}

学生列表组件:

import React from 'react'
import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'
import { List as VList } from 'react-virtualized/dist/commonjs/List'

class StudentList extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            list: []
        }
    }
    getList = () => {
        api.getList.then(res => {
            this.setState({
                list: res
            })
        })
    }
    componentDidMount() {
        this.getList()
    }
    render() {
        const { list } = this.state  
        const renderItem = ({ index, key, style }) => {
            return <Student key={key} student={list[index]} style{style} />
        }
        return (
            <div style={{height: 1000}}>
                <AutoSizer>
                    {({ width, height }) => (
                        <VList
                            width={width}
                            height={height}
                            overscanRowCount={10}
                            rowCount={list.length}
                            rowHeight={100}
                            rowRenderer={renderItem}
                        />
                    )}
                </AutoSizer>
            </div>
        )
    }
}


(外层div样式中的高度不是必须的, 比如你的网页是flex布局, 你可以用flex: 1来让react-virtualized计算出这个高度)

这个时候, 如果每个Student的高度相同的话, 问题基本上就解决啦!

可是, 问题又来了, 有时候我们的Student会是不确定高度的, 可以有两种方法解决问题, 推荐react-virtualized的CellMeasurer组件解决方案

方法一

学生列表组件修改为:

import React from 'react'
import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'
import { List as VList } from 'react-virtualized/dist/commonjs/List'
import { CellMeasurerCache, CellMeasurer } from 'react-virtualized/dist/commonjs/CellMeasurer'
 
class StudentList extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            list: []
        }
    }
    measureCache = new CellMeasurerCache({
        fixedWidth: true,
        minHeight: 58
    })
    getList = () => {
        api.getList.then(res => {
            this.setState({
                list: res
            })
        })
    }
    componentDidMount() {
        this.getList()
    }
    render() {
        const { list } = this.state  
        const renderItem = ({ index, key, parent, style }) => {
            return (
                <CellMeasurer cache={this.measureCache} columnIndex={0} key={key} parent={parent} rowIndex={index}>
                    <Student key={key} student={list[index]} />
                </CellMeasurer>
            )
        }
        return (
            <div style={{height: 1000}}>
                <AutoSizer>
                    {({ width, height }) => (
                        <VList
                            ref={ref => this.VList = ref}
                            width={width}
                            height={height}
                            overscanRowCount={10}
                            rowCount={list.length}
                            rowHeight={this.getRowHeight}
                            rowRenderer={renderItem}
                            deferredMeasurementCache={this.measureCache}
                            rowHeight={this.measureCache.rowHeight}
                        />
                    )}
                </AutoSizer>
            </div>
        )
    }
}

方法二

通过react-height或者issue中提到的通过计算回调的方法解决, 以使用react-height为例:
学生列表组件修改为:

import React from 'react'
import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'
import { List as VList } from 'react-virtualized/dist/commonjs/List'
import ReactHeight from 'react-height'
 
class StudentList extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            list: []
            heights = []
        }
    }
    getList = () => {
        api.getList.then(res => {
            this.setState({
                list: res
            })
        })
    }
    componentDidMount() {
        this.getList()
    }
    handleHeightReady = (height, index) => {
        const heights = [...this.state.heights]
        heights.push({
            index,
            height
        })
        this.setState({
            heights
        }, this.vList.recomputeRowHeights(index))
    }
    getRowHeight = ({ index }) => {
        const row = this.heights.find(item => item.index === index)
        return row ? row.height : 100
    }
    render() {
        const { list } = this.state  
        const renderItem = ({ index, key, style }) => {
            if (this.heights.find(item => item.index === index)) {
                return <Student key={key} student={list[index]} style{style} />
            }
            return (
                <div key={key} style={style}>
                    <ReactHeight
                        onHeightReady={height => {
                            this.handleHeightReady(height, index)
                        }}
                    >
                        <Student key={key} student={list[index]} />
                    </ReactHeight>
                </div>
            )
        }
        return (
            <div style={{height: 1000}}>
                <AutoSizer>
                    {({ width, height }) => (
                        <VList
                            ref={ref => this.VList = ref}
                            width={width}
                            height={height}
                            overscanRowCount={10}
                            rowCount={list.length}
                            rowHeight={this.getRowHeight}
                            rowRenderer={renderItem}
                        />
                    )}
                </AutoSizer>
            </div>
        )
    }
}

现在, 如果你的列表数据都是一次性获取得来的话, 基本上是解决问题了!

那如果是滚动加载呢?

react-virtualized官方有提供InfiniteLoader, 写法同官方!

如果抛开这个经典案例, 开发的是聊天框呢?

聊天框是倒序显示, 首次加载到数据的时候, 滚动条的位置应该位于最底部, react-virtualized中的List组件暴露了scrollToRow(index)方法给我们去实现, Student高度不一致时直接使用有一个小问题, 就是不能一次性滚动到底部, 暂时性的解决方法是:

scrollToRow = (): void => {
    const rowIndex = this.props.list.length - 1
    this.vList.scrollToRow(rowIndex)
    clearTimeout(this.scrollToRowTimer)
    this.scrollToRowTimer = setTimeout(() => {
        if (this.vList) {
            this.vList.scrollToRow(rowIndex)
        }
    }, 10)
}

在首次加载到数据时调用

由于InfiniteLoader并不支持倒序加载这样的需求, 只能自己通过onScroll方法获取滚动数据并执行相关操作, 需要注意的是, 上一页数据返回时, 如果使用方法一, 需要执行this.measureCache.clear/clearAll, 通知react-virtualized重新计算. 方法二则 应该把state.heights数组中的index全部加上本次数据的数量

getList = () => {
    api.getList.then(res => {
        const heights = [...this.state.heights]
        heights.map(item => {
            return {
                index: item.index + res.length,
                height: item.height
            }
        })
        this.setState({
            list: [...res, ...this.state.list],
            heights
        })
    })
}

socket

在src/util/socket.js

import { message } from 'antd'

const checkWebSocket = () => 'WebSocket' in window

export default class Socket {

  constructor (config = {}) {
    this.webSocket = null
    this.isConnect = false // 连接状态
    this.timeoutNum = config.timeoutNum || 5000 // 重连频率
    this.timer = null // 定时器 句柄
    this.maxNum = config.maxNum || 10 // 最大重连次数
    this.reconnectNum = 0
    this.isActivelyClose = false // 是否手动关闭
    this.config = config
    // console.log(this)
  }

  connect() {
    if (checkWebSocket()) {
      this.webSocket = new WebSocket(this.config.url)
      this.initSocket()
    } else {
      message.warning('您的浏览器不知道实时消息推送,请更换Chrome或火狐浏览器!')
    }
  }

  initSocket() {
    this.isActivelyClose = false
    this.webSocket.addEventListener('close', this.onClose, false)
    this.webSocket.addEventListener('error', this.onError, false)
    this.webSocket.addEventListener('open', this.onOpen, false)
    this.webSocket.addEventListener('message', this.onMessage, false)
  }

  destroyed() {
    this.webSocket.removeEventListener('close', this.onClose, false)
    this.webSocket.removeEventListener('error', this.onError, false)
    this.webSocket.removeEventListener('open', this.onOpen, false)
    this.webSocket.removeEventListener('message', this.onMessage, false)
  }

  // 断开
  onClose = (e) => {
    // console.log('websocket连接关闭~', this.config, e)
    this.isConnect = false
    if (!this.isActivelyClose) {
      this.reconnectSocket()
    }
  }

  // 异常
  onError = (e) => {
    // console.log('websocket连接错误~', this.config, e)
    this.isConnect = false
    this.reconnectSocket()
  }

  // 成功
  onOpen = (e) => {
    // console.log('websocket连接成功~', this.config, e)
    const { msg } = this.config
    this.isConnect = true
    if (msg) this.send(msg)
  }

  // 接收消息
  onMessage = (e) => {
    const { callback } = this.config
    if (callback) {
      try {
        callback(JSON.parse(e.data))
      } catch (e) {
        console.error('接收消息错误', e)
      }
    }
  }

  // 断开重连
  reconnectSocket() {
    if (this.isConnect || this.reconnectNum >= this.maxNum) {
      this.reconnectNum = 0
      this.clearTimer()
      return
    }
    this.reconnectNum++
    // console.log('websocket 重新连接~ ', this)
    this.clearTimer()
    this.timer = setTimeout(() => this.connect(), this.timeoutNum)
  }

  clearTimer() {
    this.timer && clearTimeout(this.timer)
  }

  // 发送消息
  send(msg) {
    try {
      this.webSocket.send(JSON.stringify(msg))
    } catch (e) {
      console.error('发送消息错误', e)
    }
  }
  // 关闭
  close() {
    this.isActivelyClose = true
    if (this.webSocket) {
      this.webSocket.close()
      this.destroyed()
    }
  }
}

layout => BasicLayout.jsx

const WS_HOST = '自己的链接scoket 推送地址'
  useEffect(() => {
    const { id } = currentUser
    if (id) {
      let num = 0
      const callback = (data) => {
        if (Array.isArray(data) && data.length) {
          console.log(`第${num++}次接收消息`)
          dispatch({
            type: 'global/saveNotices',
            payload: data
          })
        }
      }
      const host = isDev ? WS_HOST : window.location.host
      const socket = new Socket({
        url: `${WS_PROTOCOL}${host}/路径/路径/${id}`,
        msg: 'websocket:success',
        callback
      })
      socket.connect()
      return () => socket.close()
    }
  }, [currentUser])

NoticeDrawer组件

import { WindowScroller, List } from 'react-virtualized'
import 'react-virtualized/styles.css';

  <div className={styles['notice-head']}>
        <h3>监控&日志中提供了详细信息</h3>
        {notices.length ? <Button type="link" onClick={delAll}>全部消除</Button> : null}
      </div>
      {
        noticeVisible ? (
          <WindowScroller>
            {
              ({ isScrolling, onChildScroll, scrollTop }) => (
                <List
                  className={styles['list-warp']}
                  width={446}
                  height={maxHeight}
                  rowCount={notices.length}
                  rowHeight={95}
                  isScrolling={isScrolling}
                  onScroll={onChildScroll}
                  scrollTop={scrollTop}
                  rowRenderer={rowRenderer}
                />
              )
            }
          </WindowScroller>
        ) : null
      }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值