One place for hosting & domains

      на

      Настройка аутентификации на базе ключей SSH на сервере Linux


      Введение

      SSH или защищенная оболочка — это шифрованный протокол, используемый для администрирования и связи с серверами. При работе с сервером Linux вы, скорее всего, проведете больше всего времени в сеансах терминала с подключением к серверу через SSH.

      Хотя существует несколько разных способов входа на сервер SSH, в этом учебном модуле мы уделим основное внимание настройке ключей SSH. Ключи SSH обеспечивают простой, но при этом очень безопасный способ входа на сервер. Поэтому мы рекомендуем этот метод всем пользователям.

      Как работают ключи SSH?

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

      Хотя пароли отправляются на сервер в безопасном режиме, обычно они недостаточно сложные и длинные, чтобы обеспечить надежную защиту против упорных злоумышленников, совершающих многократные атаки. Вычислительная мощность современных систем и автоматизированные скрипты позволяют достаточно легко взломать учетную запись методом прямого подбора пароля. Хотя существуют и другие методы усиления мер безопасности (fail2ban и т. д.), ключи SSH показали себя надежной и безопасной альтернативой.

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

      Закрытый ключ хранится клиентом и должен быть абсолютно защищен. Любое нарушение безопасности закрытого ключа позволит злоумышленникам входить на серверы с соответствующим открытым ключом без дополнительной аутентификации. В качестве дополнительной меры предосторожности ключ можно зашифровать на диске с помощью парольной фразы.

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

      Открытый ключ выгружается на удаленный сервер, на который вы хотите заходить, используя SSH. Этот ключ добавляется в специальный файл ~/.ssh/authorized_keys в учетной записи пользователя, которую вы используете для входа.

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

      Создание ключей SSH

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

      Для этого мы можем использовать специальную утилиту ssh-keygen, которая входит в стандартный набор инструментов OpenSSH. По умолчанию она создает пару 2048-битных ключей RSA, что подходит для большинства сценариев использования.

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

      ssh-keygen
      
      Generating public/private rsa key pair.
      Enter file in which to save the key (/home/username/.ssh/id_rsa):
      

      Утилита предложит вам выбрать место размещения генерируемых ключей. По умолчанию ключи хранятся в каталоге ~/.ssh внутри домашнего каталога вашего пользователя. Закрытый ключ будет иметь имя id_rsa, а соответствующий открытый ключ будет иметь имя id_rsa.pub.

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

      Если ранее вы сгенерировали пару ключей SSH, вы можете увидеть следующий диалог:

      /home/username/.ssh/id_rsa already exists.
      Overwrite (y/n)?
      

      Если вы решите перезаписать ключ на диске, вы больше не сможете выполнять аутентификацию с помощью предыдущего ключа. Будьте осторожны при выборе варианта yes, поскольку этот процесс уничтожает ключи, и его нельзя отменить.

      Created directory '/home/username/.ssh'.
      Enter passphrase (empty for no passphrase):
      Enter same passphrase again:
      

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

      Возможно вам будет интересно, в чем заключаются преимущества ключа SSH, если вам все равно нужна парольная фраза. Вот некоторые его преимущества:

      • Закрытый ключ SSH (защищенная паролем часть) никогда не доступен через сеть. Парольная фраза используется только для расшифровки ключа на локальном компьютере. Это означает, что парольную фразу нельзя взломать через сеть методом прямого подбора.
      • Закрытый ключ хранится в каталоге с ограниченным доступом. Клиент SSH не принимает закрытые ключи, хранящиеся в каталогах, доступ к которым не ограничен. У самого ключа могут быть ограниченные разрешения (чтение и запись доступны только владельцу). Это означает, что другие пользователи системы не смогут создать уязвимость.
      • Для попытки взлома защищенного парольной фразой закрытого ключа SSH злоумышленнику уже необходим доступ к системе. Это означает, что у него уже должен быть доступ к учетной записи пользователя или учетной записи root. Если вы окажетесь в такой ситуации, парольная фраза может помешать злоумышленнику сразу же попасть на ваши другие серверы. Это может дать вам достаточно времени, чтобы создать и внедрить новую пару ключей SSH и запретить доступ с взломанным ключом.

      Поскольку закрытый ключ недоступен через сеть и защищен системой разрешений, доступ к этому файлу будет только у вас (и у пользователя root). Парольная фраза служит дополнительным уровнем защиты на случай взлома одной из этих учетных записей.

      Парольная фраза представляет собой необязательное дополнение. Если вы решите ее использовать, вам нужно будет вводить ее при каждом использовании соответствующего ключа (если вы не используете программный агент SSH, хранящий зашифрованный ключ). Мы рекомендуем использовать парольную фразу, но если вы не хотите ее задавать, вы можете просто нажать ENTER, чтобы пропустить этот диалог.

      Your identification has been saved in /home/username/.ssh/id_rsa.
      Your public key has been saved in /home/username/.ssh/id_rsa.pub.
      The key fingerprint is:
      a9:49:2e:2a:5e:33:3e:a9:de:4e:77:11:58:b6:90:26 username@remote_host
      The key's randomart image is:
      +--[ RSA 2048]----+
      |     ..o         |
      |   E o= .        |
      |    o. o         |
      |        ..       |
      |      ..S        |
      |     o o.        |
      |   =o.+.         |
      |. =++..          |
      |o=++.            |
      +-----------------+
      

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

      Как встроить открытый ключ при создании сервера

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

      Внизу страницы создания дроплета есть опция для добавления ключей SSH на ваш сервер:

      Встраивание ключа SSH

      Если вы уже добавили файл открытого ключа в учетную запись DigitalOcean, вы сможете выбрать данную опцию (в примере выше указаны два существующих ключа: “Work key” и “Home key”). Чтобы встроить существующий ключ, нажмите на него, чтобы его выделить. Вы можете встроить несколько ключей на один сервер:

      Выбор ключа SSH

      Если в вашу учетную запись еще не выгружен открытый ключ SSH, или если вы хотите добавить новый ключ, нажмите кнопку “+ Add SSH Key”. При этом будет открыто диалоговое окно:

      Диалог ключа SSH

      Вставьте содержимое открытого ключа SSH в поле “SSH Key content”. Если вы сгенерировали ключи, используя указанный выше метод, вы можете получить содержимое открытого ключа на локальном компьютере, введя следующую команду:

      cat ~/.ssh/id_rsa.pub
      
      ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNqqi1mHLnryb1FdbePrSZQdmXRZxGZbo0gTfglysq6KMNUNY2VhzmYN9JYW39yNtjhVxqfW6ewc+eHiL+IRRM1P5ecDAaL3V0ou6ecSurU+t9DR4114mzNJ5SqNxMgiJzbXdhR+j55GjfXdk0FyzxM3a5qpVcGZEXiAzGzhHytUV51+YGnuLGaZ37nebh3UlYC+KJev4MYIVww0tWmY+9GniRSQlgLLUQZ+FcBUjaqhwqVqsHe4F/woW1IHe7mfm63GXyBavVc+llrEzRbMO111MogZUcoWDI9w7UIm8ZOTnhJsk7jhJzG2GpSXZHmly/a/buFaaFnmfZ4MYPkgJD username@example.com
      

      Вставьте это значение в более крупное поле целиком. В поле “Comment (optional)” вы можете выбрать ярлык для данного ключа. Этот ярлык будет отображаться как имя ключа в интерфейсе DigitalOcean:

      Новый ключ SSH

      При создании дроплета выбранные вами открытые ключи SSH будут помещены в файл ~/.ssh/authorized_keys в учетной записи пользователя root. Это позволит вам входить на сервер с компьютера, используя ваш закрытый ключ.

      Как скопировать открытый ключ на ваш сервер

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

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

      Копирование открытого ключа с использованием SSH-Copy-ID

      Самый удобный способ скопировать открытый ключ на существующий сервер — использовать утилиту под названием ssh-copy-id. Поскольку этот метод очень простой, если он доступен, его рекомендуется использовать.

      Инструмент ssh-copy-id входит в пакеты OpenSSH во многих дистрибутивах, так что, возможно, он уже установлен на вашей локальной системе. Чтобы этот метод сработал, вы должны уже настроить защищенный паролем доступ к серверу через SSH.

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

      Синтаксис выглядит следующим образом:

      ssh-copy-id username@remote_host
      

      Вы можете увидеть следующее сообщение:

      The authenticity of host '111.111.11.111 (111.111.11.111)' can't be established.
      ECDSA key fingerprint is fd:fd:d4:f9:77:fe:73:84:e1:55:00:ad:d6:6d:22:fe.
      Are you sure you want to continue connecting (yes/no)? yes
      

      Это означает, что ваш локальный компьютер не распознает удаленный хост. Это произойдет при первом подключении к новому хосту. Введите «yes» и нажмите ENTER, чтобы продолжить.

      Затем утилита проведет сканирование локальной учетной записи для поиска ранее созданного ключа id_rsa.pub. Когда ключ будет найден, вам будет предложено ввести пароль учетной записи удаленного пользователя:

      /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
      /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
      username@111.111.11.111's password:
      

      Введите пароль (для безопасности вводимый текст не будет отображаться) и нажмите ENTER. Утилита подключится к учетной записи на удаленном хосте, используя указанный вами пароль. Затем содержимое ключа ~/.ssh/id_rsa.pub будет скопировано в основной каталог ~/.ssh удаленной учетной записи в файл с именем authorized_keys.

      Вы получите следующий результат:

      Number of key(s) added: 1
      
      Now try logging into the machine, with:   "ssh 'username@111.111.11.111'"
      and check to make sure that only the key(s) you wanted were added.
      

      Теперь ваш ключ id_rsa.pub выгружен в удаленную учетную запись. Теперь вы можете перейти к следующему разделу.

      Копирование открытого ключа с помощью SSH

      Если у вас нет ssh-copy-id, но вы активировали защищенный паролем доступ к учетной записи на вашем сервере через SSH, вы можете выгрузить ключи с помощью стандартного метода SSH.

      Для выполнения этой задачи мы можем вывести содержимое нашего открытого ключа SSH на локальный компьютер и передать его через соединение SSH на удаленный сервер. С другой стороны, мы можем подтвердить существование каталога ~/.ssh в используемой нами учетной записи и вывести переданные данные в файл authorized_keys в этом каталоге.

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

      Полная команда выглядит следующим образом:

      cat ~/.ssh/id_rsa.pub | ssh username@remote_host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
      

      Вы можете увидеть следующее сообщение:

      The authenticity of host '111.111.11.111 (111.111.11.111)' can't be established.
      ECDSA key fingerprint is fd:fd:d4:f9:77:fe:73:84:e1:55:00:ad:d6:6d:22:fe.
      Are you sure you want to continue connecting (yes/no)? yes
      

      Это означает, что ваш локальный компьютер не распознает удаленный хост. Это произойдет при первом подключении к новому хосту. Введите «yes» и нажмите ENTER, чтобы продолжить.

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

      username@111.111.11.111's password:
      

      После ввода пароля содержимое ключа id_rsa.pub будет скопировано в конец файла authorized_keys учетной записи удаленного пользователя. Если процедура будет успешно выполнена, переходите к следующему разделу.

      Копирование открытого ключа вручную

      Если у вас нет защищенного паролем доступа SSH к вашему серверу, вам нужно будет выполнить вышеуказанную процедуру вручную.

      Содержимое файла id_rsa.pub нужно будет каким-то образом добавить в файл ~/.ssh/authorized_keys на удаленном компьютере.

      Чтобы вывести содержимое ключа id_rsa.pub, введите на локальном компьютере следующую команду:

      cat ~/.ssh/id_rsa.pub
      

      Вы увидите содержимое ключа, которое может выглядеть примерно так:

      ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCqql6MzstZYh1TmWWv11q5O3pISj2ZFl9HgH1JLknLLx44+tXfJ7mIrKNxOOwxIxvcBF8PXSYvobFYEZjGIVCEAjrUzLiIxbyCoxVyle7Q+bqgZ8SeeM8wzytsY+dVGcBxF6N4JS+zVk5eMcV385gG3Y6ON3EG112n6d+SMXY0OEBIcO6x+PnUSGHrSgpBgX7Ks1r7xqFa7heJLLt2wWwkARptX7udSq05paBhcpB0pHtA1Rfz3K2B+ZVIpSDfki9UVKzT8JUmwW6NNzSgxUfQHGwnW7kj4jp4AT0VZk3ADw497M2G/12N0PPB5CnhHf7ovgy6nL1ikrygTKRFmNZISvAcywB9GVqNAVE+ZHDSCuURNsAInVzgYo9xgJDW8wUw2o8U77+xiFxgI5QSZX3Iq7YLMgeksaO4rBJEa54k8m5wEiEE1nUhLuJ0X/vh2xPff6SQ1BL/zkOhvJCACK6Vb15mDOeCSq54Cr7kvS46itMosi/uS66+PujOO+xt/2FWYepz6ZlN70bRly57Q06J+ZJoc9FfBCbCyYH7U/ASsmY095ywPsBo1XQ9PqhnN1/YOorJ068foQDNVpm146mUpILVxmq41Cj55YKHEazXGsdBIbXWhcrRf4G2fJLRcGUr9q8/lERo9oxRm5JFX6TCmj6kmiFqv+Ow9gI0x8GvaQ== demo@test
      

      Откройте удаленный хост, используя любой доступный метод. Например, если вы используете дроплет DigitalOcean Droplet как сервер, вы можете выполнить вход, используя веб-консоль на панели управления:

      Доступ к консоли DigitalOcean

      Когда вы получите доступ к учетной записи на удаленном сервере, вам нужно будет убедиться, что каталог ~/.ssh создан. При необходимости эта команда создаст каталог, а если каталог уже существует, команда ничего не сделает:

      mkdir -p ~/.ssh
      

      Теперь вы можете создать или изменить файл authorized_keys в этом каталоге. Вы можете добавить содержимое файла id_rsa.pub в конец файла authorized_keys и, при необходимости, создать этот файл, используя следующую команду:

      echo public_key_string >> ~/.ssh/authorized_keys
      

      В вышеуказанной команде замените public_key_string результатами команды cat ~/.ssh/id_rsa.pub, выполненной на локальном компьютере. Она должна начинаться с ssh-rsa AAAA....

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

      Аутентификация на сервере с использованием ключей SSH

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

      Базовый процесс выглядит аналогично:

      ssh username@remote_host
      

      Если вы подключаетесь к этому хосту первый раз (если вы используете указанный выше последний метод), вы сможете увидеть следующее:

      The authenticity of host '111.111.11.111 (111.111.11.111)' can't be established.
      ECDSA key fingerprint is fd:fd:d4:f9:77:fe:73:84:e1:55:00:ad:d6:6d:22:fe.
      Are you sure you want to continue connecting (yes/no)? yes
      

      Это означает, что ваш локальный компьютер не распознает удаленный хост. Введите «yes» и нажмите ENTER, чтобы продолжить.

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

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

      Отключение аутентификации с помощью пароля на сервере

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

      Прежде чем выполнять описанные в этом разделе шаги, убедитесь, что вы настроили аутентификацию на базе ключей SSH для учетной записи root на этом сервере или (что предпочтительно) вы настроили аутентификацию на базе ключей SSH для учетной записи с доступом sudo на этом сервере. На этом шаге вы сможете заблокировать вход в систему на основе паролей, так что вам необходимо сохранить возможность доступа для администрирования.

      Когда вышеуказанные условия будут выполнены, войдите на удаленный сервер с помощью ключей SSH с учетной записью root или с учетной записью с привилегиями sudo. Откройте файл конфигурации демона SSH:

      sudo nano /etc/ssh/sshd_config
      

      Найдите в файле директиву PasswordAuthentication. Она может быть помечена как комментарий. Удалите символ комментария в начале строки и установите значение «no». После этого вы потеряете возможность входа в систему через SSH с использованием паролей учетной записи:

      PasswordAuthentication no
      

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

      На компьютерах под управлением Ubuntu или Debian можно использовать следующую команду:

      sudo service ssh restart
      

      На компьютерах под управлением CentOS/Fedora этот демон носит имя sshd:

      sudo service sshd restart
      

      Выполнив этот шаг, вы успешно перенастроили демон SSH так, чтобы он реагировал только на ключи SSH.

      Заключение

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



      Source link

      Включение рендеринга на стороне сервера для приложения React


      Введение

      Рендеринг на стороне сервера (SSR) — это популярная методика рендеринга одностраничного клиентского приложения (SPA) на сервере и последующей отправки на клиент полностью отрисованной страницы. Это позволяет использовать динамические компоненты в качестве статической разметки HTML.

      Такой подход может быть полезным для поисковой оптимизации (SEO), когда при индексации код JavaScript не обрабатывается надлежащим образом. Это также может быть полезно в ситуациях, когда загрузка большого блока JavaScript затруднена из-за медленной скорости сети.

      В этом учебном модуле мы инициализируем приложение React с помощью Create React App, а затем изменим проект, чтобы он активировал рендеринг на стороне сервера.

      После прохождения учебного модуля вы получите работающий проект с клиентским приложением React и серверным приложением Express.

      Примечание. Также Next.js позволяет использовать современный подход к созданию статических приложений React и приложений, рендеринг которых выполняется на сервере.

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

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

      Этот учебный модуль был проверен с версиями Node v14.4.0 и npm v6.14.5.

      Шаг 1 — Создание приложения React и изменение компонента приложения

      Вначале мы используем npx для запуска нового приложения React с помощью последней версии Create React App.

      Назовем наше приложение my-ssr-app:

      • npx create-react-app@3.4.1 my-ssr-app

      Перейдем в новый каталог с помощью команды cd:

      cd my-ssr-app
      

      В заключение мы запустим наше новое приложение на стороне клиента для проверки установки:

      Вы должны увидеть пример приложения React в окне браузера.

      Теперь создадим компонент <Home>:

      Затем добавим следующий код в файл Home.js:

      src/Home.js

      import React from 'react';
      
      export default props => {
        return <h1>Hello {props.name}!</h1>;
      };
      

      При этом будет создан заголовок <h1> с сообщением "Hello“, адресованным имени.

      Далее мы выполним рендеринг <Home> в компоненте <App>. Откройте файл App.js:

      Затем заменим существующие строки кода новыми строками кода:

      src/App.js

      import React from 'react';
      import Home from './Home';
      
      export default () => {
        return <Home name="Sammy" />;
      };
      

      Они передают name в компонент <Home> так, что ожидаемое сообщение будет выглядеть так: "Hello Sammy!".

      В файле index.js нашего приложения мы будем использовать метод ReactDOM hydrate вместо render, чтобы указать блоку рендеринга DOM, чтобы мы восстанавливаем приложение после рендеринга на стороне сервера.

      Откроем файл index.js:

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

      index.js

      import React from 'react';
      import ReactDOM from 'react-dom';
      import App from './App';
      
      ReactDOM.hydrate(<App />, document.getElementById('root'));
      

      Мы завершили настройку на стороне клиента и теперь можем перейти к настройке на стороне сервера.

      Шаг 2 — Создание сервера Express и рендеринг компонента приложения

      Теперь наше приложение готово, и мы настроим сервер, который будет отправлять готовую версию после рендеринга. Для сервера мы будем использовать Express. Добавим его в проект, введя следующую команду в окне терминала:

      • npm install express@4.17.1

      Также можно использовать yarn:

      Создайте каталог server рядом с каталогом src нашего приложения:

      Затем создайте новый файл index.js, содержащий код сервера Express:

      Добавим необходимые элементы импорта и определим некоторые константы:

      server/index.js

      import path from 'path';
      import fs from 'fs';
      
      import React from 'react';
      import express from 'express';
      import ReactDOMServer from 'react-dom/server';
      
      import App from '../src/App';
      
      const PORT = process.env.PORT || 3006;
      const app = express();
      

      Затем добавим код сервера с обработкой ошибок:

      server/index.js

      // ...
      
      app.get('/', (req, res) => {
        const app = ReactDOMServer.renderToString(<App />);
      
        const indexFile = path.resolve('./build/index.html');
        fs.readFile(indexFile, 'utf8', (err, data) => {
          if (err) {
            console.error('Something went wrong:', err);
            return res.status(500).send('Oops, better luck next time!');
          }
      
          return res.send(
            data.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
          );
        });
      });
      
      app.use(express.static('./build'));
      
      app.listen(PORT, () => {
        console.log(`Server is listening on port ${PORT}`);
      });
      

      Как видите, мы можем импортировать наш компонент <App> из клиентского приложения непосредственно с сервера.

      Здесь происходит три важные вещи:

      • Мы предписываем Express вывести содержимое каталога build в виде статичных файлов.
      • Мы будем использовать метод ReactDOMServer, renderToString, для рендеринга нашего приложения в статичную строку HTML.
      • Затем мы считываем статичный файл index.html из готового клиентского приложения, вставляем статичное содержимое нашего приложения в <div> с id "root", а затем отправляем результат в качестве ответа на запрос.

      Шаг 3 — Настройка webpack, Babel и скриптов npm

      Чтобы наш серверный код работал, нам нужно объединить его в комплект и провести транспиляцию, используя webpack и Babel. Для этого добавим в проект зависимости dev, введя следующую команду в окне терминала:

      • npm install webpack@4.42.0 webpack-cli@3.3.12 webpack-node-externals@1.7.2 @babel/core@7.10.4 babel-loader@8.1.0 @babel/preset-env@7.10.4 @babel/preset-react@7.10.4 --save-dev

      Также можно использовать yarn:

      • yarn add webpack@4.42.0 webpack-cli@3.3.12 webpack-node-externals@1.7.2 @babel/core@7.10.4 babel-loader@8.1.0 @babel/preset-env@7.10.4 @babel/preset-react@7.10.4 --dev

      Примечание. В более ранней версии этого учебного модуля мы устанавливали babel-core, babel-preset-env и babel-preset-react-app. Эти пакеты с тех пор были архивированы, и вместо них используются версии с одним репозиторием.

      Далее мы создадим файл конфигурации Babel:

      После этого добавьте готовые настройки env и react-app:

      .babelrc.json

      {
        "presets": [
          "@babel/preset-env",
          "@babel/preset-react"
        ]
      }
      

      Примечание. В более ранней версии этого учебного модуля мы использовали файл .babelrc (без расширения .json). Это был файл конфигурации Babel 6, однако для Babel 7 он больше не используется.

      Теперь мы создадим конфигурацию webpack для сервера, который использует Babel Loader для транспиляции кода. Начните с создания файла:

      Затем добавьте следующие конфигурации в файл webpack.server.js:

      webpack.server.js

      const path = require('path');
      const nodeExternals = require('webpack-node-externals');
      
      module.exports = {
        entry: './server/index.js',
      
        target: 'node',
      
        externals: [nodeExternals()],
      
        output: {
          path: path.resolve('server-build'),
          filename: 'index.js'
        },
      
        module: {
          rules: [
            {
              test: /.js$/,
              use: 'babel-loader'
            }
          ]
        }
      };
      

      С этой конфигурацией наш транспилированный серверный комплект будет выводиться в папку server-build в файле с именем called index.js.

      Обратите внимание на использование target: 'node' и externals: [nodeExternals()] из webpack-node-externals. При этом опускаются файлы из node_modules в комплекте, сервер сможет получить доступ к этим файлам напрямую.

      Это завершает установку зависимости и конфигурации webpack и Babel.

      Теперь мы снова вернемся к файлу package.json и добавим вспомогательные скрипты npm:

      Мы добавим скрипты dev:build-server, dev:start и dev в файл package.json, чтобы легко выполнять сборку и подачу нашего приложения SSR:

      package.json

      "scripts": {
        "dev:build-server": "NODE_ENV=development webpack --config webpack.server.js --mode=development -w",
        "dev:start": "nodemon ./server-build/index.js",
        "dev": "npm-run-all --parallel build dev:*",
        ...
      },
      

      Мы используем nodemon для перезапуска сервера при внесении изменений. Также мы используем npm-run-all для параллельного выполнения нескольких команд.

      Давайте установим эти пакеты, введя следующие команды в окне терминала:

      • npm install nodemon@2.0.4 npm-run-all@4.1.5 --save-dev

      Также можно использовать yarn:

      • yarn add nodemon@2.0.4 npm-run-all@4.1.5 --dev

      Так вы можете запустить следующий код для сборки приложения на стороне клиента, объединения в пакет и транспиляции кода сервера и запуска сервера на порту :3006:

      Также можно использовать yarn:

      Наша конфигурация сервера webpack будет следить за изменениями, и в случае изменений наш сервер перезапустится. Однако для клиентского приложения нам нужно выполнять сборку каждый раз при внесении изменений. Для этого есть открытая проблема.

      Откройте в браузере адрес http://localhost:3006/ и вы увидите приложение после рендеринга на стороне сервера.

      Ранее исходный код показал следующее:

      Output

      <div id="root"></div>

      С внесенными изменениями исходный код показывает:

      Output

      <div id="root"><h1 data-reactroot="">Hello <!-- -->Sammy<!-- -->!</h1></div>

      При рендеринге на стороне сервера компонент <App> был успешно конвертирован в формат HTML.

      Заключение

      В этом учебном модуле мы инициализировали приложение React и активировали рендеринг на стороне сервера.

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

      Преимущество использования SSR заключается в наличии приложения, содержимое которого может просмотреть даже сборщик, не выполняющий код JavaScript. Это поможет для поисковой оптимизации (SEO) и отправки метаданных на каналы социальных сетей.

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

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



      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