如何在 React 中实现无限滚动

介绍

无限滚动是当用户到达页面的底部,新内容被收集和加载,以便用户可以继续滚动,以实现相对无缝的体验。

你可能在像Instagram这样的应用程序中遇到无限的滚动,你被呈现了一个图像的流量,当你滚动时,更多的图像继续出现。

在本教程中,您将触及允许无限滚动工作的两个关键概念 - 检测用户何时到达页面底部,并加载下一批内容来显示。

前提条件

要完成本教程,您将需要:

本教程已通过 Node v14.12.0, npm v6.14.8, react v16.13.1, superagent v6.1.0 和 lodash.debounce v2.7.1 进行验证。

步骤1 - 设置项目

开始使用 create-react-app来生成 React App,然后安装依赖:

1npx create-react-app react-infinite-scroll-example

更改到新项目目录:

1cd react-infinite-scroll-example

要从 APOD API 上加载数据,您将使用 superagent

當你使用它時,你會使用它(LINK0)。

要通过npm运行将superagentlodash.debounce添加到您的项目:

现在,您可以运行 React 应用程序:

1npm start

修复您的项目中的任何错误或问题,并在网页浏览器中访问localhost:3000

一旦你有一个工作 React 应用程序,你可以开始建立你的无限滚动功能。

步骤 2 – 实施滚动下载Apods

无限滚动将需要两个关键部分. 一部分将是检查窗口滚动位置和窗口的高度,以确定用户是否已经到达页面的底部。

让我们先创建一个InfiniteSpace.js文件:

1nano src/InfiniteSpace.js

构建你的无限空间组件:

 1[label src/InfiniteSpace.js]
 2import React from 'react';
 3import request from 'superagent';
 4import debounce from 'lodash.debounce';
 5
 6class InfiniteSpace extends React.Component {
 7  constructor(props) {
 8    super(props);
 9
10    this.state = {
11      apods: [],
12    };
13  }
14
15  render() {
16    return (
17      <div>
18        <h1>Infinite Space!</h1>
19        <p>Scroll down to load more!!</p>
20      </div>
21    )
22  }
23}
24
25export default InfiniteSpace;

无限滚动组件的关键部分将是一个滚动事件,它会检查用户是否滚动到页面的底部。

当约束事件时,特别是滚动事件时,最好是 debounce 事件,而Debouncing 是当您只运行一个函数,因为它上次被调用已经过去了一定的时间。

Debouncing 通过限制事件的频率来提高用户的性能,并有助于从事件处理器上调用任何服务。

 1[label src/InfiniteSpace.js]
 2class InfiniteSpace extends Component {
 3  constructor(props) {
 4    super(props);
 5
 6    this.state = {
 7      apods: [],
 8    };
 9
10    window.onscroll = debounce(() => {
11      const {
12        loadApods
13      } = this;
14
15      if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
16        loadApods();
17      }
18    }, 100);
19  }
20
21  // ...
22}

此代码建立了 100 毫秒的退出迭代。

loadApods函数将使用superagent请求GET当天的天文图像:

 1[label src/InfiniteSpace.js]
 2class InfiniteSpace extends Component {
 3  constructor(props) {
 4    // ...
 5  }
 6
 7  dayOffset = () => {
 8    let today = new Date();
 9    let day = today.setDate(-1 * this.state.apods.length);
10    return new Date(day).toISOString().split('T')[0];
11  }
12
13  loadApods = () => {
14    request
15      .get('https://api.nasa.gov/planetary/apod?date=' + this.dayOffset() + '&api_key=DEMO_KEY')
16      .then((results) => {
17        const nextApod = {
18          date: results.body.date,
19          title: results.body.title,
20          explanation: results.body.explanation,
21          copyright: results.body.copyright,
22          media_type: results.body.media_type,
23          url: results.body.url
24        };
25
26        this.setState({
27          apods: [
28            ...this.state.apods,
29            nextApod
30          ]
31        });
32      });
33  }
34
35  render() {
36    // ...
37  }
38}

dayOffset函数将用于计算前一天的天文图像。

此代码将对 APOD 的响应进行映射,以存储日期,标题,解释,版权,media_typeurl的值。

已加载的数据将附加到组件状态的数组中,并在组件的渲染方法中进行迭代。

要验证你的两块工作在一起,让我们返回答案:

 1class InfiniteSpace extends Component {
 2  // ...
 3
 4  render() {
 5    return(
 6      <div>
 7        <h1>Infinite Space!</h1>
 8        <p>Scroll down to load more!!</p>
 9
10        {apods.map(apod => (
11          <React.Fragment key={apod.date}>
12            <hr />
13            <div>
14              <h2>{apod.title}</h2>
15              {apod.media_type === 'image' &&
16                <img
17                  alt={`NASA APOD for {apod.date}`}
18                  src={apod.url}
19                  style={{
20                    maxWidth: '100%',
21                    height: 'auto'
22                  }}
23                />
24              }
25              {apod.media_type === 'video' &&
26                <iframe
27                  src={apod.url}
28                  width='640'
29                  height='360'
30                  style={{
31                    maxWidth: '100%'
32                  }}
33                ></iframe>
34              }
35              <div>{apod.explanation}</div>
36              <div>{apod.copyright}</div>
37            </div>
38          </React.Fragment>
39        ))}
40
41        <hr />
42      </div>
43    );
44  }
45}

