Наши заметки в Sesame о переносе высокоактивной кодовой базы с клиента Apollo на Urql.

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

В этой части мы поделимся нашим опытом с:

  • Отсутствующие элементы (функции Apollo, отсутствующие в Urql)
  • Тестирование и имитация
  • Влияние
  • Заключение

Недостающие части

fetchПодробнее

Apollo предоставляет встроенный API разбивки на страницы, который состоит из двух частей:

Функция fetchMore, которая возвращается хуком useQuery и ожидает новое смещение:

fetchMore({
  variables: {
    offset: pageSize * pageIndex,
  },
});

А также функция слияния, чтобы Аполлон знал, что нужно объединить страницы и обновить кеш.

Слияние можно реализовать, определив его как часть конфигурации кеша или используя функцию updateQuery:

function mergeData(prev, { fetchMoreResult }) => {
  return [...prev, ...fetchMoreResult];
})
fetchMore({
  variables: {
    offset: pageSize * pageIndex,
  },
  updateQuery: mergeData,
});

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

Реализация альтернативной логики разбивки на страницы при сохранении того же API оказалась не слишком сложной (но и не тривиальной).

Начнем с редьюсера.

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

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

Приведенный выше редуктор можно использовать для сохранения внутреннего состояния разбиения на страницы для пользовательского хука usePaginatedQuery:

Теперь мы можем сохранить тот же fetchMore API, что и у Apollo:

const { data, loading, error, fetchMore } = usePaginatedQuery(FEED_QUERY, {limit: 10, offset: 0});

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

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

Давайте быстро пробежимся по тому, что происходит после вызова fetchMore:

// user scrolls / clicks on next page button
fetchMore();
--->
// usePaginatedQuery -> fetchMore increments the page
dispatch({ type: 'incrementPage' });
--->
// query is run with the new offset
const { data: currentPageData } = useCustomQuery(query, {
  variables: {
    offset: newPageIndex * limit
  },
});
--->
// currentPageData is returned - merge results effect is triggered
useEffect(mergeResults, [currentPageData]);
--->
// effect dispatches an update results action with the new page
dispatch({ type: 'updateResults', payload: currentPageData });
--->
// current page is added to full results
fullResults: [...results, ...currentPageData]
---> 
// returned data is updated
data: fullResults

Опрос

Apollo предоставляет готовый удобный способ периодического выполнения запроса с заданным интервалом (опрос).

Urql поддерживал опрос в прошлом, но с тех пор он был удален, как описано в разделе Сравнение Urql с другими клиентами.

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

const [result, executeQuery] = useQuery(...);
useEffect(() => {
  if (!result.fetching) {
    const id = setTimeout(
      () => executeQuery({ requestPolicy: 'network-only' }),
      5000
    );
    return () => clearTimeout(id);
  }
}, [result.fetching, executeQuery]);

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

Хотя это было неудачное время (без каламбура), что эта функция была удалена примерно в то же время, что и наши усилия по переносу, и что потребовалась дополнительная работа, чтобы сделать ее обратной совместимой с Apollo, она должна быть ожидается при стремлении к облегченной библиотеке.

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

Тестирование и насмешка

Как упоминалось ранее, автоматические тесты были одной из основных причин, по которой мы решили двигаться вперед с этой миграцией, поскольку имитация гораздо более прямолинейна, контролируема и проста в реализации с Urql, чем с Apollo.

Давайте углубимся в различия.

Насмешки с Аполлоном

Прямо из документов:

import { GET_DOG_QUERY, Dog } from './dog';
const mocks = [
  {
    request: {
      query: GET_DOG_QUERY,
      variables: {
        name: 'Buck',
      },
    },
    result: {
      data: {
        dog: { id: '1', name: 'Buck', breed: 'bulldog' },
      },
    },
  },
];
it('renders without error', () => {
  const result = render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <Dog name="Buck" />
    </MockedProvider>,
  );
	
	...
});

Пока все хорошо, проблема начинается, когда макет и запрос не выровнены на 100%.

Отсутствующие поля

Если есть хотя бы одно несоответствие между запрошенными полями и моками — Аполлон просто вернет пустой ответ. Нет ошибок. Никаких указаний.

Итак, в приведенном выше примере — если бы GET_DOG_QUERY запрос был

  • Не запрашивать поле breed или
  • Запросить дополнительное поле age — его нет в вымышленном ответе

MockedProvider просто вернет пустой ответ ("data": undefined), и удачи вам в выяснении этого с гораздо большим запросом.

Запросы, которые выполняются более одного раза

В приведенном выше примере — если GET_DOG_QUERY вызывается более одного раза — тест завершится ошибкой с

ERROR: No more mocked responses for...

Поскольку массив mocks должен содержать новый экземпляр при каждом вызове запроса.

Несоответствие переменных

Такая же непрозрачная ошибка No more mocked responses for появлялась бы при несоответствии переменных.

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

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

Насмешка с Urql

У Urql гораздо более прямой подход.

Никаких черных ящиков, никаких расплывчатых сообщений об ошибках (или их отсутствия).

Просто старый добрый, полностью контролируемый и уверенный jest.fn макет

Издевательство над одним запросом

Таким образом, эквивалентом приведенного выше примера с Аполлоном будет:

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

Или мы могли бы последовать подходу Аполлона и сделать утверждения частью макета:

Имитация нескольких запросов

Приведенный выше пример идеален, когда ваш компонент/хук использует только один запрос.

Но что происходит, когда он использует более одного?

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

const executeQueryImplementation = ({ query, variables }) => {
  const resolveDogResponse = () => {
    switch (variables.slug) {
      case 'Buck':
        return fromValue(buckResult);
      case 'Snuffles':
        return fromValue(snufflesResult);
    }
  };
  switch (query) {
    case GET_DOG_QUERY:
      return resolveDogResponse();
    case GET_CAT_QUERY:
      return fromValue(catResult);
    default:
      return fromValue({});
  }
};

Обратите внимание, что в этом случае утверждения запроса/переменных являются неявными, как и в случае с Apollo.

Важное отличие в том, что вы можете очень легко отладить, если ответ не соответствует ожидаемому.

Постепенный перенос тестов

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

Наличие отдельных наборов тестов Apollo/Urql (несмотря на возможное дублирование кода) для каждого компонента контейнера казалось наиболее удобным в сопровождении и чистке подходом.

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

Затем мы запускали обе версии по отдельности, чтобы иметь четкое представление о том, в какой библиотеке были регрессии.

- name: 'Test - Apollo'
  run: |
    yarn test
- name: 'Test - Urql'
  run: |
    URQL_ENABLED=true yarn test

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

Как только у нас было полное покрытие как для Urql, так и для Apollo, мы, наконец, установили флаг URQL_ENABLED в значение true в рабочей среде.

Влияние

Как упоминалось во введении, нашими основными целями миграции были:

  • Уменьшение размера основного пакета
  • Более легкое издевательство и тестирование
  • Первоклассная интеграция с NextJS

И действительно, в результатах оказалось, как мы и ожидали, и даже больше.

Размер пакета

Размер нашего основного пакета, который является общим для всех страниц, был уменьшен с 300,87 КБ до 268,32 КБ, сэкономлено более 30 КБ сжатого JS.

Время выполнения теста CI

Мы не ожидали такого результата, но время выполнения теста сократилось более чем на 50%(!), примерно с 2 минут с Apollo до менее 1 минуты с Urql.

Несмотря на то, что большинство наших тестов по-прежнему имитировали состояние загрузки:

import { delay, fromValue, pipe } from 'wonka';
export const delayed = data => pipe(fromValue(data), delay(0));

Оказывается, потоковый подход Urql намного быстрее.

Время разработки теста

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

Интеграция с NextJS

Apollo всегда был проблемой для любого повышения версии NextJS (достойного отдельного поста в блоге).

Официально поддерживаемый пакет next-urql оказался настолько надежным, что мы не столкнулись с проблемами при следующей интеграции Urql при обновлении двух последних основных версий (Next 11 и 12).

Заключение

С точки зрения 6-месячной перспективы полной миграции с Аполлона на Уркль мы можем с достаточной уверенностью сказать:

Если вас волнует размер пакета, тестирование и имитация и особенно если вы используете NextJS, предполагая, что ваше приложение не использует слишком много Расширенные функции Apollo — скорее всего, Urql — лучший вариант для вас.