Это пятая часть серии статей: От фиксации к развертыванию. Вы можете найти часть 1, часть 2, часть 3 и 4 здесь.

Общение с Backend API

В этой части мы узнаем, как расширить наше приложение «Цитата дня» для получения цитат с удаленного сервера, а не жестко запрограммировать их в исходном коде.

Выполнение сетевых запросов для получения данных является обычным шаблоном для большинства приложений. Фронтенд инициирует запросы по протоколу HTTP, бэкенд-приложение анализирует и обрабатывает запрос (определяя формат запроса и предпочтительный формат фронтенда), а затем возвращает соответствующие данные. Общие API на основе HTTP включают RESTful API и GraphQL.

Цитируемый API

Мы будем использовать Quotable в качестве нашего серверного API. Quotable API — это бесплатный RESTful API, который предоставляет огромное количество известных цитат и высказываний. Каждая цитата включает автора, контент и связанные с ним теги или категории.

Вот его основные особенности:

  • Получить случайную цитату. Вы можете вызвать API, чтобы получить случайную цитату, которая включает в себя содержание и информацию об авторе.
  • Получить цитаты конкретного автора. Вы можете получить все цитаты автора, выполнив поиск по его имени.
  • Поиск цитат. API предоставляет функцию поиска, позволяющую находить релевантные цитаты по ключевым словам.
  • Извлечение цитат на основе тегов. Каждая цитата помечена соответствующими категориями, такими как «любовь», «жизнь», «дружба» и т. д. Вы можете получить связанные цитаты на основе этих тегов.

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

Мы можем получить доступ к Quotable API через интерфейс командной строки, чтобы увидеть формат данных:

curl https://api.quotable.io/quotes/random

Эта команда использует curl с URL-адресом цитируемого в качестве параметра для получения случайной цитаты. Сервер возвращает следующий формат:

[
  {
    "_id": "N43bqeXRBeqI",
    "content": "Just trust yourself, then you will know how to live.",
    "author": "Johann Wolfgang von Goethe",
    "tags": [
      "Famous Quotes"
    ],
    "authorSlug": "johann-wolfgang-von-goethe",
    "length": 52,
    "dateAdded": "2021-05-07",
    "dateModified": "2023-04-14"
  }
]

Как видите, сервер возвращает массив с одним элементом, содержащим такие данные, как контент, автор, а также некоторые метаданные, такие как длина, дата добавления и теги.

Quotable также поддерживает поиск по тегу или автору. Например, мы можем получить все цитаты, связанные со счастьем:

curl https://api.quotable.io/quotes?tags=happiness&limit=3

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

Код внешнего интерфейса

Во внешнем интерфейсе мы можем использовать fetch для отправки запроса, а затем визуализировать данные, как и раньше. Нам нужно использовать useEffect в нашем App.jsx:

const App = () => {
  const [quotes, setQuotes] = useState([]);

  useEffect(() => {
    const fetchQuotes = async () => {
      fetch("https://api.quotable.io/quotes/random?limit=3")
        .then((r) => r.json())
        .then(quotes => setQuotes(quotes))

    }
    fetchQuotes();
  }, []);
  return (
        //...
  );
};

Сначала мы заменяем жестко закодированные кавычки. Затем внутри useEffect мы используем fetch, чтобы получить три случайные цитаты. Наконец, как только данные будут получены, страница отобразит эти три кавычки.

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

Это происходит потому, что fetch — асинхронная операция. Когда запрос отправлен, массив quotes все еще пуст. Однако, поскольку для рендеринга мы использовали выражение типа quotes[index].content, мы получаем ошибку при попытке доступа к свойству content неопределенного объекта.

Чтобы решить проблему, мы можем добавить к этому выражению защитное предложение:

return (
  <>
    <div>
      <p className="content">{quotes[index] && quotes[index].content}</p>
      <p>
        <span className="author">&mdash; {quotes[index] && quotes[index].author}</span>
      </p>
    </div>
    <button onClick={clickHandler}>next</button>
  </>
);

Приведенный выше код гарантирует, что quotes[index] существует (не является неопределенным) перед доступом к его свойствам content и author. После этого изменения страница снова работает как положено.

Исправление тестов

Если мы сейчас запустим тесты, используя npm run e2e, мы обнаружим, что все тесты не пройдены. Это связано с тем, что тест по-прежнему ожидает, что данные будут жестко запрограммированными значениями, которые мы изначально использовали в исходном коде. 🤔️ Это может быть немного сложно. С одной стороны, мы хотим, чтобы эти цитаты были случайными, но с другой, нам нужно протестировать цитаты на странице. Как при запуске тестов тестовый код узнает, какие «случайные» данные вернет серверная часть?

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

В Cypress мы можем использовать метод cy.intercept() для перехвата сетевых запросов и возврата заглушек. Это позволяет нашим внешним тестам больше не зависеть от фактических результатов от внутреннего API, а скорее проверять наш код на основе наших собственных предустановленных данных.

