Мы часто создаем веб-приложений, в которых требуется доставить большие наборы записей данных с удаленного сервера, API или базы данных. Например, если вы создаете платежную систему, она может доставлять тысячи транзакций. Если это приложение социальной сети, оно может отображать множество комментариев, профилей и действий пользователей. В любом случае существует несколько способов представления данных в такой форме, которая не перегрузит конечного пользователя приложения.
Один из методов работы с большими наборами данных — это разбивка на страницы. Разбивка на страницы эффективно работает, когда вы заранее знаете размер набора данных (общее количество записей в наборе данных). Вам нужно загрузить только требуемую часть данных из набора в зависимости от взаимодействия конечного пользователя с элементом управления разбивкой на страницы. Эта методика используется для отображения результатов поиска в поисковике Google.
В этом учебном модуле вы научитесь создавать специальный компонент разбивки на страницы с помощью React для разбивки на страницы больших наборов данных. Мы построим разбитое на страницы представление всех стран мира, используя набор данных с известным размером.
Вот демонстрация того, что вы сделаете в этом учебном модуле:
Для данного обучающего руководства вам потребуется следующее:
npm < 5.2
, возможно, вам потребуется установить create-react-app
как глобальную зависимость.Этот учебный модуль был проверен с использованием Node v14.2.0, npm
v6.14.4, react
v16.13.1 и react-scripts
v3.4.1.
Создайте новое приложение React, используя команду create-react-app
. Вы можете назвать приложение как угодно, но в этом учебном модуле мы присвоим ему имя react-pagination
:
- npx create-react-app react-pagination
Затем мы установим зависимости, необходимые для вашего приложения. Для начала используйте окно терминала, чтобы перейти в каталог проекта:
- cd react-pagination
Выполните следующую команду для установки требуемых зависимостей:
- npm install bootstrap@4.1.0 prop-types@15.6.1 react-flags@0.1.13 countries-api@2.0.1 node-sass@4.14.1
При этом будут установлены элементы bootstrap
, prop-types
, react-flags
, countries-api
и node-sass
.
Вы установили пакет bootstrap
как зависимость для вашего приложения, поскольку вам потребуются некоторые стили, доступные по умолчанию. Также вы будете использовать стили компонента Bootstrap pagination
.
Чтобы включить Bootstrap в приложение, отредактируйте файл src/index.js
:
- nano src/index.js
Добавьте следующую строку перед другими выражениями import
:
import "bootstrap/dist/css/bootstrap.min.css";
Теперь в вашем приложении будут доступны стили Bootstrap.
Также вы установили react-flags
как зависимость для вашего приложения. Чтобы получить доступ к иконкам флагов из вашего приложения, вам потребуется скопировать изображения иконок в каталог public
вашего приложения.
Создайте каталог img
в вашем каталоге public
:
- mkdir public/img
Скопируйте файлы изображения из flags
в img
:
- cp -R node_modules/react-flags/vendor/flags public/img
Это обеспечивает создание копии всех изображений react-flag
в вашем приложении.
Теперь мы добавили некоторые зависимости и можем запустить приложение, выполнив следующую команду с npm
в каталоге проекта react-pagination
:
- npm start
Теперь вы запустили приложение и можете начинать разработку. Обратите внимание, что для вас открыта вкладка браузера с функцией перезагрузки в реальном времени, что позволит сохранять синхронизацию с приложением в процессе разработки.
Представление приложения должно выглядеть как на следующем снимке экрана:
Теперь вы готовы начать создание компонентов.
CountryCard
На этом шаге мы создадим компонент CountryCard
. Компонент CountryCard
выполняет рендеринг имени, региона и флага определенной страны.
Для начала создадим каталог components
в каталоге src
:
- mkdir src/components
Затем создадим новый файл CountryCard.js
в каталоге src/components
:
- nano src/components/CountryCard.js
Добавим в него следующий блок кода:
import React from 'react';
import PropTypes from 'prop-types';
import Flag from 'react-flags';
const CountryCard = props => {
const {
cca2: code2 = '', region = null, name = {}
} = props.country || {};
return (
<div className="col-sm-6 col-md-4 country-card">
<div className="country-card-container border-gray rounded border mx-2 my-3 d-flex flex-row align-items-center p-0 bg-light">
<div className="h-100 position-relative border-gray border-right px-2 bg-white rounded-left">
<Flag country={code2} format="png" pngSize={64} basePath="./img/flags" className="d-block h-100" />
</div>
<div className="px-3">
<span className="country-name text-dark d-block font-weight-bold">{ name.common }</span>
<span className="country-region text-secondary text-uppercase">{ region }</span>
</div>
</div>
</div>
)
}
CountryCard.propTypes = {
country: PropTypes.shape({
cca2: PropTypes.string.isRequired,
region: PropTypes.string.isRequired,
name: PropTypes.shape({
common: PropTypes.string.isRequired
}).isRequired
}).isRequired
};
export default CountryCard;
Для компонента CountryCard
требуется объект country
, содержащий данные о стране, которые будут выводиться. Как можно увидеть в списке propTypes
для компонента CountryCard
, объект country
должен содержать следующие данные:
cca2
— 2-значный код страныregion
— регион страны (например, «Африка»)name.common
— общее название страны (например, «Нигерия»)Вот образец объекта страны:
{
cca2: "NG",
region: "Africa",
name: {
common: "Nigeria"
}
}
Обратите внимание на рендеринг флага страны с использованием пакета react-flags
. Вы можете посмотреть документацию react-flags
, чтобы узнать больше о требуемых объектах и использовании пакета.
Теперь вы заполнили отдельный компонент CountryCard
. В конечном итоге вы будете использовать компоненты CountryCard
несколько раз, чтобы выводить в приложении разные флаги и информацию о разных странах.
Pagination
На этом шаге мы создадим компонент Pagination
. Компонент Pagination
содержит логику построения, рендеринга и переключения страниц в элементе управления разбивкой на страницы.
Создайте новый файл Pagination.js
в каталоге src/components
:
- nano src/components/Pagination.js
Добавим в него следующий блок кода:
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
class Pagination extends Component {
constructor(props) {
super(props);
const { totalRecords = null, pageLimit = 30, pageNeighbours = 0 } = props;
this.pageLimit = typeof pageLimit === 'number' ? pageLimit : 30;
this.totalRecords = typeof totalRecords === 'number' ? totalRecords : 0;
// pageNeighbours can be: 0, 1 or 2
this.pageNeighbours = typeof pageNeighbours === 'number'
? Math.max(0, Math.min(pageNeighbours, 2))
: 0;
this.totalPages = Math.ceil(this.totalRecords / this.pageLimit);
this.state = { currentPage: 1 };
}
}
Pagination.propTypes = {
totalRecords: PropTypes.number.isRequired,
pageLimit: PropTypes.number,
pageNeighbours: PropTypes.number,
onPageChanged: PropTypes.func
};
export default Pagination;
Компонент Pagination
может принимать четыре специальных объекта, указанные в объекте propTypes
.
onPageChanged
— это функция, вызываемая с данными по текущему состоянию разбивки на страницы, только в случае изменения текущей страницы.totalRecords
указывает общее количество записей, которое требуется разбить на страницы. Это значение является обязательным.pageLimit
указывает количество отображаемых записей на каждой странице. Если этот параметр не указан, по умолчанию используется значение 30
, определенное в constructor()
.pageNeighbours
указывает количество номеров дополнительных страниц, отображаемое на каждой стороне текущей страницы. Минимальное значение 0
, максимальное значение 2
. Если параметр не определен, по умолчанию используется значение 0
, определенное в constructor()
.На следующем изображении показан эффект различных значений объекта pageNeighbours
:
В функции constructor()
мы рассчитываем общее количество страниц следующим образом:
this.totalPages = Math.ceil(this.totalRecords / this.pageLimit);
Обратите внимание, что здесь мы используем Math.ceil()
, чтобы получить целое значение общего количества страниц. При этом также обеспечивается регистрация лишних записей на последней странице, особенно в случаях, когда количество лишних записей меньше количества записей, отображаемого на странице.
Наконец, вы инициализировали состояние с установленным для свойства currentPage
значением 1
. Это свойство состояния нужно вам для внутреннего отслеживания текущей активной страницы.
Затем вы создадите метод для генерирования номеров страниц.
После import
, но до класса Pagination
нужно добавить следующие константы и функцию range
:
// ...
const LEFT_PAGE = 'LEFT';
const RIGHT_PAGE = 'RIGHT';
/**
* Helper method for creating a range of numbers
* range(1, 5) => [1, 2, 3, 4, 5]
*/
const range = (from, to, step = 1) => {
let i = from;
const range = [];
while (i <= to) {
range.push(i);
i += step;
}
return range;
}
В классе Pagination
после функции constructor
нужно добавить следующий метод fetchPageNumbers
:
class Pagination extends Component {
// ...
/**
* Let's say we have 10 pages and we set pageNeighbours to 2
* Given that the current page is 6
* The pagination control will look like the following:
*
* (1) < {4 5} [6] {7 8} > (10)
*
* (x) => terminal pages: first and last page(always visible)
* [x] => represents current page
* {...x} => represents page neighbours
*/
fetchPageNumbers = () => {
const totalPages = this.totalPages;
const currentPage = this.state.currentPage;
const pageNeighbours = this.pageNeighbours;
/**
* totalNumbers: the total page numbers to show on the control
* totalBlocks: totalNumbers + 2 to cover for the left(<) and right(>) controls
*/
const totalNumbers = (this.pageNeighbours * 2) + 3;
const totalBlocks = totalNumbers + 2;
if (totalPages > totalBlocks) {
const startPage = Math.max(2, currentPage - pageNeighbours);
const endPage = Math.min(totalPages - 1, currentPage + pageNeighbours);
let pages = range(startPage, endPage);
/**
* hasLeftSpill: has hidden pages to the left
* hasRightSpill: has hidden pages to the right
* spillOffset: number of hidden pages either to the left or to the right
*/
const hasLeftSpill = startPage > 2;
const hasRightSpill = (totalPages - endPage) > 1;
const spillOffset = totalNumbers - (pages.length + 1);
switch (true) {
// handle: (1) < {5 6} [7] {8 9} (10)
case (hasLeftSpill && !hasRightSpill): {
const extraPages = range(startPage - spillOffset, startPage - 1);
pages = [LEFT_PAGE, ...extraPages, ...pages];
break;
}
// handle: (1) {2 3} [4] {5 6} > (10)
case (!hasLeftSpill && hasRightSpill): {
const extraPages = range(endPage + 1, endPage + spillOffset);
pages = [...pages, ...extraPages, RIGHT_PAGE];
break;
}
// handle: (1) < {4 5} [6] {7 8} > (10)
case (hasLeftSpill && hasRightSpill):
default: {
pages = [LEFT_PAGE, ...pages, RIGHT_PAGE];
break;
}
}
return [1, ...pages, totalPages];
}
return range(1, totalPages);
}
}
Вначале вы определите две константы: LEFT_PAGE
и RIGHT_PAGE
. Эти константы будут использоваться для указания точек расположения элементов управления для перехода на следующую страницу слева и справа соответственно.
Также вы определили вспомогательную функцию range()
, которая поможет вам генерировать диапазоны чисел.
Примечание. Если вы используете в проекте библиотеку утилит, например, Lodash, вы можете использовать функцию _.range()
, предоставляемую Lodash. В следующем блоке кода показаны отличия между функцией range()
, которую мы только что определили, и функцией от Lodash:
range(1, 5); // returns [1, 2, 3, 4, 5]
_.range(1, 5); // returns [1, 2, 3, 4]
Далее мы определили метод fetchPageNumbers()
в классе Pagination
. Этот метод отвечает за выполнение базовой логики генерирования номеров страниц для отображения элементом управления разбивкой на страницы. Нам нужно, чтобы первая и последняя страницы всегда были видны.
Вначале мы определили две переменные. totalNumbers
представляет общее количество страниц, показываемое на элементе управления. totalBlocks
показывает общее количество страниц плюс два дополнительных блока для индикаторов страниц слева и справа.
Если значение totalPages
не больше, чем totalBlocks
, возвращается диапазон чисел от 1
до totalPages
. В ином случае возвращается массив номеров страниц с LEFT_PAGE
и RIGHT_PAGE
в точках, где можно перейти на страницу слева или справа.
Обратите внимание, что элемент управления разбивкой на страницы обеспечивает постоянную видимость первой и последней страниц. Элементы управления переходом на левую и правую страницы отображаются внутри.
Теперь мы добавим метод render()
, чтобы выполнить рендеринг элемента управления разбивкой на страницы.
В классе Pagination
после функции constructor
и метода fetchPageNumbers
нужно добавить следующий метод render
:
class Pagination extends Component {
// ...
render() {
if (!this.totalRecords || this.totalPages === 1) return null;
const { currentPage } = this.state;
const pages = this.fetchPageNumbers();
return (
<Fragment>
<nav aria-label="Countries Pagination">
<ul className="pagination">
{ pages.map((page, index) => {
if (page === LEFT_PAGE) return (
<li key={index} className="page-item">
<a className="page-link" href="#" aria-label="Previous" onClick={this.handleMoveLeft}>
<span aria-hidden="true">«</span>
<span className="sr-only">Previous</span>
</a>
</li>
);
if (page === RIGHT_PAGE) return (
<li key={index} className="page-item">
<a className="page-link" href="#" aria-label="Next" onClick={this.handleMoveRight}>
<span aria-hidden="true">»</span>
<span className="sr-only">Next</span>
</a>
</li>
);
return (
<li key={index} className={`page-item${ currentPage === page ? ' active' : ''}`}>
<a className="page-link" href="#" onClick={ this.handleClick(page) }>{ page }</a>
</li>
);
}) }
</ul>
</nav>
</Fragment>
);
}
}
Здесь мы генерируем массив
номеров страниц, вызывая метод fetchPageNumbers()
, который мы создали ранее. Затем выполняется рендеринг каждого номера страницы с использованием Array.prototype.map()
. Обратите внимание, что необходимо регистрировать обработчики событий нажатия для каждого отображаемого номера страницы, чтобы обеспечить обработку нажатий.
Также следует отметить, что элемент управления разбивкой на страницы не отображается, если объект totalRecords
не был передан в компонент Pagination
надлежащим образом или в случаях, когда имеется только 1
страница.
В заключение мы определим методы обработчика событий.
Добавьте следующее в классе Pagination
после функции constructor
, метода fetchPageNumbers
и метода render
:
class Pagination extends Component {
// ...
componentDidMount() {
this.gotoPage(1);
}
gotoPage = page => {
const { onPageChanged = f => f } = this.props;
const currentPage = Math.max(0, Math.min(page, this.totalPages));
const paginationData = {
currentPage,
totalPages: this.totalPages,
pageLimit: this.pageLimit,
totalRecords: this.totalRecords
};
this.setState({ currentPage }, () => onPageChanged(paginationData));
}
handleClick = page => evt => {
evt.preventDefault();
this.gotoPage(page);
}
handleMoveLeft = evt => {
evt.preventDefault();
this.gotoPage(this.state.currentPage - (this.pageNeighbours * 2) - 1);
}
handleMoveRight = evt => {
evt.preventDefault();
this.gotoPage(this.state.currentPage + (this.pageNeighbours * 2) + 1);
}
}
Вы определили метод gotoPage()
, который изменяет состояние и устанавливает указанную страницу как currentPage
. Он гарантирует, что аргумент page
имеет значение не менее 1
и не более общего количества страниц. В завершение он вызывает функцию onPageChanged()
, которая была передана как объект, с данными, указывающими новое состояние разбивки на страницы.
При монтировании компонента мы переходим на первую страницу, вызывая this.gotoPage(1)
, как показано в методе жизненного цикла componentDidMount()
.
Обратите внимание на использовании (this.pageNeighbours * 2)
в handleMoveLeft()
и handleMoveRight()
для прокрутки номеров страниц влево и вправо соответственно в зависимости от текущего номера страницы.
Вот демонстрация взаимодействия при движении слева направо.
Мы закончили работу над компонентом Pagination
. Теперь пользователи смогут использовать элементы навигации этого компонента для отображения разных страниц с флагами.
App
Обратите внимание, что теперь у нас имеются компоненты CountryCard
и Pagination
, и мы можем использовать их в нашем компоненте App
.
Измените файл App.js
в каталоге src
:
- nano src/App.js
Замените содержимое файла App.js
следующими строками кода:
import React, { Component } from 'react';
import Countries from 'countries-api';
import './App.css';
import Pagination from './components/Pagination';
import CountryCard from './components/CountryCard';
class App extends Component {
state = { allCountries: [], currentCountries: [], currentPage: null, totalPages: null }
componentDidMount() {
const { data: allCountries = [] } = Countries.findAll();
this.setState({ allCountries });
}
onPageChanged = data => {
const { allCountries } = this.state;
const { currentPage, totalPages, pageLimit } = data;
const offset = (currentPage - 1) * pageLimit;
const currentCountries = allCountries.slice(offset, offset + pageLimit);
this.setState({ currentPage, currentCountries, totalPages });
}
}
export default App;
Здесь мы инициализируем состояние компонента App
, используя следующие атрибуты:
allCountries
— это массив всех стран в вашем приложении. Инициализируется как пустой массив ([]
).currentCountries
— это массив всех стран, отображаемых на активной странице. Инициализируется как пустой массив ([]
).currentPage
— номер активной страницы. Инициализируется как null
.totalPages
— общее количество страниц со всеми записями стран. Инициализируется как null
.Затем в методе жизненного цикла componentDidMount()
мы доставляем все страны мира, используя пакет countries-api
посредством вызова Countries.findAll()
. Затем мы обновляем состояние приложения, устанавливая все страны мира как содержимое allCountries
. Вы можете посмотреть [документацию по countries-api
], countries-apiчтобы узнать больше о пакете.
В заключение мы определили метод onPageChanged()
, который будет вызываться при каждом переходе на новую страницу из элемента управления разбивкой на страницы. Этот метод будет передаваться в объект onPageChanged
компонента Pagination
.
При использовании этого метода следует обратить внимание на две строки. Вот первая из этих строк:
const offset = (currentPage - 1) * pageLimit;
Значение offset
указывает на начальный индекс для доставки записей для текущей страницы. Благодаря использованию (currentPage - 1)
коррекция основана на нулевом значении. Допустим, мы отображаем 25
записей на каждой странице, и вы просматриваете страницу 5
. Тогда значение коррекции
будет ((5 - 1) * 25 = 100)
.
Например, если вы доставляете записи по запросу из базы данных, этот образец запроса SQL покажет вам, как использовать данную коррекцию:
SELECT * FROM `countries` LIMIT 100, 25
Поскольку мы не доставляем записи из базы данных или внешнего источника, нам нужен способ извлечь требуемое количество записей для отображения на текущей странице.
Вторая строка выглядит так:
const currentCountries = allCountries.slice(offset, offset + pageLimit);
Здесь мы использовали метод Array.prototype.slice()
для извлечения требуемого блока записей из массива allCountries
, передавая offset
как указатель начала блока и (offset + pageLimit)
как указатель конца блока.
Примечание. В этом учебном модуле мы не доставляли записи из внешнего источника. В реальном приложении записи обычно доставляются из базы данных или через API. Логику доставки записей можно разместить в методе onPageChanged()
компонента App
.
Допустим, вы используете вымышленную конечную точку API /api/countries?page={current_page}&limit={page_limit}
. В следующем блоке кода показано, как доставлять страны по запросу из API, используя пакет axios
HTTP:
onPageChanged = data => {
const { currentPage, totalPages, pageLimit } = data;
axios.get(`/api/countries?page=${currentPage}&limit=${pageLimit}`)
.then(response => {
const currentCountries = response.data.countries;
this.setState({ currentPage, currentCountries, totalPages });
});
}
Теперь вы можете закончить компонент App
, добавив метод render()
.
В классе App
после componentDidMount
и onPageChanged
нужно добавить следующий метод render
:
class App extends Component {
// ... other methods here ...
render() {
const { allCountries, currentCountries, currentPage, totalPages } = this.state;
const totalCountries = allCountries.length;
if (totalCountries === 0) return null;
const headerClass = ['text-dark py-2 pr-4 m-0', currentPage ? 'border-gray border-right' : ''].join(' ').trim();
return (
<div className="container mb-5">
<div className="row d-flex flex-row py-5">
<div className="w-100 px-4 py-5 d-flex flex-row flex-wrap align-items-center justify-content-between">
<div className="d-flex flex-row align-items-center">
<h2 className={headerClass}>
<strong className="text-secondary">{totalCountries}</strong> Countries
</h2>
{ currentPage && (
<span className="current-page d-inline-block h-100 pl-4 text-secondary">
Page <span className="font-weight-bold">{ currentPage }</span> / <span className="font-weight-bold">{ totalPages }</span>
</span>
) }
</div>
<div className="d-flex flex-row py-4 align-items-center">
<Pagination totalRecords={totalCountries} pageLimit={18} pageNeighbours={1} onPageChanged={this.onPageChanged} />
</div>
</div>
{ currentCountries.map(country => <CountryCard key={country.cca3} country={country} />) }
</div>
</div>
);
}
}
В методе render()
мы выполняем рендеринг общего количества стран, текущей страницы, общего количества страниц, элемента управления <Pagination>
и <CountryCard>
для каждой страны на текущей странице.
Обратите внимание, что вы передали ранее определенный метод onPageChanged()
в объект onPageChanged
элемента управления <Pagination>
. Это очень важно для регистрации изменений страниц из компонента Pagination
. Мы выводим 18
стран на одной странице.
Сейчас приложение выглядит, как показано на следующем снимке экрана:
Теперь у нас имеется компонент App
, позволяющий отображать различные компоненты CountryCard
и Pagination
, разделяющие содержимое на отдельные страницы. Далее мы рассмотрим применение стилей к нашему приложению.
Возможно вы заметили, что мы добавляли определенные специальные классы в ранее созданные компоненты. Давайте определим некоторые правила стилей для этих классов в файле src/App.scss
.
- nano src/App.scss
Файл App.scss
будет выглядеть, как следующий фрагмент кода:
/* Declare some variables */
$base-color: #ced4da;
$light-background: lighten(desaturate($base-color, 50%), 12.5%);
.current-page {
font-size: 1.5rem;
vertical-align: middle;
}
.country-card-container {
height: 60px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.country-name {
font-size: 0.9rem;
}
.country-region {
font-size: 0.7rem;
}
.current-page,
.country-name,
.country-region {
line-height: 1;
}
// Override some Bootstrap pagination styles
ul.pagination {
margin-top: 0;
margin-bottom: 0;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
li.page-item.active {
a.page-link {
color: saturate(darken($base-color, 50%), 5%) !important;
background-color: saturate(lighten($base-color, 7.5%), 2.5%) !important;
border-color: $base-color !important;
}
}
a.page-link {
padding: 0.75rem 1rem;
min-width: 3.5rem;
text-align: center;
box-shadow: none !important;
border-color: $base-color !important;
color: saturate(darken($base-color, 30%), 10%);
font-weight: 900;
font-size: 1rem;
&:hover {
background-color: $light-background;
}
}
}
Измените файл App.js
так, чтобы он ссылался на App.scss
, а не на App.css
.
Примечание. Для получения дополнительной информации ознакомьтесь с документацией по Create React App.
- nano src/App.js
import React, { Component } from 'react';
import Countries from 'countries-api';
import './App.scss';
import Pagination from './components/Pagination';
import CountryCard from './components/CountryCard';
После добавления стилей приложение будет выглядеть, как показано на следующем снимке экрана:
Теперь у вас имеется полное приложение с дополнительными индивидуальными стилями. Вы можете использовать индивидуальные стили для изменения или расширения любых стилей по умолчанию, предоставляемых Bootstrap и другими библиотеками.
В этом учебном модуле мы создали специальный виджет разбивки на страницы в приложении React. Хотя в этом учебном модуле мы не вызывали API и не взаимодействовали с сервером баз данных, для вашего приложения может потребоваться такое взаимодействие. Описанный в этом учебном модуле подход не ограничивает ваши возможности, вы можете расширять его как угодно, чтобы решить стоящие перед вашим приложением задачи.
Полный исходный код этого учебного модуля можно найти в репозитории build-react-pagination-demo на GitHub. Также вы можете посмотреть работающую демонстрацию этого учебного модуля на Code Sandbox.
Если вы хотите узнать больше о React, почитайте нашу серию «Программирование на React.js» или посмотрите страницу тем React, где вы найдете упражнения и программные проекты.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!