此代码将显示imgiframe,取决于 APOD 的media_type

在此时,您可以修改您的应用组件以导入InfiniteSpace

1nano src/App.js

并用无限空间组件替换Create React App 生成的内容:

 1[label src/App.js]
 2import React from 'react';
 3import InfiniteSpace from './InfiniteSpace';
 4
 5function App() {
 6  return (
 7    <div className="App">
 8      <InfiniteSpace />
 9    </div>
10  );
11}
12
13export default App;

此时,您可以重新运行您的应用程序:

1npm start

修复您的项目中的任何错误或问题,并在网页浏览器中访问localhost:3000

如果您向下滚动网页的高度,您将触发滚动事件的条件,启动加载Apods,并在屏幕上显示一个新的APOD。

有了这两个无限滚动的部件,你已经建立了大部分的无限空间组件. 添加初始负载和错误处理将有助于使其更坚固。

步骤 3 – 添加初始负载和错误处理

目前,InfiniteSpace 在满足滚动事件的条件之前不会加载任何 APOD. 还存在三种情况,您不希望加载 APOD:如果没有更多的 APOD 可加载,如果您目前正在加载 APOD,如果您遇到错误。

首先,重新浏览InfiniteSpace.js:

1nano src/InfiniteSpace.js

然后,使用componentDidMount()进行初始加载:

 1[label src/InfiniteSpace.js]
 2class InfiniteSpace extends Component {
 3  constructor(props) {
 4    // ...
 5  }
 6
 7  componentDidMount() {
 8    this.loadApods();
 9  }
10
11  dayOffset = () => {
12    // ...
13  }
14
15  loadApods = () => {
16    // ...
17  }
18
19  render() {
20    // ...
21  }
22}

错误hasMoreisLoading添加到状态中,以解决错误并限制不必要的加载:

 1[label src/InfiniteSpace.js]
 2class InfiniteSpace extends Component {
 3  constructor(props) {
 4    super(props);
 5
 6    this.state = {
 7      error: false,
 8      hasMore: true,
 9      isLoading: false,
10      apods: []
11    };
12
13    // ...
14  }
15
16  // ...
17}

Error 最初设置为 false; hasMore 最初设置为 true; isLoading 最初设置为 false

然后,应用 stat 到onscroll:

 1[label src/InfiniteSpace.js]
 2class InfiniteSpace extends Component {
 3  constructor(props) {
 4    super(props);
 5
 6    this.state = {
 7      error: false,
 8      hasMore: true,
 9      isLoading: false,
10      apods: []
11    };
12
13    window.onscroll = debounce(() => {
14      const {
15        loadApods,
16        state: {
17          error,
18          isLoading,
19          hasMore
20        }
21      } = this;
22
23      if (error || isLoading || !hasMore) return;
24
25      if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
26        loadApods();
27      }
28    }, 100);
29  }
30
31  // ...
32}

此检查将提前进行,并防止在存在错误的情况下调用loadApods,它目前正在加载,或者没有额外的APOD来加载。

然后,将状态应用到loadApods:

 1[label src/InfiniteSpace.js]
 2class InfiniteSpace extends Component {
 3  // ...
 4
 5  loadApods = () => { this.setState({ isLoading: true }, () => {
 6    request
 7      .get('https://api.nasa.gov/planetary/apod?date=' + this.dayOffset() + '&api_key=DEMO_KEY')
 8      .then((results) => {
 9        const nextApod = {
10          date: results.body.date,
11          title: results.body.title,
12          explanation: results.body.explanation,
13          copyright: results.body.copyright,
14          media_type: results.body.media_type,
15          url: results.body.url
16        };
17
18        this.setState({
19          hasMore: (this.state.apods.length < 5),
20          isLoading: false,
21          apods: [
22            ...this.state.apods,
23            nextApod
24          ],
25        });
26      })
27      .catch((err) => {
28        this.setState({
29          error: err.message,
30          isLoading: false
31          });
32      });
33    });
34  }
35
36  // ...
37}

此代码使用 setState with a callback function 作为第二个参数传入。在loadApods 方法中,初始调用setStateisLoading的值设置为true,然后在回调函数中,下一个 APOD 被加载,然后setState再次调用以将isLoading设置为false

对于本教程的目的,hasMore是一个布尔式检查来限制APOD的数量为5。在不同的场景中,API可能返回某些值作为有效负载的一部分,该值表明是否有更多内容要加载。

如果loadApods遇到错误,则errorcatch块中设置为err.message