Например:

cy.intercept('GET', '/quotes/1', {
  statusCode: 200,
  body: {
    id: 1,
    quote: "Truth can only be found in one place: the code.",
    author: "Robert C. Martin"
  }
});

В приведенном выше коде метод cy.intercept() перехватывает все запросы GET к /quotes/1 и возвращает предварительно установленные данные JSON. После этого все GET-запросы к /quotes/1 будут возвращать наши предустановленные данные, а не фактический возврат от внутреннего API.

Для нашего тестового кода нам необходимо внести следующие изменения в quote-of-the-day.spec.cy.js:

describe("quote of the day spec", () => {
  beforeEach(() => {
    cy.intercept('GET', 'https://api.quotable.io/quotes/random*', {
      statusCode: 200,
      body: quotes
    });
  });

  it("displays a quote", () => {});
  it("clicks next button", () => {});
});

В приведенном выше тестовом коде мы использовали beforeEach: функцию-перехватчик Cypress, которая запускается перед каждым тестовым примером. В этой функции мы используем метод cy.intercept() для перехвата всех запросов GET, указывающих на https://api.quotable.io/quotes/random*, и заставляем их возвращать предустановленные данные quotes.

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

Рефакторинг кода

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

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

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

С защитой тестов мы можем внести некоторые структурные корректировки в нынешнюю

код, не беспокоясь о том, что конечная программа будет повреждена. Это очень важно в программировании.

Мы можем переместить часть, которая извлекает сетевые данные, в отдельный пользовательский хук и сохранить его в новом файле useFetchQuotes.js:

import { useEffect, useState } from "react";

const useFetchQuotes = () => {
  const [quotes, setQuotes] = useState([]);
  const [loading, setLoading] = useState(false);

  const fetchQuotes = () => {
    setLoading(true);
    fetch("https://api.quotable.io/quotes/random?limit=3")
      .then((r) => r.json())
      .then((data) => {
        setLoading(false);
        setQuotes(data);
      })
      .catch((e) => {
        setLoading(false);
      });
  };

  useEffect(() => {
    fetchQuotes();
  }, []);

  return { quotes, loading, fetchQuotes };
};

export { useFetchQuotes };

Этот код определяет пользовательский React Hook с именем useFetchQuotes, который извлекает три случайные цитаты из Quotable API. Вот подробное объяснение этого кода:

  • useState используется для определения состояния внутри функциональных компонентов. quotes — это переменная состояния, используемая для хранения котировок, полученных из API, setQuotes — соответствующая функция установки. loading — это переменная состояния, используемая для указания того, загружаются ли данные, а setLoading — это соответствующая ей функция установки.
  • Функция fetchQuotes извлекает случайные котировки из Quotable API. Когда он начинает получать данные, он устанавливает loading на true. Когда данные успешно извлечены, котировки сохраняются в переменной состояния quotes и присваивается loading значению false. Если во время выборки данных возникает ошибка, для loading устанавливается только значение false.
  • Хук useEffect вызывает функцию fetchQuotes при первой визуализации компонента, поэтому кавычки извлекаются при первой визуализации компонента. [] в качестве массива зависимостей для useEffect означает, что функция fetchQuotes вызывается только при первой визуализации компонента, а не каждый раз при его визуализации.
  • useFetchQuotes в конечном итоге возвращает объект, содержащий переменные состояния quotes и loading, представляющие полученные котировки и статус загрузки соответственно. Это позволяет компонентам, использующим этот пользовательский хук, деструктурировать и использовать эти две переменные состояния.

Затем в App.jsx мы можем напрямую использовать статус загрузки и массив котировок, предоставляемый этим хуком. Для части JSX мы также можем извлечь новый компонент Quote, чтобы сделать код более понятным и кратким:

import React from "react";

const Quote = ({ quote }) => {
  return (
    <div>
      <p className="content">{quote && quote.content}</p>
      <p>
        <span className="author">&mdash; {quote && quote.author}</span>
      </p>
    </div>
  );
};
export { Quote };

Таким образом, обновленный App.jsx будет упрощен до:

const App = () => {
  const [index, setIndex] = useState(0);
  const { loading, quotes } = useFetchQuotes();

  const clickHandler = () => {
    setIndex((index) => (index + 1) % 3);
  };

  return (
    <>
      {loading && <div>loading...</div>}
      {!loading && <Quote quote={quotes[index]} />}
      <button onClick={clickHandler}>next</button>
    </>
  );
};

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

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

Заключение

В этой части мы обсудили тему создания приложений с использованием React и API. Мы рассказали, как использовать Quotable API для получения данных, а также как получать и отображать данные. Познакомившись с сетевыми запросами, мы научились использовать метод cy.intercept(), предоставляемый Cypress, для перехвата и имитации запросов API.

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

Следующий…

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

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