ПРИМЕЧАНИЕ. В этом руководстве основное внимание уделяется синтаксису, изложенному в спецификации GraphQL, для запроса и обновления ваших данных в клиенте. Я скоро опубликую статью о том, как написать серверную часть с помощью Apollo, а потом обновлю эту статью ссылкой. Приносим извинения за то, что это такая длинная статья - просто нам нужно изучить так много материала. Надеюсь, это поможет! знак равно

Я не собираюсь приукрашивать это, изучение всего «пакета» GraphQL (синтаксис, реализация с помощью Express и, наконец, реализация с помощью Apollo) было трудным, болезненным и трудоемким. 🔪

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

И все же, несмотря на все сказанное, оно того стоит по двум основным причинам:

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

Мораль этой истории такова: это того стоит.

Зачем использовать GraphQL

Есть много причин принять GraphQL, и я не буду рассматривать их все, но вот краткий список:

  • Сводит несколько запросов к разным частям данных в один запрос
  • GraphQL самодокументируется, поэтому со временем вы сможете увидеть, какие запросы вам доступны и какие параметры им требуются.
  • Легче увидеть, какие именно данные или изменения запрашиваются в схеме GraphQL, чем в вызове REST.

Клиентская и серверная GraphQL

GraphQL - это формальный шаблон, и этот шаблон реализуется различными библиотеками (например, express-graphql, graphql, apollo-client, apollo-client-micro, apollo-boost, graphql-tag). », Graphql-tools и многие другие). По сути, очень легко запутаться и сбиться с толку от того, сколько библиотек существует и что они все чертовски делают, не говоря уже о том, как их все правильно использовать.

Так что давайте на секунду потупим ...

Любое приложение, реализующее GraphQL, состоит из двух основных частей - клиента и сервера.

Клиент

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

Сервер

Сервер получает запросы от Клиента и либо извлекает и возвращает данные из запроса, либо вносит некоторые изменения в данные и возвращает их в результате запроса на изменение.

Вот что интересно: вам нужно написать другой тип кода GraphQL на клиенте, чем на сервере, и почти в каждой ситуации вам понадобится библиотека (например, Apollo), реализующая стандарт GraphQL. для написания как клиентского, так и серверного кода GraphQL.

Это сильно отличается от REST, который является просто шаблоном и который вы можете реализовать с помощью соглашений об именах, HTTP-запросов и объектов Express request и response.

Слово об Аполлоне

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

Apollo предоставляет вам следующее:

  • Способ отправки запросов и мутаций от Клиента к Серверу (apollo-client, apollo-boost)
  • Способ для Сервера получать эти запросы и отправлять данные обратно Клиенту (apollo-server, apollo-server-micro)
  • Способ извлечь эти данные в клиенте и отобразить их в ваших компонентах.
  • Способ обновления ваших компонентов при мутации
  • Способ проверить, обрабатывается ли запрос все еще, успешно ли он или произошла ли ошибка

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

Тестовая среда

В этой статье мы будем использовать Github GraphQL Explorer. Иногда его называют Graph i QL (обратите внимание на i). Вам нужно будет войти в систему с учетной записью Github, и будьте осторожны - он работает с оперативными производственными данными, поэтому, если вы вносите изменения (мутации) через проводник, вы я смогу увидеть эти изменения в вашей фактической учетной записи / репозиториях Github, к которым у вас есть доступ.

Вы можете найти Github GraphQL Explorer здесь: https://developer.github.com/v4/explorer/

Запросы и мутации

На случай, если вы пропустили это выше, запрос в GraphQL - это запрос данных, а мутация - запрос на создание или изменение данных на стороне сервера. Обновленная версия данных обычно возвращается клиенту после завершения обработки мутации.

Комментарии

Вы можете закомментировать строку кода с помощью символа решетки #. См. ниже.

Основные запросы

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

Если вы вошли в Github GraphQL Explorer, вам, скорее всего, будет предложен начальный запрос:

{
  # This is a comment
  viewer {
    login
  }
}

Нажмите кнопку воспроизведения посередине или Cmd + Enter, чтобы выполнить запрос. Вы увидите результаты в окне с правой стороны.

