聊天应用开发:如何获取视口中最后一个可见消息项的索引?
问题:获取聊天视口中最后一个可见消息的索引
我正在开发一款聊天应用,目前要实现滚动时根据视口中可见消息的日期,在顶部显示粘性日期的功能,但不知道如何获取视口中最后一个可见项的索引。
现有代码
const MessageComponent = () => { const { id } = useParams(); const { data: messageData, isSuccess, isError, isLoading, isFetching, hasMoreData, firstData, isUninitialized, } = useGetData(id); const { loadMore } = useGetScrollData(messageData, id); const user = User(); const chatId = messageData[0]?.type == "date" ? messageData[1]?.chat?._id : messageData[0]?.chat?._id; const islastItemChatId: boolean = messageData.length > 0 && chatId != id; const scrollRef = useScrollRef(islastItemChatId, id); const scrollFunc = (e: any) => { // let m = e.target.scrollHeight + e.target.scrollTop; // let i = e.target.scrollHeight - m; // console.log({ i, e }); if (!scrollRef.current) return; const containerMiddle = scrollRef.current.scrollTop + scrollRef.current.getBoundingClientRect().height / 2; const infiniteScrollItems = scrollRef.current.children[0].children; console.log({ containerMiddle, infiniteScrollItems, e: e.target }); }; return ( <> {messageData.length > 0 && ( <div id="scrollableDiv" style={{ height: "80%", overflow: "auto", display: "flex", flexDirection: "column-reverse", padding: "10px 0px", }} ref={scrollRef} > <InfiniteScroll dataLength={messageData.length} hasMore={hasMoreData} onScroll={scrollFunc} loader={ <div className="loading-container"> <div className="lds-ring"> <div></div> </div> </div> } endMessage={ <div className="message-container date"> <div className={`text-container sender large-margin`}> <span>You have seen all the messages</span> </div> </div> } style={{ display: "flex", flexDirection: "column-reverse" }} next={loadMore} inverse={true} scrollableTarget="scrollableDiv" > {messageData.map((item, index: number) => { const isUserChat = item?.sender?._id === user._id; const className = item?.type == "date" ? "date" : isUserChat ? "user-message" : "sender-message"; const prevItem: IMessageData | null = index < messageData?.length ? messageData[index - 1] : null; const nextItem: IMessageData | null = index < messageData?.length ? messageData[index + 1] : null; return ( <Message key={item._id} item={item} prevItem={prevItem} className={className} isUserChat={isUserChat} index={index} nextItem={nextItem} /> ); })} </InfiniteScroll> </div> )} </> ); };
解决方案
方法1:利用getBoundingClientRect在滚动事件中计算
直接修改scrollFunc,遍历所有消息元素,判断元素是否在容器视口中,记录最后一个可见的元素索引:
const scrollFunc = (e: any) => { if (!scrollRef.current) return; const container = scrollRef.current; const containerRect = container.getBoundingClientRect(); const messageElements = Array.from(container.children[0].children); let lastVisibleIndex = -1; // 遍历所有消息元素,判断可见性 messageElements.forEach((el, index) => { const elRect = el.getBoundingClientRect(); // 元素与容器视口有交集即判定为可见 const isVisible = elRect.top <= containerRect.bottom && elRect.bottom >= containerRect.top; if (isVisible) { lastVisibleIndex = index; } }); // lastVisibleIndex对应messageData中的索引,可直接获取对应的消息项 if (lastVisibleIndex !== -1) { const lastVisibleItem = messageData[lastVisibleIndex]; console.log("最后一个可见元素:", lastVisibleItem); // 这里可以根据lastVisibleItem的日期更新顶部粘性日期 } };
方法2:使用IntersectionObserver(性能更优)
滚动事件触发频繁,getBoundingClientRect会导致频繁重排,推荐用IntersectionObserver监听元素可见性:
- 给每个
Message组件添加ref:
// 初始化ref数组存储每个Message的DOM节点 const messageRefs = useRef<(HTMLElement | null)[]>([]); // 在map渲染时绑定ref return ( <Message key={item._id} ref={(el) => messageRefs.current[index] = el} item={item} prevItem={prevItem} className={className} isUserChat={isUserChat} index={index} nextItem={nextItem} /> );
- 在
useEffect中初始化观察者,监听元素可见性:
useEffect(() => { if (!scrollRef.current) return; // 存储所有可见的元素索引 const visibleIndices = new Set<number>(); const observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { const elementIndex = messageRefs.current.indexOf(entry.target as HTMLElement); if (elementIndex === -1) return; if (entry.isIntersecting) { visibleIndices.add(elementIndex); } else { visibleIndices.delete(elementIndex); } // 获取最大的索引(即视口中最顶部的可见元素) const lastVisibleIndex = visibleIndices.size > 0 ? Math.max(...visibleIndices) : -1; if (lastVisibleIndex !== -1) { const lastVisibleItem = messageData[lastVisibleIndex]; console.log("最后一个可见元素:", lastVisibleItem); // 更新顶部粘性日期逻辑 } }); }, { root: scrollRef.current, // 监听的根容器 rootMargin: "0px", threshold: 0.1 // 元素有10%可见即判定为可见 } ); // 遍历所有消息元素添加监听 messageRefs.current.forEach(ref => { if (ref) observer.observe(ref); }); // 组件卸载时取消监听 return () => { messageRefs.current.forEach(ref => { if (ref) observer.unobserve(ref); }); }; }, [messageData]);
注意事项
由于容器使用了flex-direction: column-reverse,渲染的元素顺序与messageData数组顺序一致,但页面显示时是从下到上排列的,所以lastVisibleIndex对应的是messageData数组中的索引,直接取对应项即可拿到视口中最顶部的可见消息。
内容的提问来源于stack exchange,提问作者Ayush Kumar
相关产品推荐
相关产品推荐

