Если вы используете веб-сайты поиска изображений, такие как Google Image Search или Flickr, вы заметите, что их изображения отображаются в виде сетки, которая выглядит как стена из кирпичей. Изображения неравномерны по высоте, но равны по ширине. Это называется эффектом кладки, потому что выглядит как стена из кирпича.

Чтобы реализовать эффект каменной кладки, мы должны установить ширину изображения, пропорциональную ширине экрана, и установить высоту изображения, пропорциональную соотношению сторон изображения.

Это больно делать, если это делается без каких-либо библиотек, поэтому люди создали библиотеки для создания этого эффекта.

В этой статье мы создадим приложение для фотографий, которое позволит пользователям искать изображения и отображать изображения в сетке каменной кладки. Сетка изображений будет иметь бесконечную прокрутку для получения дополнительных изображений. Мы построим его с помощью React и библиотеки React Masonry Component. Для бесконечной прокрутки мы будем использовать библиотеку React Infinite Scroller. Мы обернем React Infinite Scroller за пределы компонента React Masonry, чтобы получить бесконечную прокрутку с эффектом каменной кладки при отображении изображений.

Наше приложение будет отображать изображения из API Pixabay. Вы можете просмотреть документацию по API и зарегистрироваться для получения ключа на странице https://pixabay.com/api/docs/

Для начала мы запускаем Create React App, чтобы создать приложение. Запустите npx create-react-app photo-app, чтобы создать начальный код для приложения.

Затем мы устанавливаем собственные библиотеки. Нам нужны React Infinite Scroller, React Masonry Component, Bootstrap для стилизации, Axios для выполнения HTTP-запросов, Formik и Yup для привязки данных значения формы и проверки формы, а также React Router для маршрутизации URL-адресов на наши страницы.

Чтобы установить все пакеты, запустите:

npm i axios bootstrap formik react-bootstrap react-infinite-scroller react-masonry-component react-router-dom yup

для установки всех пакетов.

Установив все пакеты, мы можем приступить к созданию приложения. Сначала начните с замены кода в App.js на:

import React from "react";
import { Router, Route } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import TopBar from "./TopBar";
import ImageSearchPage from "./ImageSearchPage";
import "./App.css";
const history = createHistory();
function App() {
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route path="/" exact component={HomePage} />
        <Route path="/imagesearch" exact component={ImageSearchPage} />
      </Router>
    </div>
  );
}
export default App;

чтобы добавить верхнюю панель и маршруты для нашего приложения в точку входа в приложение.

Затем удалите весь код в App.css и добавьте:

.page {
  padding: 20px;
}

чтобы добавить отступы на наши страницы.

Затем мы устанавливаем параметры нашего компонента React Masonry, создавая exports.js в папке src и добавляя:

export const masonryOptions = {
  fitWidth: true,
  columnWidth: 300,
  gutter: 5
};

Эти параметры очень важны. Нам нужно установить fitWidth на true, чтобы центрировать нашу сетку. columnWidth должен быть числом, чтобы получить постоянную ширину. Он будет масштабироваться в соответствии с размером экрана только с постоянной шириной. Значение gutter - это разница между элементами.

Полный список опций находится на https://masonry.desandro.com/options.html.

Затем мы создаем домашнюю страницу нашего приложения, создав HomePage.js в папке src и добавляя:

import React from "react";
import { getImages } from "./request";
import InfiniteScroll from "react-infinite-scroller";
import Masonry from "react-masonry-component";
import "./HomePage.css";
import { masonryOptions } from "./exports";
function HomePage() {
  const [images, setImages] = React.useState([]);
  const [page, setPage] = React.useState(1);
  const [total, setTotal] = React.useState(0);
  const [initialized, setInitialized] = React.useState(false);
  const getAllImages = async (pg = 1) => {
    const response = await getImages(page);
    let imgs = images.concat(response.data.hits);
    setImages(imgs);
    setTotal(response.data.total);
    pg++;
    setPage(pg);
  };
  React.useEffect(() => {
    if (!initialized) {
      getAllImages();
      setInitialized(true);
    }
  });
  return (
    <div className="page">
      <h1 className="text-center">Home</h1>
      <InfiniteScroll
        pageStart={1}
        loadMore={getAllImages}
        hasMore={total > images.length}
      >
        <Masonry
          className={"grid"}
          elementType={"div"}
          options={masonryOptions}
          disableImagesLoaded={false}
          updateOnEachImageLoad={false}
        >
          {images.map((img, i) => {
            return (
              <div key={i}>
                <img src={img.previewURL} style={{ width: 300 }} />
              </div>
            );
          })}
        </Masonry>
      </InfiniteScroll>
    </div>
  );
}
export default HomePage;

На домашней странице мы просто получаем изображения при загрузке страницы. Когда пользователь прокручивает страницу вниз, мы загружаем больше изображений, добавляя 1 к значению currentpage, и получаем изображение с номером страницы.

С компонентом InfiniteScroll, который предоставляется React Infinite Scroll, заключенным за пределы компонента Masonry, который предоставляется React Masonry Component, мы отображаем наши изображения в сетке, а также отображаем больше, когда пользователь прокручивает вниз до length из images массив больше или равен total, который взят из поля total, указанного в результатах Pixabay API.

Мы загружаем изображения при загрузке страницы, проверяя, установлен ли флаг initialized на true, мы загружаем изображения только при загрузке страницы, если initialized равно false, а когда запрос сначала выполняется к API и завершается успешно, мы устанавливаем флаг initialized на true, чтобы остановить запросы. на каждом рендере.