Этот запрос извлекает объект viewer, который является в настоящее время аутентифицированным пользователем Github (то есть вами), и извлекает имя login из этого объекта.

Чтобы прояснить некоторую терминологию: конечные части данных, которые нам нужны (например, login), называются полями, а контейнеры, которые обтекают эти поля, называются объектами. В приведенном выше примере viewer упоминается как объект.

В GraphQL вы не можете просто запросить объект, вам также необходимо запросить некоторые поля, которые он содержит. Подробнее об этом позже.

Документы GraphQL Explorer

Где-то в правой части экрана вы увидите вкладку Документы. Щелкните по нему, и откроется вкладка документации API. С его помощью вы можете увидеть все доступные запросы и изменения, которые вы можете сделать, какие поля каждое будет возвращать и какие поля обязательны. Изначально использовать это королевская заноза в заднице, но продолжайте практиковаться с этим, и вы научитесь этому. Хорошее упражнение - попытаться найти viewer и login из приведенного выше запроса. Позже это поможет вам ориентироваться как в собственных, так и в чужих API.

Именованные запросы

Текущий запрос анонимный. GraphQL понимает, что это запрос, а не мутация, но мы можем дополнительно указать это.

query {
  viewer {
    login
  }
}

Ключевое слово query выше сообщает GraphQL, что мы работаем с запросом. Позже мы добавим к запросу префикс mutation, которое сообщит GraphQL, что мы хотим изменить некоторые данные. Вот как выглядит простая мутация. Это не будет работать в Github GraphQL Explorer - это просто пример:

mutation updateBook(name: "Big Fish", author: "Ben Grunfeld") {
  id
}

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

query LoginName {
  viewer {
    login
  }
}

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

mutation UpdateBook($name: String!, $author: String!) {
  updateBook(name: $name, author: $author) {
    id
    name
    author
  }
}

Аргументы

Некоторые запросы принимают аргументы. Они могут потребоваться, а могут и не потребоваться - вы должны прочитать документацию, чтобы проверить. Если требуется аргумент или поле, после него будет восклицательный знак !, например name: String!.

Давайте посмотрим на запрос, который требует аргумента в проводнике: repositoryOwner.

{
  repositoryOwner(login: "bengrunfeld") {
    id
    url
  }
}

Теперь, если мы заглянем в документацию, мы увидим, что аргумент login является строковым и является обязательным (см. ! после него).

Типы возврата, не допускающие NULL

Разработчик схемы, который пишет код GraphQL на стороне сервера, также может определить, что некоторые возвращаемые типы не допускают значения NULL. Например. viewer: User!

Если вы запросите объект viewer, он не сможет вернуть null. Мы можем видеть это по восклицательному знаку после типа возвращаемого значения User.

Использование переменных в запросах аргументов

В запросах можно определять переменные, которые используются в качестве аргументов. Это полезно, потому что вы часто не знаете, каковы ваши критерии фильтрации во время написания кода запроса. Например, Мне нужны все книги, написанные автором: ______. Эту переменную можно использовать для заполнения поля позже.

Затем вы можете предоставить эти переменные либо через внешний код, либо как JSON в GraphQL Explorer. Мы собираемся использовать только GraphQL Explorer, потому что в противном случае мне пришлось бы углубиться в подробный код Apollo, о котором я расскажу в другой статье.

Давайте возьмем приведенный выше пример, но вместо «bengrunfeld» для входа в систему мы поместим переменную, а затем мы сможем заменить ее на то, что захотим позже.

query RepoInfo($owner: String!) {
  repositoryOwner(login: $owner) {
    id
    url
  }
}

Шутка ли, я совершенно не понял этот код с первого раза. Знаки доллара действительно запутали мой мозг.

У нас есть именованный запрос. Название не имеет значения - это просто способ понять, что делает этот запрос. После имени запроса мы видим, что эта функция принимает переменную с именем $owner, которая ДОЛЖНА иметь тип String и не должна быть нулевой.

Затем внутри этой функции мы запрашиваем объект repositoryOwner и запрашиваем данные для записи со значением логина $owner. Затем мы просим id и url этой записи.

Смущает (для некоторых) то, что $owner используется дважды. Просто подумайте о первом использовании, таком как объявление параметров, которые принимает функция, и втором использовании, как о том, когда оно фактически выполняется в запросе. Он похож на следующий код:

