С момента выпуска GraphQL и Relay прошел почти год. С тех пор GraphQL позиционируется как чистый декларативный преемник REST - и не зря. С пользовательским интерфейсом GraphiQL для запросов, встроенной схемой, которая устраняет необходимость в ORM, и единственной конечной точкой, можно с уверенностью сказать, что GraphQL является хитом. Напротив, Relay не испытывал такого же энтузиазма, как его серверный аналог.

Означает ли это, что это плохой код? Точно нет! Это подвиг современной инженерии, созданный невероятно умными людьми для невероятно умных людей; идеи, лежащие в основе этого, просто фантастические. Чтобы читатель не упустил этот момент (а также потому, что я только начал смотреть Кремниевую долину), я буду предварять каждую критику, сначала заявляя, что R elay i s g reat, b ut, y a'know (Rigby).

  • Ригби, Redux - отличный способ управлять локальным состоянием, и я хотел бы использовать его также для управления состоянием домена.
  • Ригби, клиентский кеш не должен требовать больших изменений сервера. Если это способствует более эффективному обновлению, то почему бы и нет? но, надеюсь, клиентский кеш работает с любым сервером GraphQL
  • Ригби, мне нужен только клиентский кеш, а не полная переписывание моих маршрутов и контейнеров, тесно связанных с React.
  • Здесь есть Rigby, HTTP / 2 и веб-сокеты, поэтому не очень важно, чтобы я получил всю информацию за 1 поездку. Частые поездки с меньшей полезной нагрузкой могут даже улучшить UX
  • Ригби, мне не нужно быть экспертом в теории графов, чтобы понять, как написать мутацию
  • Ригби: при изменении массива иногда новому документу требуется больше логики, чем добавить или добавить.
  • Ригби, если я знаю, на какие запросы влияет мутация, мне не нужно писать толстый запрос.
  • Ригби, оптимистичные обновления настолько похожи на обновления сервера, что написание одного и того же дважды меня огорчает
  • Ригби, мой проект не размером с facebook, поэтому размер Relay в сжатом виде больше, чем совокупная экономия полезной нагрузки, которую он обеспечивает.

Хорошо, достаточно обоснования того, почему нам нужно что-то новое; давайте сосредоточимся на решении. Я хотел взять лучшие части Relay (сотни часов размышлений) и лучшие части Redux (простой в использовании код и дружественный API) и объединить их. В результате получился пакет, который я назвал Cashay и разместил на github. Я начал писать это некоторое время назад, но жизнь была слишком веселой, чтобы проводить за компьютером. Прекрасно зная, что новый пакет привлечет море сильных сторонников усталости (я скажу это… нытики усталости JS раздражают больше, чем поклонники Джастина Бибера), я решил пройти через каждую болевую точку, которая у меня была в Relay, и систематически объясните, как и почему Cashay делает это по-другому.

Проблема №1: Философия Unix: делайте одно и делайте это хорошо

Кэш - это инструмент, который жертвует небольшим объемом памяти в обмен на более быстрый результат. Вот и все. Это все, что пытается сделать Кашей. Он берет то, что вы хотите, видит то, что у вас уже есть, идет и получает разницу. Действительно, очень просто. Хочешь увидеть, что у тебя есть? Взломайте redux-devtools. Вы увидите все свои переменные и нормализованные по схеме данные, ожидающие вашего нетерпеливого взгляда. Кто-то сказал сериализуемые постоянные данные? Нет? Хорошо, тогда только я ...

Совместное размещение запросов данных с требованиями к данным - это фантастика, но это не место в кеше. Кэш должен быть независимым от внешнего интерфейса; colocation нет. Если вы больше заботитесь о колокации, чем о кешировании, я настоятельно рекомендую адреналин, и в будущем, возможно, Cashay и адреналин будут хорошо сочетаться друг с другом. В любом случае та же проблема влияет и на серверный CSS, и обе проблемы могут быть решены одним и тем же методом. Узнать больше:



Проблема # 2: использование конечной точки ванильного GraphQL

Сосредоточимся на сервере. Клиентское программное обеспечение, требующее изменений на вашем сервере, может вызывать некоторые красные флажки, но Relay делает это по уважительной причине: ребра и узлы. В теории графов все состоит из ребер (отношений) и узлов (объектов). Теперь я не новичок в графах, я даже написал несколько пакетов npm для очень непонятных эвристик двудольных графов, которые никто не использует, но то, что графы мощные, не означает, что они необходимы. Итак, давайте посмотрим, как мы можем выгрузить курсоры и pageInfo (hasNextPage, hasPreviousPage) с краев. Оказывается, это не так уж сложно.

