Cuando estemos desarrollando un chat de mensajería instantánea, encontraremos dos requisitos:
1. Cuando se carga un mensaje nuevo, deje que la barra de desplazamiento se deslice automáticamente hacia abajo
2. Desplácese hacia arriba para cargar el registro del historial y mantener la barra de desplazamiento en la posición original
Te mostré parte del código. Eliminé parte de la lógica vinculada al negocio. El código puede ser un poco complicado.
Los principales puntos a tener en cuenta son:
- Use el método scrollIntoView () en el componente secundario. Este método desplazará el contenedor principal del elemento para hacer que el elemento llamado scrollIntoView () sea visible para el usuario, que logra la función de desplazarse hacia abajo a medida que se carga el mensaje.
useEffect(() => {
if (shouldScroll) {
$container.current.scrollIntoView()
}
}, [])
- Al cargar subcomponentes de mensajes en un bucle, no use el índice de índice para la clave, pero use el id, de modo que si dos elementos tienen la misma clave y satisfacen el mismo tipo de elemento, si los atributos del elemento cambian, React solo actualice el propiedad del componente, también asegurará que la barra de desplazamiento para cargar los mensajes del historial permanezca en la posición original en lugar de desplazarse hasta el final. (Por favor, comprenda el mecanismo de React Key usted mismo para principios específicos)
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)