然后,应用 stat 到render:

 1[label src/InfiniteSpace.js]
 2class InfiniteSpace extends Component {
 3  // ...
 4
 5  render() {
 6    const {
 7      error,
 8      hasMore,
 9      isLoading,
10      apods
11    } = this.state;
12
13    return (
14      <div>
15        {/* ... React.Fragment ... */}
16
17        {error &&
18          <div style={{ color: '#900' }}>
19            {error}
20          </div>
21        }
22
23        {isLoading &&
24          <div>Loading...</div>
25        }
26
27        {!hasMore &&
28          <div>Loading Complete</div>
29        }
30      </div>
31    );
32  }
33]

这将显示错误isLoadinghasMore的消息。

当所有零件都聚在一起时,无限空间将看起来像这样:

  1[label src/InfiniteSpace.js]
  2import React from 'react';
  3import request from 'superagent';
  4import debounce from 'lodash.debounce';
  5
  6class InfiniteSpace extends React.Component {
  7  constructor(props) {
  8    super(props);
  9
 10    this.state = {
 11      error: false,
 12      hasMore: true,
 13      isLoading: false,
 14      apods: []
 15    };
 16
 17    window.onscroll = debounce(() => {
 18      const {
 19        loadApods,
 20        state: {
 21          error,
 22          isLoading,
 23          hasMore,
 24        },
 25      } = this;
 26
 27      if (error || isLoading || !hasMore) return;
 28
 29      if (window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight) {
 30        loadApods();
 31      }
 32    }, 100);
 33  }
 34
 35  componentDidMount() {
 36    this.loadApods();
 37  }
 38
 39  dayOffset = () => {
 40    let today = new Date();
 41    let day = today.setDate(-1 * this.state.apods.length);
 42    return new Date(day).toISOString().split('T')[0];
 43  }
 44
 45  loadApods = () => {this.setState({ isLoading: true }, () => {
 46    request
 47      .get('https://api.nasa.gov/planetary/apod?date=' + this.dayOffset() + '&api_key=DEMO_KEY')
 48      .then((results) => {
 49        const nextApod = {
 50          date: results.body.date,
 51          title: results.body.title,
 52          explanation: results.body.explanation,
 53          copyright: results.body.copyright,
 54          media_type: results.body.media_type,
 55          url: results.body.url
 56        };
 57
 58        this.setState({
 59          hasMore: (this.state.apods.length < 5),
 60          isLoading: false,
 61          apods: [
 62            ...this.state.apods,
 63            nextApod
 64          ],
 65        });
 66      })
 67      .catch((err) => {
 68        this.setState({
 69          error: err.message,
 70          isLoading: false
 71        });
 72      });
 73    });
 74  }
 75
 76  render() {
 77    const {
 78      error,
 79      hasMore,
 80      isLoading,
 81      apods
 82    } = this.state;
 83
 84    return (
 85      <div style={{
 86        padding: 10
 87      }}>
 88        <h1>Infinite Space!</h1>
 89        <p>Scroll down to load more!!</p>
 90
 91        {apods.map(apod => (
 92          <React.Fragment key={apod.date}>
 93            <hr />
 94            <div>
 95              <h2>{apod.title}</h2>
 96              {apod.media_type === 'image' &&
 97                <img
 98                  alt={`NASA APOD for {apod.date}`}
 99                  src={apod.url}
100                  style={{
101                    maxWidth: '100%',
102                    height: 'auto'
103                  }}
104                />
105              }
106              {apod.media_type === 'video' &&
107                <iframe
108                  src={apod.url}
109                  width='640'
110                  height='360'
111                  style={{
112                    maxWidth: '100%'
113                  }}
114                ></iframe>
115              }
116              <div>{apod.explanation}</div>
117              <div>{apod.copyright}</div>
118            </div>
119          </React.Fragment>
120        ))}
121
122        <hr />
123
124        {error &&
125          <div style={{ color: '#900' }}>
126            {error}
127          </div>
128        }
129
130        {isLoading &&
131          <div>Loading...</div>
132        }
133
134        {!hasMore &&
135          <div>Loading Complete</div>
136        }
137      </div>
138    );
139  }
140}
141
142export default InfiniteSpace;

最后,再次运行您的应用程序:

1npm start

修复您的项目中的任何错误或问题,并在网页浏览器中访问localhost:3000

向下滚动,您的应用程序将收集并显示5个APOD。

结论

在本教程中,您在React应用程序中实施了无限滚动,无限滚动是解决潜在问题的一个现代解决方案,可以向最终用户提供大量信息,而无需大量的初始加载时间。

如果您的项目包含您希望用户访问的页面底部的内容,无限滚动可能会导致用户体验更差。

还有其他提供此功能的库,可能最适合您项目的需求。

如果您想了解更多关于 React 的信息,请查看我们的 如何在 React.js 中编码系列,或查看 我们的 React 主题页面以获取练习和编程项目。

Published At
Categories with 技术
Tagged with
comments powered by Disqus