Сначала займемся курсорами. Relay на 100% понял это правильно, используя разбиение на страницы с помощью курсора. Нет никакой логической причины переходить на страницу 3. Если это то, что делают ваши пользователи, это явный признак того, что вы не можете предложить правильный запрос, сортировку или фильтр. Я часто задаюсь вопросом, почему Google до сих пор делает это (хотя Google Images - это бесконечная прокрутка) ... может быть, они просто признают, что люди не любят перемен? Тем не менее, Relay требует отношения один к одному между курсорами и документами. Таким образом, если вам нужно 10 документов после документа № 5, вы получите курсор для № 5 и отправите запрос для следующих 10. Итак, если мы предположим, что документ всегда имеет один и тот же курсор, независимо от запроса (а я не могу придумать причину, по которой это ложь), тогда мы просто помещаем курсор на сам документ. В будущем эти метаданные можно будет даже прикрепить через аннотацию GraphQL. Это может быть отметка времени, UUID, как угодно. Чтобы узнать больше, посмотрите, как это делает Disqus: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api

Во-вторых, займемся pageInfo. Спецификация Relay для него чертовски хрупкая:

hasPreviousPage имеет смысл, только если включен last, в противном случае он всегда имеет значение false.

Что это значит? Что ж, если я нажимаю «следующая страница» и оказываюсь на странице 2, тогда hasPreviousPage ложно. Шутки в сторону. Так как же нам его улучшить? Опять же, довольно просто. pageInfo должен быть получен из того, сколько данных у нас есть в местном штате. Если я показываю документы 1–10 и у меня есть 11-й документ локально, тогда hasNextPage должно быть истинным. Так как же нам этого добиться?

Вариант №: 1 Запросите еще и скройте остальное в логике приложения

Вы можете сделать это без какой-либо логики вашего сервера или кеша (то есть на уровне модели представления). Если вы хотите 5, попросите 6, а последний станет белым, чтобы пользователь понял, что ему нужно запросить больше. Когда они запрашивают больше, вы отправляете свой запрос, начиная с курсора потускневшего, затем уменьшаете его, а затем ждете, пока появится следующая страница. Когда больше нет, вы не увидите затененный документ.

Дополнительная логика приложения не для вас? Не возражаете изменить сервер GraphQL? Ok…

Вариант №2: попросите ваш сервер отправить n + 1 документов

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

Поговорим о прекрасном UX! Но я знаю, что есть какая-то фигня, говорящая что-то вроде: «Я очень важный человек, и я не могу позволить себе послать по сети, возможно, бесполезные 72 байта». Действительно? … правда? Хорошо.

Вариант № 3. Попросите ваш сервер отправить дополнительный ноль

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

Это решает нашу проблему с краями, но как насчет узлов? Relay предлагает действительно умную идею с их интерфейсом node. Вы отправляете тип и идентификатор (в непрозрачной строке base64), и он извлекает этот документ. Это похоже на запрос getPostById, но он работает для сообщений, комментариев и всего остального в вашей схеме. Давайте расширим эту идею:

  1. Вызовите запрос getTop5PostIDs, который возвращает массив из 5 идентификаторов.
  2. Отфильтруйте этот список по идентификаторам, которые у вас уже есть
  3. Вызов второго запроса getPostsByIDs, передаваемого в отфильтрованном списке.

Это обеспечивает сверхэффективную загрузку за счет двух сетевых запросов. Итак, теперь у нас есть варианты: либо получить все за один запрос (быстрее, полезная нагрузка больше), либо использовать метод прогнозирующей выборки. Например, если пользователь наводит курсор на кнопку, выполните getTop5PostIDs. Если они не нажмут на нее, вы потеряете только несколько байтов (массив, полный идентификаторов). Если они нажимают на нее, вы эффективно получаете сообщения, которых у вас еще нет, с помощью getPostsByIDs. Отделение кеша от слоя представления для победы!

И на этом проблема с сервером решена! Больше никаких серверов с поддержкой Relay, только ваша стандартная схема GraphQL. В будущем это также может работать со сторонними сервисами, которые возвращают только 1 курсор на страницу (хотя и менее эффективно).

Я буду работать над этими функциями, как только у меня будет время… или это потребуется для оплачиваемого проекта. *Подсказка Подсказка*

Проблема # 3: мутации

Ригби, API мутаций для Relay уродлив. Но не зря. 80% сложности создания клиентского кеша заключается в мутации. Во многом это связано с огромным количеством переменных. Например, предположим, вы удаляете сообщение, которое было в Top5Posts. Сохраняете ли вы этот список как есть с документом, который больше не существует? Вы показываете только оставшиеся 4 сообщения? Вы заполняете дыру следующим лучшим постом, который у вас есть на местном уровне? Вы говорите, черт возьми, и запросите все? Ответ, как любит говорить каждый измученный до локтя профессора колледжа, - это зависит от обстоятельств.

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

Я говорю, к черту оба.

  1. Если каждый запрос обрабатывает мутацию по-разному, позвольте разработчику решить, как именно конкретный запрос должен обрабатывать конкретную мутацию. Нет более сложной логики getConfigs и более 100 мутаций LOC.
  2. Схема клиента не такая уж большая. Cashay делает его еще меньше. Размер gzip-схемы клиентской схемы для приложения среднего размера составляет ‹10 КБ gzip, и наличие схемы на клиенте дает некоторые большие преимущества.

После того, как вы включите clientSchema и напишете по одному обработчику на каждый запрос-мутацию, жизнь станет проще. Например, вам не нужно писать fatQueries; Cashay пишет их для вас (я объясню, как в одном из следующих постов). Вы также можете использовать один и тот же обработчик для оптимистичного пользовательского интерфейса и изменения вашего сервера. Лучше всего то, что нет предела тому, как мутация может повлиять на запрос. Хотите вставить новый документ в середину массива? Хорошо. Если этот новый документ содержит X, можете ли вы отредактировать каждый документ в другом массиве, а затем отменить его? Странно, но верно. Должно ли удаление документа вызывать аннулирование всего запроса? Ты главный!

Решение

Все эти разговоры бесполезны без POC, поэтому я создал Cashay-Playground. Давайте посмотрим, как разбиение на страницы узнает, когда больше нет документов для получения. Или как получаются комментарии. Или как вы можете добавить комментарий. Установите redux devtools extension и посмотрите, как Cashay хранит все это в вашем состоянии.

Под капотом Cashay кэширует данные после того, как извлекает и денормализует их из состояния. Он также управляет зависимостями для этих денормализованных запросов, поэтому нет ненужного повторного рендеринга. Конечным результатом является чертовски простой инструмент, обладающий высокой производительностью и простым в использовании кодом. Что интересно в этом, так это то, что со всей информацией, хранящейся в вашем состоянии, упорядоченной по типу GraphQL, создание инструмента администрирования в стиле Django чрезвычайно просто - просто выведите все свое состояние на экран. Скоро я создам пакет именно для этого.

Заключительные замечания

Моя цель здесь не в том, чтобы гадить на Relay. Это великолепно, но слишком сложно. Реальность такова, что Relay создан для решения проблемы, специфичной для facebook, и эта проблема не так распространена, как та, которую решает GraphQL, поэтому появятся разные решения. Он отлично работает для некоторых, а некоторые даже могут предпочесть его Cashay. В конце концов, facebook вероятно более авторитетен, чем какой-нибудь чувак, живущий в Мексике, выкладывающий код по выходным. Опять же, представление о том, что небольшой магазин может делать что-то лучше, чем мегакорпорация, вероятно, является причиной того, что у половины из нас есть работа. Вот почему мы будем использовать Cashay в Parabol для создания нашей новой службы управления проектами с открытым кодом: Action. Он не только убережет вас от скучных встреч, но и служит примером веб-приложения производственного качества, в котором так отчаянно нуждается сообщество JavaScript.

В конце концов, я хочу начать разговор, чтобы обсудить, как мы можем улучшить кеши клиентов. Через 5 лет ни один из них не станет излюбленным клиентом. В конечном итоге нам понадобится что-то, что может обрабатывать TTL для сохранения редко запрашиваемых данных, сначала автономно, подписок (над этим мы работаем!), и подписки CmRDT (например, совместное редактирование документов, например swarm.js). Это действительно захватывающие времена для JavaScript. Cashay - это не окончательный ответ, но, возможно, это будет ступенька на пути к этому. В конце концов, прелесть OSS в том, что независимо от того, кто все делает правильно, мы все выигрываем.