const getRepoInfo = owner => getRepositoryOwner(owner)
const { id, url } = getRepoInfo("bengrunfeld")

Таким образом, переменная $owner передается из объявления параметров именованного запроса в аргумент, используемый объектом repositoryOwner. Надеюсь, это достаточно хорошо объясняет ...

Чтобы предоставить переменную в GraphQL Explorer, в левом нижнем углу экрана должно быть окно с надписью Переменные запроса. Если вы используете другую установку GraphiQL, а ее там нет, вы должны иметь возможность нажать где-нибудь кнопку, чтобы она отобразилась.

В окне вы можете указать переменные запроса в формате JSON, например:

{
  "owner": "facebook"
}

Псевдонимы

Но что, если мы захотим составить два похожих запроса с разными аргументами? Например. нам нужны данные repositoryOwner для bengrunfeld и те же данные для facebook. Если мы просто повторим запрос как есть и изменим аргумент (см. Ниже), GraphQL выдаст нам неприятную ошибку.

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

{
  personalInfo: repositoryOwner(login: "bengrunfeld") {
    id
    url
  }
  fbInfo: repositoryOwner(login: "facebook") {
    id
    url
  }
}

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

Пришло время поговорить о типах

Безопасность типов стала очень важной задачей в мире Javascript за последние несколько лет. Я оставлю это на усмотрение других «великих умов», чтобы спорить, помогает ли это больше, чем причиняет вред 😕, но общее мнение таково, что промышленность должна внедрить проверку типов во всем, что она делает (пожалуйста, не создавайте Typescript для CSS. Просто ... пожалуйста не надо).

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

Каждый фрагмент данных в GraphQL должен быть представлен как один из следующих типов:

Скалярные типы

  • Целое число
  • Плавать
  • Нить
  • Логический
  • ID
  • Нулевой
  • Enum

Нескалярные типы

  • Список
  • Объект

Тип список или объект, в свою очередь, должен содержать фрагменты данных, которые относятся к скалярным типам. Запрос завершается и возвращается только тогда, когда каждое запрошенное поле разрешается в скалярный тип. Это из GraphQL Docs:

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

Перечисления

Если вы такой же разработчик-самоучка, как я, то вы, вероятно, не прошли курс информатики и, вероятно, не знаете, что такое Enum. Вы знаете, что такое все остальные типы - ну, список, вероятно, всего лишь массив, верно? Верный! Но что, черт возьми, такое Enum?

Enum расшифровывается как «Enumerated Type», и в основном это означает, что входные данные ДОЛЖНЫ быть одним из допустимых значений, которые были заложены в коде Server GraphQL. Здесь мы объявляем Enum на стороне сервера:

enum SwitchState {
  ON
  OF
}
type Query {
  switches(state: SwitchState): [Switch]
}

Приведенный выше код возвращает список переключателей, отфильтрованных по их состоянию, т.е. находятся ли они в положении ВКЛ или ВЫКЛ.

Теперь это имеет смысл. Если мы имеем дело с данными, которые представляют состояние переключателя света, он может быть ВКЛ или ВЫКЛ. Да, некоторые вещи на самом деле являются двоичными 😉. Однако у вас может быть более двух вариантов в Enum, но применяются те же правила. Например. вы не можете указать значение НЕ РЕШЕНО для Switch.

Вот пример использования Enum в коде на стороне клиента.

{
  switches(state: ON)
}

Или используя переменную:

query ActiveSwitches($state: SwitchState) {
  switches(state: $state)
}

А затем вы можете указать это значение в формате JSON следующим образом:

{
  "state": "ON"
}

ID

ID - это скалярный тип, который представляет собой уникальный идентификатор и используется для идентификации ресурсов в системе (например, в базе данных). Он имеет тип String, но все, что объявлено как идентификатор типа, не предназначено для чтения человеком.

Списки

Часто вам нужно запросить список (массив) элементов, например books, который, как вы понимаете, возвращает список объектов типа Book. Как вы можете видеть в приведенном выше коде с [Switch], списки представлены в квадратных скобках. Вы сможете увидеть это обозначение в Документах, где используется список.