Затем мы создаем страницу поиска изображений, создав файл ImageSearchPage.js и добавив следующее:

import React from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import InfiniteScroll from "react-infinite-scroller";
import Masonry from "react-masonry-component";
import { masonryOptions } from "./exports";
import { searchImages } from "./request";
const schema = yup.object({
  keyword: yup.string().required("Keyword is required")
});
function ImageSearchPage() {
  const [images, setImages] = React.useState([]);
  const [keyword, setKeyword] = React.useState([]);
  const [page, setPage] = React.useState(1);
  const [total, setTotal] = React.useState(0);
  const [searching, setSearching] = React.useState(false);
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    setKeyword(evt.keyword);
    searchAllImages(evt.keyword, 1);
  };
  const searchAllImages = async (keyword, pg = 1) => {
    setSearching(true);
  const response = await searchImages(keyword, page);
    let imgs = response.data.hits;
    setImages(imgs);
    setTotal(response.data.total);
    setPage(pg);
  };
  const getMoreImages = async () => {
    let pg = page;
    pg++;
    const response = await searchImages(keyword, pg);
    const imgs = images.concat(response.data.hits);
    setImages(imgs);
    setTotal(response.data.total);
    setPage(pg);
  };
  React.useEffect(() => {});
  return (
    <div className="page">
      <h1 className="text-center">Search</h1>
      <Formik validationSchema={schema} onSubmit={handleSubmit}>
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="keyword">
                <Form.Label>Keyword</Form.Label>
                <Form.Control
                  type="text"
                  name="keyword"
                  placeholder="Keyword"
                  value={values.keyword || ""}
                  onChange={handleChange}
                  isInvalid={touched.keyword && errors.keyword}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.keyword}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Search
            </Button>
          </Form>
        )}
      </Formik>
      <br />
      <InfiniteScroll
        pageStart={1}
        loadMore={getMoreImages}
        hasMore={searching && total > images.length}
      >
        <Masonry
          className={"grid"}
          elementType={"div"}
          options={masonryOptions}
          disableImagesLoaded={false}
          updateOnEachImageLoad={false}
        >
          {images.map((img, i) => {
            return (
              <div key={i}>
                <img src={img.previewURL} style={{ width: 300 }} />
              </div>
            );
          })}
        </Masonry>
      </InfiniteScroll>
    </div>
  );
}
export default ImageSearchPage;

Мы не загружаем изображения при первой загрузке на эту страницу. Вместо этого пользователь вводит условие поиска в форму, и когда пользователь нажимает кнопку «Поиск», вызывается handleSubmit. Объект evt имеет значения формы, которые обновляются компонентом Formik. Ага предоставляет объект проверки формы с объектом schema, мы просто проверяем, требуется ли keyword.

В функции handlesubmit мы получаем объект evt, который проверяем на соответствие схеме, вызываяschema.validate, который возвращает обещание. Если обещание возвращается к чему-то правдивому, мы продолжаем делать запрос к API Pixabay с ключевым словом поиска и номером страницы.

У нас те же настройки, что и на домашней странице, для сетки изображений с эффектом бесконечной прокрутки и каменной кладки. Единственное отличие состоит в том, что мы вызываем функцию searchAllImages, логика которой аналогична функции getAllImages, за исключением того, что мы передаем параметр keyword в дополнение к параметру страницы. Мы устанавливаем переменную imgs в массив, возвращаемый API Pixabay, и устанавливаем images, вызывая setImages. Мы также устанавливаем страницу, вызывая setPage.

Когда пользователь прокручивает страницу настолько глубоко, что содержимое заканчивается, функция getMoreImages вызывается, когда images.length меньше, чем total. total устанавливается путем получения поля total из API.

Мы используем masonryOptions из exports.js точно так же, как на домашней странице, и отображаем изображения таким же образом.

Затем создайте request.js в папке src, чтобы добавить код для выполнения HTTP-запросов в серверную часть, например:

const axios = require("axios");
const APIURL = "https://pixabay.com/api";
export const getImages = (page = 1) =>
  axios.get(`${APIURL}/?page=${page}&key=${process.env.REACT_APP_APIKEY}`);
export const searchImages = (keyword, page = 1) =>
  axios.get(
    `${APIURL}/?page=${page}&key=${process.env.REACT_APP_APIKEY}&q=${keyword}`
  );

У нас есть getImages только для получения изображений и searchImages, который также отправляет поисковый запрос в API. process.env.REACT_APP_APIKEY - это значение переменной REACT_APP_APIKEY в файле .env в корневой папке проекта.

Затем создайте TopBar.js в папке src и добавьте:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";
function TopBar({ location }) {
  React.useEffect(() => {});
  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Photo App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={location.pathname == "/"}>
            Home
          </Nav.Link>
          <Nav.Link
            href="/imagesearch"
            active={location.pathname == "/imagesearch"}
          >
            Search
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}
export default withRouter(TopBar);

Он содержит React Bootstrap Navbar для отображения верхней панели со ссылкой на домашнюю страницу и именем приложения. Мы проверяем location.pathname, чтобы выделить правильные ссылки, установив свойство active, где свойство location предоставляется React Router путем обертывания функции withRouter за пределами компонента TopBar.

Наконец, в index.js мы заменяем существующий код на:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Photo App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

чтобы добавить CSS Bootstrap и изменить заголовок.

Теперь runnpm start и вы получите: