Потратив некоторое время на изучение и реализацию стека на основе graphQL, я поражен тем, насколько легко быстро разрабатывать сложные приложения. Я особенно впечатлен тем, как запросы graphQL позволяют разработчикам интерфейсов получать доступ к данным, которые им нужны, в нескольких связанных типах данных, не требуя отдельной конечной точки для каждого запроса.
Тем не менее, стек не лишен болевых точек. Во внешнем интерфейсе одна из этих проблем связана с признанием недействительными данных, хранящихся в кэше Apollo, когда эти данные были изменены на стороне сервера. это может быть особенно проблематичным при работе с запросами с разбивкой на страницы.
В этой статье мы рассмотрим несколько примеров простого приложения Next.js, которое использует Apollo для извлечения данных из еще более простой серверной части graphQL Yoga / Prisma. Если вы хотите следовать этим примерам, вы можете скачать стартовые файлы здесь, в главной ветке: «https://github.com/martinseanhunt/padgey-nation-fronten lived
Этот интерфейс связан с демо-сервером, который размещен на now.sh, поэтому вы должны просто иметь возможность запускать npm install
и npm run dev
, и все будет в порядке!
Эта проблема
При использовании клиента Apollo ответ от любого запроса, отправляемого на сервер, кэшируется локально (если мы явно не укажем ему не к которому мы вернемся позже). Это отлично подходит для ускорения работы нашего приложения, поскольку Apollo будет извлекать запрошенные данные из кеша, а не запрашивать у сервера данные, которые у нас уже есть.
Однако изначально, когда мы вносим изменения в данные на нашем сервере с помощью мутации graphQL, это изменение не будет отражено в состоянии приложения, если мы не обновим страницу.
refetchQueries
Apollo действительно предоставляет нам удобную функцию под названием refetchQueries
, в которую можно передать массив запросов, требующих повторной выборки, после того, как мы выполним мутацию. Однако любые запросы, которые мы передаем refetchQueries
, будут вызываться, как только мутация закончится, это далеко не идеально для данных с разбивкой на страницы. Предполагая, что у нас есть 100 страниц данных, которые были заполнены отдельными запросами, использование refetchQueries
отправит на наш сервер 100 сетевых запросов одновременно.
readQuery и writeQuery
Мы также можем читать и записывать данные в кэш Apollo, используя функции readQuery
и writeQuery
, которые во многих случаях идеально подходят для работы с такими операциями. Мы можем просто вставить наш недавно созданный элемент в или отфильтровать удаленный элемент из нашего кеша, мгновенно отображая обновления на экране. Однако этот подход может вызвать проблемы при работе с запросами с разбивкой на страницы в зависимости от того, какие страницы пользователь кэшировал перед тем, как инициировать мутацию. Это может быть особенно проблематично, когда вносятся несколько изменений.
Например, вот проблема, с которой мы можем столкнуться при удалении элемента из кеша вручную с помощью функции обновления в нашем Create.js
компоненте:
Вот наглядный пример одной из проблем, которые это может создать:
Мы сталкиваемся с аналогичной проблемой при добавлении вновь созданных элементов в кеш таким образом.
Здесь мы также сталкиваемся с хитрой ошибкой. Поскольку мы всегда хотим добавить данные для нового элемента на страницу 1, и если эти кэшированные данные не существуют, мы получаем ошибку при вызове кеша для них.
Если вы хотите изучить кодовую базу для этого подхода, вы можете найти ее здесь: https://github.com/martinseanhunt/padgey-nation-frontend/tree/update-cache-directly
fetchPolicy
Другое возможное решение - установить fetchPolicy
наших начальных запросов равным ‘network-only’
. Это означает, что данные для текущей страницы всегда будут поступать с сервера. Этот подход работает отлично, однако вы теряете мгновенное ощущение, которое возникает при обслуживании кэшированных данных, когда они доступны, и на сервер будет отправлено больше запросов, чем необходимо.
<Query query={GET_LIST_ITEMS_QUERY} fetchPolicy='network-only' variables={{ skip: (page - 1) * perPage }} >
Куда мы отправимся отсюда ?
Эта проблема известна команде Apollo, и в настоящее время они работают над решением, которое станет частью API Apollo. Однако, если вы, как и я, жаждете временного решения, то у вас есть несколько вариантов!
Временное рабочее решение…
К счастью, есть очень простой обходной путь, который должен работать в большинстве случаев . Когда у нас есть доступ к объекту кеша, мы можем вызвать cache.data.delete(key)
, где key
- это ключ, который Apollo использует для хранения данных для определенного элемента. И запись будет полностью удалена из кеша.
Вы можете видеть, что Apollo берет typename
нашего конкретного фрагмента данных и добавляет уникальный идентификатор для создания key
.
Это означает, что в наших функциях обновления мы можем просто перебрать наш кеш и удалить все записи, где key
начинается с ListItem:
, удаляя все наши запросы с разбивкой на страницы из кеша и вызывая повторную выборку при следующем запросе страницы.
Чтобы это работало в нашем приложении, мы можем сделать следующее:
1) Создайте вспомогательную функцию, которая будет обрабатывать удаление наших запросов с разбивкой на страницы из кеша.
2) Обновите нашу Мутацию в Create.js, чтобы она вызывала нашу новую вспомогательную функцию при обновлении при создании нового элемента списка. (не забудьте также повторно загрузить запрос на подключение)
3) В Index.js деструктурируйте refetch
из объекта, предоставленного для рендеринга функции prop в нашем запросе, и передайте его в ListItem.js.
Мы передаем эту функцию повторной выборки в ListItems.js, чтобы мы могли вызывать повторную выборку текущей страницы при удалении элемента. В противном случае в текущей версии Apollo компонент Index.js не будет повторно отрисован, и запрос не будет повторно запущен. Я считаю, что причина, по которой использование cache.data.delete
не запускает повторную визуализацию родительских компонентов, заключается в том, что мы напрямую изменяем объект данных кеша, но в этом я не уверен на 100%.
4) Создайте и вызовите нашу функцию обновления в ListItems.js, когда listItem будет удален (снова не забудьте повторно загрузить наш запрос на подключение к странице).
5) Убедитесь, что у вас есть все правильные операции импорта и экспорта для запросов и вспомогательных функций и ... Успех!
Теперь у нас есть работающее приложение, в котором вы можете добавлять и удалять элементы, а данные страницы будут запрашиваться с сервера только при необходимости.
Полная кодовая база для этого решения доступна здесь: https://github.com/martinseanhunt/padgey-nation-frontend/tree/simple-solution
Вы можете увидеть полную разницу между стартовыми файлами и рабочим решением здесь: https://github.com/martinseanhunt/padgey-nation-frontend/compare/master...simple-solution
А живую демонстрацию вы можете увидеть на https://padgey-nation.now.sh
5) Недостатки этого временного решения
Одна из проблем этого обходного пути заключается в том, что любые другие кэшированные данные с именем типа ListItem будут удалены из локального кеша и потребуют повторной выборки, даже если они не являются частью наших запросов с разбивкой на страницы. Например, если бы у нас был вторичный список «помеченных» элементов списка, который нам не обязательно обновлять, когда элемент обновляется или удаляется, у нас не было бы возможности различать эти два, чтобы эти запросы также были обновлены. с сервера при следующем запросе.
Поскольку нам приходится отображать весь наш кеш каждый раз, когда добавляется или удаляется запись, я не уверен, вызовет ли это проблемы с производительностью большого и сложного внешнего интерфейса.
Другая проблема заключается в том, что мы напрямую изменяем наш локальный кеш, и это потенциально может иметь неожиданные побочные последствия (как мы видели, когда приходилось вручную вызывать refetch
после удаления элемента).
В целом, хотя я думаю, что это отличное временное решение для большинства случаев использования, пока Apollo не сможет предложить лучший способ.
Я знаю еще один способ
Я столкнулся с вышеуказанным обходным путем, когда кто-то упомянул об этом несколько дней назад в проблеме с github, которую я отслеживал (https://github.com/apollographql/apollo-feature-requests/issues/4#issuecomment-431119231 ). В целом, я думаю, что на данный момент это более простое и элегантное решение. Однако в тот момент, когда я столкнулся с вышеуказанным методом, я уже работал над решением, которое не требует сопоставления всего кеша и не изменяет напрямую объект кеша. Также этот метод БУДЕТ работать, если у вас есть элементы с таким же именем в других частях приложения, которые не нужно обновлять.
Однако это решение немного сложнее по своей природе, и я считаю, что его еще можно улучшить.
Общий обзор идеи состоит в том, что мы сохраняем в локальном состоянии массив номеров страниц, которые необходимо обновить, и в зависимости от того, находится ли текущая запрашиваемая страница в массиве, мы либо отправляем запрос с fetchPolicy
из ‘network-only’
, либо с ‘cache-first’
Вот две основные функции, задействованные в этой работе:
1) setPagesToBeRefreshed - это вызывается при обновлении нашими мутациями, ответственными за создание и удаление элементов списка
2) getItemsForPage - это вызывается в различных точках жизненного цикла Index.js, чтобы решить, искать ли наши данные в кеше или идти прямо на сервер.
Я не буду вдаваться в подробности здесь, так как считаю, что первый метод является более простым и применимым в большинстве случаев. Однако, если есть какие-либо требования, я был бы счастлив написать продолжение этого и более подробно объяснить второе решение.
Если вам интересно, вы можете изучить полную базу кода здесь: https://github.com/martinseanhunt/padgey-nation-frontend/tree/refetch-queries-only-when-needed
Разницу между начальными файлами и конечным продуктом можно увидеть здесь: https://github.com/martinseanhunt/padgey-nation-frontend/compare/master...refetch-queries-only-when-needed
Подведение итогов
Разобраться во всем этом было настоящим путешествием. Я все еще новичок в экосистеме graphQL, но очень рад увидеть, куда нас приведут эти технологии. Если у кого-то есть конструктивная критика или отзыв об этой статье, я буду рад это услышать.
Я с нетерпением жду официального решения этой проблемы, но в то же время я искренне надеюсь, что это может быть кому-то полезно.
Удачного кодирования!