Списки, не допускающие значения NULL

Если бы разработчик схемы хотел указать, что возвращаемые объекты не будут нулевыми, он бы написал это так: [Switch!], и если бы он собирался определить, что сам список не может возвращать null, И что возвращаемые объекты могут не быть нулевым, он бы написал это так: [Switch!]!.

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

{
  codesOfConduct {
    id
    name 
  }
}

Если вы заглянете в Документы под основной записью Запрос, вы увидите codesOfConduct: [CodeOfConduct]. Если вы перейдете вниз, вы увидите, что можете получить поля id и name. Это возвращает список объектов типа CodeOfConduct.

В настоящее время возвращаются только два результата, но что, если их было 75 000? Или 2,450,000, если на то пошло? Вы бы хотели получить все эти данные? Черт побери!

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

Фильтрация данных списка

Допустим, нам нужен список последних 5 репозиториев, которые я создал. Сейчас у меня может быть 10 тысяч (правда, нет), но кто знает. Разработчики схемы в своей безмерной мудрости определили аргумент фильтра с именем last, так что мы можем его использовать.

{
  repositoryOwner(login: "bengrunfeld") {
    id
    url
    login
    repositories(last: 5) {
      nodes {
        name
      }
    }
  }
}

Вы можете увидеть спецификацию аргумента фильтра на изображении ниже.

Но зачем нам указывать nodes в запросе? Я имею в виду, что даже является узлом и почему тип возвращаемого значения в документации использует термин RepositoryConnection? Обсудим в следующем разделе.

Соединения, края и узлы

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

Узел. Узел представляет собой фрагмент данных или набор полей данных. Примерами узла могут быть: Пользователь, Автомобиль, Домашнее животное и т. Д.

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

Соединение: соединение - это способ получить все узлы, которые подключены к другому узлу определенным образом. Представьте, что вы хотите получить все Книги (узел) , написанные (край) конкретным Автором (узлом). Такой способ будет называться Соединением.

Распространенное соглашение об именах, которое используется при разработке схемы, которое вы, вероятно, встретите в проектах GraphQL:

Края:

${Origin Type}${Relationship Type}Edge

Подключения:

${Origin Type}${Relationship Type}Connection

E.g. EnterpriseMemberEdge, or RegistryPackageConnection.

Фрагменты

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

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

{
  graphqlRepo: repository(name: "graphql", owner: "facebook") {
    id
    url
  }
reactRepo: repository(name: "react", owner: "facebook") {
    id
    url
  }
}

Вместо того, чтобы постоянно повторять id и url, мы могли бы просто объявить фрагмент, который определяет их обоих, а затем использовать его, например:

{
  graphqlRepo: repository(name: "graphql", owner: "facebook") {
    ...infoFields
  }
  reactRepo: repository(name: "react", owner: "facebook") {
    ...infoFields
  }
}
fragment infoFields on Repository {
  id
  url
}

Обратите внимание, что вы должны использовать ключевое слово on, чтобы указать, для какого объекта вы определяете поля. Вкратце, фрагменты помогают ОСУШИТЬ код.

Мутации

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

Вот пример мутации, использующей переменные:

mutation updatePostageAddress ($street: String!, $city: String!) {
  address (street: $street, city: $city) {
    name
    street
    city
  }
}

Обратите внимание, как после мутации address объект указывается с полями name, street и city. Это поля, которые ожидает мутация, будут возвращены после обработки мутации на стороне сервера. Эти возвращаемые данные полезны, поскольку позволяют увидеть, что данные были обновлены, как ожидалось.

Заключение

Уф… это была длинная статья, которую нужно было написать! Я очень надеюсь, что это помогло вам немного больше узнать о GraphQL и, возможно, даже помочь вам преодолеть некоторые из болевых точек, которые я лично считаю действительно сложными. Если вам понравился этот пост, пожалуйста, похлопайте. Кроме того, если я что-то пропустил или вы заметите ошибки, напишите о них ниже в комментариях, и я постараюсь их исправить. Следующая статья будет о реализации серверной части в Apollo. А пока заботьтесь и удачного кодирования! 😁

Рекомендуемое чтение

Я настоятельно рекомендую прочитать эти две статьи: