当我们在开发im消息聊天的时候,都会遇到两种需求
1、当加载了新消息后让滚动条自动向下滑动
2、向上滚动加载历史记录滚动条保持在原来位置上
给大家展示了部分代码,我删除了和业务挂钩的部分逻辑,可能代码会有一点点的乱。
主要要注意的是:
- 在子组件中使用scrollIntoView()方法,这个方法会滚动元素的父容器,使被调用scrollIntoView()的元素对用户可见,也就达到了滚动条随着消息加载向下滑动的功能。
useEffect(() => {
if (shouldScroll) {
$container.current.scrollIntoView()
}
}, [])
- 循环加载消息子组件的时候,key不要用index索引,而是要用id,这样如果两个元素是相同的key,且满足元素类型相同, 若元素属性有所变化,则React只更新组件对应的属性,也就会保证加载历史消息滚动条保持在原来位置,而不是直接滚动到底。(具体原理请自行了解React Key机制)
MessageBox.js
import React, {
useRef, useEffect, memo } from 'react'
import PropTypes from 'prop-types'
import {
Spin } from 'antd'
import {
isEmpty } from 'lodash'
import * as api from '../../api'
import MessageItemBox from './MessageItemBox'
import '../index.scss'
const MessageBox = ({
isEnd,
queryChatRecord,
loading,
action = {
},
socket = {
},
focus,
sessionList = {
},
customerInfo = {
},
messages = {
},
}) => {
const $containerEl = useRef()
let isFetching = false // 判断是否是拉取数据操作
// 上滑滚动加载
const handleScroll = async e => {
const {
scrollHeight, clientHeight, scrollTop } = $containerEl.current || {
}
if (scrollTop + clientHeight === scrollHeight && sessionList[focus]?.messages?.hasNew) {
// 监听当滑动到底去掉新消息提醒(业务相关,可忽略)
action.clearMessageStatus({
sessionId: focus })
}
if (isEnd) {
return
}
if ($containerEl.current && e.target !== $containerEl.current) {
return
}
if (isFetching) {
return
}
const $div = e.target
if ($div.scrollTop === 0 && $div.scrollHeight > $div.clientHeight && !loading) {
isFetching = true
queryChatRecord() // 拉取历史消息
isFetching = false
}
}
/**
*
* 按照消息发送时间排序
* @param {*} session1
* @param {*} session2
* @return {*}
*/
const sort = (session1, session2) => {
return (session1.createTime) > (session2.createTime) ? 1 : -1
}
// 滚动到底部
const handleScrollBottom = () => {
const {
scrollHeight, clientHeight } = $containerEl.current
$containerEl.current.scrollTop = scrollHeight - clientHeight
// 清除新消息提醒
action.clearMessageStatus({
sessionId: focus })
}
const renderMessage = (item, index) => {
let shouldScroll = true
const isSelf = item.from?.uid === customerInfo.uid
// 【重点】
if ($containerEl.current) {
const {
scrollHeight, clientHeight, scrollTop } = $containerEl.current
shouldScroll = isSelf ||
scrollHeight === clientHeight ||
scrollTop === 0 ||
scrollTop > scrollHeight - clientHeight * 2
}
return (
<MessageItemBox
key={
item.imMsgId} //【重点】key必须使用数组内的唯一值,而不能使用index
content={
item.content}
type={
item.type}
direction={
item.direction || 'right'}
shouldScroll={
shouldScroll}
avatar={
item.from?.avatar}
username={
item.from?.name}
createTime={
item.createTime}
loading={
item.loading}
success={
item.success}
sendContent={
item.sendContent}
focus={
focus}
imMsgId={
item.imMsgId}
socket={
socket}
action={
action}
/>
)
}
return (
<>
<div
styleName='session-content-dialog'
ref={
$containerEl}
onScroll={
handleScroll}
>
{
!isEnd && loading && <div className='flex-column' style={
{
width: '100%' }}> <Spin spinning={
loading} /></div>
}
{
!isEmpty(messages) && messagesInfo.sort(sort).map((item, index) =>
renderMessage(item, index)
)}
{
isEmpty(messages) && !loading ? (
<div style={
{
textAlign: 'center', color: '#969696', marginTop: 30 }}>无记录</div>
) : (
''
)}
</div>
{
messages?.hasNew && (
<div className='flex-row-reverse' style={
{
width: '100%' }} onClick={
handleScrollBottom}>
<div
style={
{
backgroundColor: '#fff',
textAlign: 'center',
color: '#1890ff',
display: 'inline-block',
zIndex: 10,
width: 100,
padding: 5,
borderRadius: 8,
marginTop: '-34px',
cursor: 'pointer',
}}
>你有新消息
</div>
</div>
)
}
</>
)
}
MessageBox.propTypes = {
isEnd: PropTypes.bool,
loading: PropTypes.bool,
queryChatRecord: PropTypes.func,
action: PropTypes.any,
socket: PropTypes.any,
messages: PropTypes.object,
focus: PropTypes.string,
sessionList: PropTypes.object,
customerInfo: PropTypes.object,
}
export default memo(MessageBox)
MessageItemBox.js
import React, {
useEffect, useRef, useState, memo, lazy, Suspense } from 'react'
import PropTypes from 'prop-types'
import {
Icon, message } from 'antd'
import moment from 'moment'
import {
post } from 'utils/request'
import {
emojiData } from '../../config'
import '../index.scss'
const MediaMessage = lazy(() => import('./MediaMessage'))
const validKnowledge = payload => post('/im/imMessageService/validKnowledge', payload)
/** 客服相关 */
// 客服状态列表
const MessageItemBox = ({
msgSource = 1,
createTime,
content = {
},
direction,
avatar,
type,
shouldScroll,
loading,
success,
username,
sendContent = {
},
focus,
imMsgId,
action,
socket,
}) => {
const $container = useRef()
// const action = useAction()
// const socket = useSocket()
const [curLoading, setCurLoading] = useState(loading)
const [visible, setVisible] = useState(false)
useEffect(() => {
// 【重点】判断是否需要滚动,滚动条自动向下滑动
if (shouldScroll) {
$container.current.scrollIntoView()
}
}, [])
useEffect(() => {
setCurLoading(loading)
}, [loading])
const handleMedia = () => {
setVisible(true)
}
const getContent = () => {
switch (type) {
...
default: {
if (!content.msg) return ''
const res = renderText(content.msg)
return <>
<div styleName='dialogue-arrow' />
{
content.msg}
</>
}
}
}
/**
* 重发消息
*/
const handleReSend = async () => {
setCurLoading(true)
try {
const res = await socket.send(sendContent)
action.updateSessionMessage(focus, imMsgId, sendContent, res)
} catch (error) {
action.updateSessionMessage(focus, imMsgId, sendContent, {
sendSuccess: false })
}
setCurLoading(false)
}
return (
<div style={
{
textAlign: 'center', marginBottom: 10 }} ref={
$container}>
<div className='flex-column' style={
{
alignItems: direction === 'left' ? 'flex-start' : 'flex-end' }}>
<div className='mb8'>
{
`${
username}(${
moment(createTime).format('YYYY-MM-DD HH:mm:ss')})`}
</div>
<div
style={
{
display: 'flex', flexDirection: direction === 'left' ? 'row' : 'row-reverse', alignItems: 'center' }}
>
<div>
<span styleName='dialogue-avatar'>
<img src={
avatar + '?imageView2/1/w/40/h/40'} />
</span>
</div>
<div styleName={
`dialogue-popover-${
direction}`}>{
getContent()}</div>
{
curLoading && <Icon type='loading' />}
{
!success && !curLoading &&
<div onClick={
handleReSend}><Icon type='exclamation' style={
{
color: 'red' }} /></div>
}
</div>
</div>
{
visible &&
<Suspense>
<MediaMessage visible={
visible} type={
type} onCancel={
() => setVisible(false)} src={
content.url} />
</Suspense>
}
</div>
)
}
MessageItemBox.propTypes = {
msgSource: PropTypes.number,
createTime: PropTypes.number,
type: PropTypes.number,
content: PropTypes.object,
shouldScroll: PropTypes.bool,
avatar: PropTypes.string,
direction: PropTypes.string,
loading: PropTypes.bool,
success: PropTypes.bool,
sendContent: PropTypes.object,
imMsgId: PropTypes.string,
focus: PropTypes.string,
username: PropTypes.string,
action: PropTypes.any,
socket: PropTypes.any,
}
export default memo(MessageItemBox)