[英]How to implement a React infinite scroll component for a chat app like Facebook Messenger?
我知道關於這個主題有很多問題(React 無限滾動),我的問題旨在更深入地確定當前可用的最佳解決方案來實現這樣的組件。
我正在開發一個聊天應用程序,我創建了一個類似於 Facebook Messenger 聊天窗口的組件,您可以在桌面瀏覽器上看到它。
Facebook:
我的(到目前為止):
實現無限加載的無限滾動結果是很棘手的。 從用戶體驗的角度來看,我需要始終至少滿足以下屬性:
現在,為了做到這一點,我已經嘗試了幾個庫:
react-list
組件的錯誤。 此外,該組件不允許我向上顯示滾動底部(參見https://github.com/coderiety/react-list/issues/50 );List
與InfiniteLoader
與AutoSizer
、 CellMeasurer
和CellMeasurerCache
一起使用很棘手。 此外,當我發送消息時,如果我調用List.scrollToIndex(lastIndex)
自動將容器滾動到底部,滾動不會完全到達底部,因為可滾動容器具有頂部和底部填充。 我無法使用此組件獲得令人滿意的結果。所以我的問題更像是一種相互對抗的方式:你們中的某個人是否曾經需要根據我上面寫的 3 個要求來實現一個 React 聊天組件? 你用的什么庫? 由於 Facebook Messenger 處理得很好並且他們使用 React,你們中有人知道他們是如何實現這樣的組件的嗎? 如果我檢查 Facebook 聊天窗口的聊天消息,它似乎將所有已呈現的消息保留在 DOM 中。 但是,如果是這樣,這不會影響性能嗎?
所以我現在的問題多於答案。 我真的很想找到一個適合我需要的組件。 另一種選擇是實現我自己的。
我最終實現了我自己的非常簡單的無限滾動組件(不過還沒有重構它以使用鈎子):
import React from "react";
import {
isUndefined,
hasVerticalScrollbar,
hasHorizontalScrollbar,
isInt,
debounce
} from "js-utl";
import { classNames } from "react-js-utl/utils";
export default class SimpleInfiniteScroll extends React.Component {
constructor(props) {
super(props);
this.handleScroll = this.handleScroll.bind(this);
this.onScrollStop = debounce(this.onScrollStop.bind(this), 100);
this.itemsIdsRefsMap = {};
this.isLoading = false;
this.isScrolling = false;
this.lastScrollStopPromise = null;
this.lastScrollStopPromiseResolve = null;
this.node = React.createRef();
}
componentDidMount() {
this.scrollToStart();
}
getNode() {
return this.node && this.node.current;
}
getSnapshotBeforeUpdate(prevProps) {
if (prevProps.children.length < this.props.children.length) {
const list = this.node.current;
const axis = this.axis();
const scrollDimProperty = this.scrollDimProperty(axis);
const scrollProperty = this.scrollProperty(axis);
const scrollDelta = list[scrollDimProperty] - list[scrollProperty];
return {
scrollDelta
};
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (
this.isLoading &&
((prevProps.isInfiniteLoading && !this.props.isInfiniteLoading) ||
((this.props.hasMore || prevProps.hasMore) &&
prevProps.children.length !==
this.props.children.length)) &&
snapshot
) {
if (this.props.displayInverse) {
const list = this.node.current;
const axis = this.axis();
const scrollDimProperty = this.scrollDimProperty(axis);
const scrollProperty = this.scrollProperty(axis);
const scrollDelta = snapshot.scrollDelta;
const scrollTo = list[scrollDimProperty] - scrollDelta;
this.scrollTo(scrollProperty, scrollTo);
}
this.isLoading = false;
}
}
loadingComponentRenderer() {
const { loadingComponent } = this.props;
return (
<div
className="simple-infinite-scroll-loading-component"
key={-2}
>
{loadingComponent}
</div>
);
}
axis() {
return this.props.axis === "x" ? "x" : "y";
}
scrollProperty(axis) {
return axis === "y" ? "scrollTop" : "scrollLeft";
}
offsetProperty(axis) {
return axis === "y" ? "offsetHeight" : "offsetWidth";
}
clientDimProperty(axis) {
return axis === "y" ? "clientHeight" : "clientWidth";
}
scrollDimProperty(axis) {
return axis === "y" ? "scrollHeight" : "scrollWidth";
}
hasScrollbarFunction(axis) {
return axis === "y" ? hasVerticalScrollbar : hasHorizontalScrollbar;
}
scrollToStart() {
const axis = this.axis();
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ? 0 : this.scrollDimProperty(axis)
);
}
scrollToEnd() {
const axis = this.axis();
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ? this.scrollDimProperty(axis) : 0
);
}
scrollTo(scrollProperty, scrollPositionOrPropertyOfScrollable) {
const scrollableContentNode = this.node.current;
if (scrollableContentNode) {
scrollableContentNode[scrollProperty] = isInt(
scrollPositionOrPropertyOfScrollable
)
? scrollPositionOrPropertyOfScrollable
: scrollableContentNode[scrollPositionOrPropertyOfScrollable];
}
}
scrollToId(id) {
if (this.itemsIdsRefsMap[id] && this.itemsIdsRefsMap[id].current) {
this.itemsIdsRefsMap[id].current.scrollIntoView();
}
}
scrollStopPromise() {
return (
(this.isScrolling && this.lastScrollStopPromise) ||
Promise.resolve()
);
}
onScrollStop(callback) {
callback();
this.isScrolling = false;
this.lastScrollStopPromise = null;
this.lastScrollStopPromiseResolve = null;
}
handleScroll(e) {
const {
isInfiniteLoading,
hasMore,
infiniteLoadBeginEdgeOffset,
displayInverse
} = this.props;
this.isScrolling = true;
this.lastScrollStopPromise =
this.lastScrollStopPromise ||
new Promise(resolve => {
this.lastScrollStopPromiseResolve = resolve;
});
this.onScrollStop(() => {
this.lastScrollStopPromiseResolve &&
this.lastScrollStopPromiseResolve();
});
this.props.onScroll && this.props.onScroll(e);
if (
this.props.onInfiniteLoad &&
(!isUndefined(hasMore) ? hasMore : !isInfiniteLoading) &&
this.node.current &&
!this.isLoading
) {
const axis = this.axis();
const scrollableContentNode = this.node.current;
const scrollProperty = this.scrollProperty(axis);
const offsetProperty = this.offsetProperty(axis);
const scrollDimProperty = this.scrollDimProperty(axis);
const currentScroll = scrollableContentNode[scrollProperty];
const currentDim = scrollableContentNode[offsetProperty];
const scrollDim = scrollableContentNode[scrollDimProperty];
const finalInfiniteLoadBeginEdgeOffset = !isUndefined(
infiniteLoadBeginEdgeOffset
)
? infiniteLoadBeginEdgeOffset
: currentDim / 2;
let thresoldWasReached = false;
if (!displayInverse) {
const clientDimProperty = this.clientDimProperty(axis);
const clientDim = scrollableContentNode[clientDimProperty];
thresoldWasReached =
currentScroll +
clientDim +
finalInfiniteLoadBeginEdgeOffset >=
scrollDim;
} else {
thresoldWasReached =
currentScroll <= finalInfiniteLoadBeginEdgeOffset;
}
if (thresoldWasReached) {
this.isLoading = true;
this.props.onInfiniteLoad();
}
}
}
render() {
const {
children,
displayInverse,
isInfiniteLoading,
className,
hasMore
} = this.props;
return (
<div
className={classNames("simple-infinite-scroll", className)}
ref={this.node}
onScroll={this.handleScroll}
onMouseOver={this.props.onInfiniteScrollMouseOver}
onMouseOut={this.props.onInfiniteScrollMouseOut}
onMouseEnter={this.props.onInfiniteScrollMouseEnter}
onMouseLeave={this.props.onInfiniteScrollMouseLeave}
>
{(hasMore || isInfiniteLoading) &&
displayInverse &&
this.loadingComponentRenderer()}
{children}
{(hasMore || isInfiniteLoading) &&
!displayInverse &&
this.loadingComponentRenderer()}
</div>
);
}
}
在this.props.children
我將以下組件類的 React 元素數組傳遞給它,該類擴展了React.PureComponent
:
...
export default class ChatMessage extends React.PureComponent {
...
}
這樣,在重新渲染時,只會重新渲染自上次渲染以來發生更改的組件。
我還使用了一個不可變的數據結構來存儲聊天消息的集合,特別是immutable-linked-ordered-map
( https://github.com/tonix-tuft/immutable-linked-ordered-map ) 這讓我實現O(1)
用於插入,刪除和消息以及幾乎更新的時間復雜度O(1)
時間復雜度的查找。 本質上, ImmutableLinkedOrderedMap
是一個有序的不可變映射,就像 PHP 中的關聯數組一樣,但不可變:
const map = new ImmutableLinkedOrderedMap({
mode: ImmutableLinkedOrderedMap.MODE.MULTIWAY,
initialItems: [
{
id: 1, // <--- "[keyPropName] === 'id'"
text: "Message text",
// ...
},
{
id: 2,
text: "Another message text",
// ...
},
// ...
]
})
map.get(2) // Will return: { id: 2, text: "Another message text", /* ... */ }
const newMessage = { id: 3, text: "Yet another message text", /* ... */ };
const newMap = map.set(newMessage);
console.log(map !== newMap); // true
console.log(map.length); // 2
console.log(newMap.length); // 3
let messages = newMap.replace(3, newMessage)
console.log(messages === newMap); // true, because newMessage with ID 3 didn't change
messages = newMap.replace(3, { ...newMessage, read: true })
console.log(messages === newMap); // false
然后,當我渲染存儲在地圖中的消息時,我只需調用它的.values()
方法,該方法返回一個數組並映射該數組以渲染消息,例如:
<SimpleInfiniteScroll>
{messages.values().map((message) => <ChatMessage ... />)}
</SimpleInfiniteScroll>
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.