One place for hosting & domains

      помощью

      Доступ к фронтальной и задней камерам с помощью getUserMedia() в JavaScript


      Введение

      В языке HTML5 появились API для доступа к аппаратному обеспечению устройств, включая MediaDevices API. Этот API предоставляет доступ к устройствам ввода мультимедиа. в том числе аудио и видео.

      С помощью этого API разработчики могут получать доступ к устройствам аудио и видео для трансляции и вывода видеопотоков в реальном времени в браузере. В этом учебном модуле вы научитесь получать доступ к видеопотокам с устройства пользователя и выводить его в браузере, используя метод getUserMedia.

      getUserMedia API использует устройства ввода мультимедиа для получения потока MediaStream. Этот поток MediaStream содержит запрошенные типы мультимедиа, то есть аудио или видео. С помощью API можно выводить видеопотоки через браузер, что можно использовать для связи в реальном времени через браузер.

      При использовании вместе с MediaStream Recording API вы можете записывать и сохранять мультимедийные данные, полученные через браузер. Этот API работает только с безопасными источниками, как и остальные новые API, а также работает с localhost и файловыми URL.

      Предварительные требования

      В начале этого учебного модуля мы объясним концепции и продемонстрируем примеры с помощью Codepen. На заключительном шаге вы создадите рабочий видеопоток в браузере.

      Шаг 1 — Проверка поддержки устройств

      Вначале мы проверим, поддерживает ли браузер пользователя mediaDevices API. Этот API существует в интерфейсе navigator и содержит текущее состояние и идентификатор пользовательского агента. Для проверки используется следующий код, который можно вставить в Codepen:

      if ('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices) {
        console.log("Let's get this party started")
      }
      

      Вначале проверяется наличие mediaDevices API в navigator, а затем проверяется доступность getUserMedia API в mediaDevices. Если возвращается результат true, можно начинать работу.

      Шаг 2 — Запрос разрешения пользователя

      После подтверждения поддержки getUserMedia в браузере необходимо запросить разрешение на использование устройств ввода мультимедиа на пользовательском агенте. Обычно после предоставления пользователем разрешения возвращается промис, который разрешается в мультимедийный поток. Если пользователь не дает разрешения, этот промис не возвращается, и доступ к данным устройствам блокируется.

      Вставьте в Codepen следующую строку, чтобы запросить разрешение:

      navigator.mediaDevices.getUserMedia({video: true})
      

      Объект, предоставляемый как аргумент для метода getUserMedia называется constraints (переводится как ограничения). Он определяет, к каким устройствам ввода мультимедиа вы запрашиваете разрешение на доступ. Например, если объект содержит текст audio: true, у пользователя запрашивается доступ к устройству ввода аудио.

      Шаг 3 — Понимание концепции ограничений мультимедиа

      В этом разделе мы расскажем о концепции contraints. Объект constraints — это объект MediaStreamConstraints, который указывает типы мультимедиа для запроса и требования каждого типа мультимедиа. Вы можете использовать объект constraints, чтобы указать требования к запрошенному потоку, например, требуемое разрешение (front, back).

      При отправке этого запроса следует указать аргумент audio или video. Если запрошенные типы мультимедиа не будут найдены в браузере пользователя, будет выведено сообщение об ошибке NotFoundError.

      Если вы планируете запросить видеопоток м разрешением 1280 x 720, вы можете обновить объект constraints, чтобы он выглядел так:

      {
        video: {
          width: 1280,
          height: 720,
        }
      }
      

      С этим обновлением браузер попробует подобрать заданные настройки качества для потока. Если видеоустройство не сможет обеспечить это разрешение, браузер возвратит другие доступные разрешения.

      Чтобы браузер гарантированно возвращал разрешение не ниже указанного, вам нужно будет использовать свойство min. Здесь вы можете обновить объект constraints, добавив в него свойство min:

      {
        video: {
          width: {
            min: 1280,
          },
          height: {
            min: 720,
          }
        }
      }
      

      Это обеспечит возвращаемое разрешение потока не ниже 1280 x 720. Если это минимальное требование не удастся выполнить, промис будет отклонен с сообщением об ошибке OverconstrainedError.

      В некоторых случаях вас может беспокоить сохранение данных и необходимость не превышать заданное разрешение потока. Это может быть полезно, если пользователь использует лимитированный тарифный план. Для реализации этой функции следует обновить объект constraints, добавив в него поле max:

      {
        video: {
          width: {
            min: 1280,
            max: 1920,
          },
          height: {
            min: 720,
            max: 1080
          }
        }
      }
      

      При таких параметрах браузер будет использовать разрешение видеопотока не менее 1280 x 720 и не более 1920 x 1080.

      Также можно использовать аргументы exact и ideal. Параметр ideal обычно используется со свойствами min и max для подбора наилучших возможных настроек, ближайших к идеальному значению.

      Вы можете обновить объект constraints для использования ключевого слова ideal:

      {
        video: {
          width: {
            min: 1280,
            ideal: 1920,
            max: 2560,
          },
          height: {
            min: 720,
            ideal: 1080,
            max: 1440
          }
        }
      }
      

      Чтобы указать браузеру использовать фронтальную или заднюю камеру (на мобильных устройствах), вы можете указать свойство facingMode в объекте video:

      {
        video: {
          width: {
            min: 1280,
            ideal: 1920,
            max: 2560,
          },
          height: {
            min: 720,
            ideal: 1080,
            max: 1440
          },
          facingMode: 'user'
        }
      }
      

      При этой настройке на всех устройствах всегда будет использоваться фронтальная камера. Чтобы использовать заднюю камеру мобильных устройств. вы можете изменить значение свойства facingMode на environment.

      {
        video: {
          ...
          facingMode: {
            exact: 'environment'
          }
        }
      }
      

      Шаг 4 — Использование метода enumerateDevices

      При вызове метода enumerateDevices возвращаются все доступные на компьютере пользователя устройства ввода мультимедиа.

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

      Ниже приведен фрагмент кода с примером использования этого метода:

      async function getDevices() {
        const devices = await navigator.mediaDevices.enumerateDevices();
      }
      

      Образец ответа для каждого из устройств выглядит следующим образом:

      {
        deviceId: "23e77f76e308d9b56cad920fe36883f30239491b8952ae36603c650fd5d8fbgj",
        groupId: "e0be8445bd846722962662d91c9eb04ia624aa42c2ca7c8e876187d1db3a3875",
        kind: "audiooutput",
        label: "",
      }
      

      Примечание. Ярлык не возвращается, если отсутствует доступный поток, или если пользователь не предоставит разрешения на доступ к устройству.

      Шаг 5 — Отображение видеопотока в браузере

      Вы выполнили процедуру запроса и получения доступа к мультимедийным устройствам, настроили объект constraints для добавления требуемых разрешений и выбрали камеру, которая потребуется для записи видео.

      После выполнения всех этих шагов нам нужно будет посмотреть соответствие потока трансляции заданным параметрам. Для этого мы используем элемент <video>, чтобы вывести видеопоток в браузере.

      Как уже говорилось ранее. метод getUserMedia возвращает промис, который может быть разрешен в поток. Возвращаемый поток можно конвертировать в URL объекта, используя метод createObjectURL. Этот URL будет установлен как источник видео.

      Вы создадите короткую демонстрационную программу, где пользователь сможет выбрать доступное видеоустройство из списка, используя метод enumerateDevices.

      Это метод navigator.mediaDevices. Он перечисляет доступные мультимедийные устройства, в том числе микрофоны и камеры. Он возвращает промис, который разрешается в массив объектов с подробными сведениями о доступных мультимедийных устройствах.

      Создайте файл index.html и обновите его содержимое с помощью следующего кода:

      index.html

      <!doctype html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport"
                content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
          <meta http-equiv="X-UA-Compatible" content="ie=edge">
          <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
          <link rel="stylesheet" href="style.css">
          <title>Document</title>
      </head>
      <body>
      <div class="display-cover">
          <video autoplay></video>
          <canvas class="d-none"></canvas>
      
          <div class="video-options">
              <select name="" id="" class="custom-select">
                  <option value="">Select camera</option>
              </select>
          </div>
      
          <img class="screenshot-image d-none" alt="">
      
          <div class="controls">
              <button class="btn btn-danger play" title="Play"><i data-feather="play-circle"></i></button>
              <button class="btn btn-info pause d-none" title="Pause"><i data-feather="pause"></i></button>
              <button class="btn btn-outline-success screenshot d-none" title="ScreenShot"><i data-feather="image"></i></button>
          </div>
      </div>
      
      <script src="https://unpkg.com/feather-icons"></script>
      <script src="script.js"></script>
      </body>
      </html>
      

      В приведенном выше фрагменте кода вы настроили требуемые элементы и пару элементов управления видео. Также мы добавили кнопку для снимков экрана текущего видеопотока.

      Теперь применим к этим компонентам стили.

      Создайте файл style.css и скопируйте в него следующие стили. Мы включили Bootstrap, чтобы сократить объем кода CSS, который нужно будет написать для работы компонентов.

      style.css

      .screenshot-image {
          width: 150px;
          height: 90px;
          border-radius: 4px;
          border: 2px solid whitesmoke;
          box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
          position: absolute;
          bottom: 5px;
          left: 10px;
          background: white;
      }
      
      .display-cover {
          display: flex;
          justify-content: center;
          align-items: center;
          width: 70%;
          margin: 5% auto;
          position: relative;
      }
      
      video {
          width: 100%;
          background: rgba(0, 0, 0, 0.2);
      }
      
      .video-options {
          position: absolute;
          left: 20px;
          top: 30px;
      }
      
      .controls {
          position: absolute;
          right: 20px;
          top: 20px;
          display: flex;
      }
      
      .controls > button {
          width: 45px;
          height: 45px;
          text-align: center;
          border-radius: 100%;
          margin: 0 6px;
          background: transparent;
      }
      
      .controls > button:hover svg {
          color: white !important;
      }
      
      @media (min-width: 300px) and (max-width: 400px) {
          .controls {
              flex-direction: column;
          }
      
          .controls button {
              margin: 5px 0 !important;
          }
      }
      
      .controls > button > svg {
          height: 20px;
          width: 18px;
          text-align: center;
          margin: 0 auto;
          padding: 0;
      }
      
      .controls button:nth-child(1) {
          border: 2px solid #D2002E;
      }
      
      .controls button:nth-child(1) svg {
          color: #D2002E;
      }
      
      .controls button:nth-child(2) {
          border: 2px solid #008496;
      }
      
      .controls button:nth-child(2) svg {
          color: #008496;
      }
      
      .controls button:nth-child(3) {
          border: 2px solid #00B541;
      }
      
      .controls button:nth-child(3) svg {
          color: #00B541;
      }
      
      .controls > button {
          width: 45px;
          height: 45px;
          text-align: center;
          border-radius: 100%;
          margin: 0 6px;
          background: transparent;
      }
      
      .controls > button:hover svg {
          color: white;
      }
      

      Следующий шаг — добавление функционала в демонстрацию. Используя метод enumerateDevices, вы получите доступные видеоустройства и зададите их как опции выбранного элемента. Создайте файл script.js и добавьте в него следующий код:

      script.js

      feather.replace();
      
      const controls = document.querySelector('.controls');
      const cameraOptions = document.querySelector('.video-options>select');
      const video = document.querySelector('video');
      const canvas = document.querySelector('canvas');
      const screenshotImage = document.querySelector('img');
      const buttons = [...controls.querySelectorAll('button')];
      let streamStarted = false;
      
      const [play, pause, screenshot] = buttons;
      
      const constraints = {
        video: {
          width: {
            min: 1280,
            ideal: 1920,
            max: 2560,
          },
          height: {
            min: 720,
            ideal: 1080,
            max: 1440
          },
        }
      };
      
      const getCameraSelection = async () => {
        const devices = await navigator.mediaDevices.enumerateDevices();
        const videoDevices = devices.filter(device => device.kind === 'videoinput');
        const options = videoDevices.map(videoDevice => {
          return `<option value="${videoDevice.deviceId}">${videoDevice.label}</option>`;
        });
        cameraOptions.innerHTML = options.join('');
      };
      
      play.onclick = () => {
        if (streamStarted) {
          video.play();
          play.classList.add('d-none');
          pause.classList.remove('d-none');
          return;
        }
        if ('mediaDevices' in navigator && navigator.mediaDevices.getUserMedia) {
          const updatedConstraints = {
            ...constraints,
            deviceId: {
              exact: cameraOptions.value
            }
          };
          startStream(updatedConstraints);
        }
      };
      
      const startStream = async (constraints) => {
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        handleStream(stream);
      };
      
      const handleStream = (stream) => {
        video.srcObject = stream;
        play.classList.add('d-none');
        pause.classList.remove('d-none');
        screenshot.classList.remove('d-none');
        streamStarted = true;
      };
      
      getCameraSelection();
      

      В приведенном выше фрагменте кода выполняется ряд действий. Давайте рассмотрим их подробнее:

      1. feather.replace(): этот метод создает экземпляр feather, набора иконок для веб-разработки.
      2. Переменная constraints хранит начальную конфигурацию видеопотока. Она будет расширена, и в нее будет добавлено выбранное пользователем мультимедийное устройство.
      3. getCameraSelection: данная функция вызывает метод enumerateDevices. Затем мы выполняем фильтрацию массива на основе разрешенного промиса, и выбираем устройства ввода видео. Из отфильтрованных результатов вы создаете <option> для элемента <select>.
      4. Вызов метода getUserMedia выполняется в средстве прослушивания onclick кнопки play. Здесь перед началом трансляции мы проверяем, поддерживает ли браузер пользователя этот метод.
      5. Затем мы вызываем функцию startStream, принимающую аргумент constraints. Она вызывает метод getUserMedia с указанными constraints. handleStream вызывается на основе трансляции из разрешенного промиса. Этот метод устанавливает возвращаемый поток для объекта srcObject видеоэлемента.

      Далее мы добавим средства прослушивания нажатий в элементы управления кнопками на страницах для приостановки, остановки и снимков экрана. Также вы добавите средство прослушивания в элемент <select>, чтобы обновить ограничения трансляции для выбранного видеоустройства.

      Добавьте в файл script.js следующий код:

      script.js

      ...
      cameraOptions.onchange = () => {
        const updatedConstraints = {
          ...constraints,
          deviceId: {
            exact: cameraOptions.value
          }
        };
        startStream(updatedConstraints);
      };
      
      const pauseStream = () => {
        video.pause();
        play.classList.remove('d-none');
        pause.classList.add('d-none');
      };
      
      const doScreenshot = () => {
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        canvas.getContext('2d').drawImage(video, 0, 0);
        screenshotImage.src = canvas.toDataURL('image/webp');
        screenshotImage.classList.remove('d-none');
      };
      
      pause.onclick = pauseStream;
      screenshot.onclick = doScreenshot;
      

      Теперь, когда вы откроете файл index.html в браузере, при нажатии Play начнется трансляция.

      Вот полная версия демонстрационной программы:

      Заключение

      В этом учебном модуле мы познакомились с getUserMedia API. Это интересное дополнение HTML5, упрощающее захват мультимедийного контента через интернет.

      Данный API принимает параметр (constraints), который можно использовать для настройки доступа к устройствам ввода звука и видео. Также его можно использовать, чтобы задать требуемое разрешение видео для вашего приложения.

      Вы можете расширить демонстрацию и предоставить пользователю возможность сохранить сделанные снимки экрана, а также записывать и сохранять аудио- и видеоданные с помощью MediaStream Recording API.



      Source link

      Специальная разбивка на страницы с помощью React


      Введение

      Мы часто создаем веб-приложений, в которых требуется доставить большие наборы записей данных с удаленного сервера, API или базы данных. Например, если вы создаете платежную систему, она может доставлять тысячи транзакций. Если это приложение социальной сети, оно может отображать множество комментариев, профилей и действий пользователей. В любом случае существует несколько способов представления данных в такой форме, которая не перегрузит конечного пользователя приложения.

      Один из методов работы с большими наборами данных — это разбивка на страницы. Разбивка на страницы эффективно работает, когда вы заранее знаете размер набора данных (общее количество записей в наборе данных). Вам нужно загрузить только требуемую часть данных из набора в зависимости от взаимодействия конечного пользователя с элементом управления разбивкой на страницы. Эта методика используется для отображения результатов поиска в поисковике Google.

      В этом учебном модуле вы научитесь создавать специальный компонент разбивки на страницы с помощью React для разбивки на страницы больших наборов данных. Мы построим разбитое на страницы представление всех стран мира, используя набор данных с известным размером.

      Вот демонстрация того, что вы сделаете в этом учебном модуле:

      Снимок экрана демонстрационного приложения — показ стран мира

      Предварительные требования

      Для данного обучающего руководства вам потребуется следующее:

      • [Node], nodejsустановленный на вашем компьютере. Процедура установки описана в документе «Установка Node.js и создание локальной среды разработки».
      • Пакет командной строки [create-react-app][`create-react-app] создает базовый код для вашего приложения React. Если вы используете версиюnpm < 5.2, возможно, вам потребуется установитьcreate-react-app` как глобальную зависимость.
      • Наконец, в этом обучающем модуле предполагается, что вы уже знакомы с React. Если это не так, вы можете ознакомиться с серией «Программирование на React.js», чтобы узнать больше о React.

      Этот учебный модуль был проверен с использованием Node v14.2.0, npm v6.14.4, react v16.13.1 и react-scripts v3.4.1.

      Шаг 1 — Настройка проекта

      Создайте новое приложение React, используя команду create-react-app. Вы можете назвать приложение как угодно, но в этом учебном модуле мы присвоим ему имя react-pagination:

      • npx create-react-app 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:

      Добавьте следующую строку перед другими выражениями import:

      src/index.js

      import "bootstrap/dist/css/bootstrap.min.css";
      

      Теперь в вашем приложении будут доступны стили Bootstrap.

      Также вы установили react-flags как зависимость для вашего приложения. Чтобы получить доступ к иконкам флагов из вашего приложения, вам потребуется скопировать изображения иконок в каталог public вашего приложения.

      Создайте каталог img в вашем каталоге public:

      Скопируйте файлы изображения из flags в img:

      • cp -R node_modules/react-flags/vendor/flags public/img

      Это обеспечивает создание копии всех изображений react-flag в вашем приложении.

      Теперь мы добавили некоторые зависимости и можем запустить приложение, выполнив следующую команду с npm в каталоге проекта react-pagination:

      Теперь вы запустили приложение и можете начинать разработку. Обратите внимание, что для вас открыта вкладка браузера с функцией перезагрузки в реальном времени, что позволит сохранять синхронизацию с приложением в процессе разработки.

      Представление приложения должно выглядеть как на следующем снимке экрана:

      Начальное представление — экран приветствия React

      Теперь вы готовы начать создание компонентов.

      Шаг 2 — Создание компонента CountryCard

      На этом шаге мы создадим компонент CountryCard. Компонент CountryCard выполняет рендеринг имени, региона и флага определенной страны.

      Для начала создадим каталог components в каталоге src:

      Затем создадим новый файл CountryCard.js в каталоге src/components:

      • nano src/components/CountryCard.js

      Добавим в него следующий блок кода:

      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.js в каталоге src/components:

      • nano src/components/Pagination.js

      Добавим в него следующий блок кода:

      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:

      src/components/Pagination.js

      // ...
      
      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:

      src/components/Pagination.js

      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:

      src/components/Pagination.js

      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="https://www.digitalocean.com/community/tutorials/#" aria-label="Previous" onClick={this.handleMoveLeft}>
                          <span aria-hidden="true">&laquo;</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="https://www.digitalocean.com/community/tutorials/#" aria-label="Next" onClick={this.handleMoveRight}>
                          <span aria-hidden="true">&raquo;</span>
                          <span className="sr-only">Next</span>
                        </a>
                      </li>
                    );
      
                    return (
                      <li key={index} className={`page-item${ currentPage === page ? ' active' : ''}`}>
                        <a className="page-link" href="https://www.digitalocean.com/community/tutorials/#" onClick={ this.handleClick(page) }>{ page }</a>
                      </li>
                    );
      
                  }) }
      
                </ul>
              </nav>
            </Fragment>
          );
        }
      }
      

      Здесь мы генерируем массив номеров страниц, вызывая метод fetchPageNumbers(), который мы создали ранее. Затем выполняется рендеринг каждого номера страницы с использованием Array.prototype.map(). Обратите внимание, что необходимо регистрировать обработчики событий нажатия для каждого отображаемого номера страницы, чтобы обеспечить обработку нажатий.

      Также следует отметить, что элемент управления разбивкой на страницы не отображается, если объект totalRecords не был передан в компонент Pagination надлежащим образом или в случаях, когда имеется только 1 страница.

      В заключение мы определим методы обработчика событий.

      Добавьте следующее в классе Pagination после функции constructor, метода fetchPageNumbers и метода render:

      src/components/Pagination.js

      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. Теперь пользователи смогут использовать элементы навигации этого компонента для отображения разных страниц с флагами.

      Шаг 4 — Построение компонента App

      Обратите внимание, что теперь у нас имеются компоненты CountryCard и Pagination, и мы можем использовать их в нашем компоненте App.

      Измените файл App.js в каталоге src:

      Замените содержимое файла App.js следующими строками кода:

      src/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:

      src/App.js

      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 стран на одной странице.

      Сейчас приложение выглядит, как показано на следующем снимке экрана:

      Снимок экрана приложения с 248 указанными странами и номерами страниц сверху и до конца каждой страницы

      Теперь у нас имеется компонент App, позволяющий отображать различные компоненты CountryCard и Pagination, разделяющие содержимое на отдельные страницы. Далее мы рассмотрим применение стилей к нашему приложению.

      Шаг 5 — Добавление специальных стилей

      Возможно вы заметили, что мы добавляли определенные специальные классы в ранее созданные компоненты. Давайте определим некоторые правила стилей для этих классов в файле src/App.scss.

      Файл App.scss будет выглядеть, как следующий фрагмент кода:

      src/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.

      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';
      

      После добавления стилей приложение будет выглядеть, как показано на следующем снимке экрана:

      Снимок экрана приложения, страница 1 из 14, со стилями

      Теперь у вас имеется полное приложение с дополнительными индивидуальными стилями. Вы можете использовать индивидуальные стили для изменения или расширения любых стилей по умолчанию, предоставляемых Bootstrap и другими библиотеками.

      Заключение

      В этом учебном модуле мы создали специальный виджет разбивки на страницы в приложении React. Хотя в этом учебном модуле мы не вызывали API и не взаимодействовали с сервером баз данных, для вашего приложения может потребоваться такое взаимодействие. Описанный в этом учебном модуле подход не ограничивает ваши возможности, вы можете расширять его как угодно, чтобы решить стоящие перед вашим приложением задачи.

      Полный исходный код этого учебного модуля можно найти в репозитории build-react-pagination-demo на GitHub. Также вы можете посмотреть работающую демонстрацию этого учебного модуля на Code Sandbox.

      Если вы хотите узнать больше о React, почитайте нашу серию «Программирование на React.js» или посмотрите страницу тем React, где вы найдете упражнения и программные проекты.



      Source link

      Форматирование кода с помощью Prettier в Visual Studio Code


      Введение

      Согласованное форматирование кода — сложная задача, но современные инструменты разработчиков позволяют автоматически обеспечивать согласованность базы кода вашей команды.

      В этой статье мы настроим Prettier для автоматического форматирования кода в Visual Studio Code или VS Code.

      Для демонстрации мы будем форматировать следующий код:

      const name = "James";
      
      const person ={first: name
      }
      
      console.log(person);
      
      const sayHelloLinting = (fName) => {
      console.log(`Hello linting, ${fName}`)
      }
      
      sayHelloLinting('James');
      

      Если вы знакомы с форматированием кода, вы можете заметить некоторые упущения:

      • Смесь одинарных и двойных кавычек.
      • Первое свойство объекта person должно находиться в отдельной строке.
      • Выражение консоли внутри функции должно быть выделено отступами.
      • Вам могут понравиться или не понравиться необязательные скобки, в которые заключен параметр функции arrow.

      Предварительные требования

      Для прохождения этого учебного модуля вам нужно будет загрузить и установить Visual Studio Code.

      Чтобы работать с Prettier в Visual Studio Code, вам потребуется установить расширение. Для этого выполните поиск инструмента Prettier - Code Formatter в панели расширений VS Code. Если вы устанавливаете его в первый раз, вы увидите кнопку install вместо кнопки uninstall, как показано здесь:

      Файл readme расширения Prettier

      Шаг 1 — Использование команды форматирования документа

      После установки расширения Prettier вы можете использовать его для форматирования вашего кода. Для начала выполним обзор, используя команду Format Document. Эта команда сделает ваш код более согласованным с отформатированными пробелами, переносами строк и кавычками.

      Чтобы открыть палитру команд, вы можете использовать COMMAND + SHIFT + P в macOS или CTRL + SHIFT + P в Windows.

      Выполните в палитре команд поиск по ключевому слову format и выберите Format Document.

      Открытая палитра команд с результатами поиска format

      Возможно, вам будет предложено выбрать формат для использования. Для этого нажмите кнопку Configure:

      Строка выбора средства форматирования по умолчанию

      Затем выберите Prettier – Code Formatter.

      Выбор Prettier

      Примечание. Если вы не видите диалога выбора формата по умолчанию, вы можете вручную изменить его в разделе «Настройки». Установите для Editor: Default Formatter значение ebsenp.prettier-vscode.

      Теперь ваш код отформатирован с пробелами, переносами строк и единообразными кавычками:

      const name="James";
      
      const person = { first: name };
      
      console.log(person);
      
      const sayHelloLinting = (fname) => {
        console.log(`Hello linting, ${fName}`);
      }
      
      sayHelloLinting('James');
      

      Это работает и для файлов CSS. Вы можете превращать код с несогласованными отступами, скобками, разрывами строк и точками с запятой в хорошо отформатированный код. Например:

      body {color: red;
      }
      h1 {
        color: purple;
      font-size: 24px
      }
      

      Будет переформатирован как:

      body {
        color: red;
      }
      h1 {
        color: purple;
        font-size: 24px;
      }
      

      Мы изучили эту команду, и теперь посмотрим, как можно реализовать ее автоматическое выполнение.

      Шаг 2 — Форматирование кода при сохранении

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

      Чтобы изменить эту настройку, нажмите COMMAND + в macOS или CTRL + в Windows, чтобы открыть меню Settings (Настройки). Выполните в меню поиск Editor: Format On Save и убедитесь, что эта опция включена:

      Опция Editor: Format On Save включена

      Теперь вы можете писать код как обычно, и он будет автоматически форматироваться при сохранении файла.

      Шаг 3 — Изменение параметров конфигурации Prettier

      Prettier выполняет много действий по умолчанию, но вы также можете внести индивидуальные изменения в его настройки.

      Откройте меню Settings (Настройки). Выполните поиск Prettier. Вы увидите список всех параметров, которые вы можете изменить:

      Параметры конфигурации для Prettier

      Вот несколько наиболее распространенных параметров:

      • Single Quote — выберите, нужно ли использовать одинарные или двойные кавычки.
      • Semi — выберите, нужно ли добавлять точку с запятой в конце строк.
      • Tab Width — укажите, сколько пробелов должно вставляться при табуляции.

      Недостаток использования меню встроенных параметров VS Code заключается в том, что при этом не обеспечивается согласованность между разработчиками в вашей команде.

      Шаг 4 — Создание файла конфигурации Prettier

      Если вы измените настройки VS Code, у другого разработчика может оказаться совершенно иная конфигурация. Вы можете обеспечить единство форматирования в своей команде, создав файл конфигурации для вашего проекта.

      Создайте новый файл .prettierrc.extension с одним из следующих расширений:

      Вот пример простого файла конфигурации в формате JSON:

      {
        "trailingComma": "es5",
        "tabWidth": 4,
        "semi": false,
        "singleQuote": true
      }
      

      Более конкретную информацию о файлах конфигурации можно найти в документации по Prettier. После создания такого файла и его добавления в проект вы можете быть уверены, что все члены команды используют одинаковые правила форматирования.

      Заключение

      Иметь согласованный код — очень хорошая практика. Это особенно полезно при работе над проектом с несколькими участниками. Согласование конфигурации делает код более удобочитаемым и понятным. Это позволяет уделить больше времени решению технических проблем, а не тратить время на исправление отступов.

      Prettier обеспечивает согласованность форматирования кода и позволяет автоматизировать этот процесс.



      Source link