GraphQL - это язык запросов для вашего API и среда выполнения на стороне сервера для выполнения запросов. Одна конечная точка может возвращать данные о нескольких ресурсах, что делает ее очень подходящей для одностраничных приложений Vue.js.

В этой статье будет рассказано, как создать GraphQL API с нуля, а также определить и реализовать запросы и мутации с пользовательскими типами. Я буду использовать Node.js для сервера GraphQL и буду делать запросы и отображать результаты с помощью одностраничного приложения Vue.js.

Исходный код этой статьи находится здесь.

Вступление

Служба GraphQL создается путем определения типов и полей, а затем предоставления функций для каждого поля каждого типа. Канонический пример из документации GraphQL:

type Query { // define the query
  me: User // define the fields
}
type User { // define the type
  id: ID
  name: String
}
function Query_me(request) { // define the function
  return request.auth.user
}

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

{
  me {
   name
  }
}

который возвращает:

{
  “me”: {
    “name”: “username”
  }
}

Официальная документация GraphQL превосходна, но мне не хватает практических примеров того, как запрашивать данные с помощью стандартного HTTP-клиента и интегрировать их в мое приложение Vue.js, поэтому в этой статье я сделаю следующее. Мы будем использовать новый vue-cli, чтобы построить проект Vue, который будет работать с ним.

Начиная

Установите vue-cli, запустив:

npm install -g @vue/cli@latest

Создайте новый проект, запустив:

vue create graphql-example

и оставьте значение по умолчанию, выбрав ❯ default (babel, eslint). Будет установлена ​​тонна узловых модулей. Нам также необходимо создать папку для сервера API, поэтому запустите следующее после cd в проект (cd graphql-example)

mkdir server
npm install express express-graphql graphql --save

Мы добавили graphql, а также express и express-graphql, который представляет собой тонкий слой, реализующий некоторые лучшие практики и рекомендации по обслуживанию запросов через HTTP.

Базовый запрос

Давайте настроим простой запрос, чтобы убедиться, что все работает, и посмотрим, как выглядит сервер GraphQL. Внутри server/index.js требуется несколько модулей:

const express = require('express')
const { graphql, buildSchema } = require('graphql')
const graphqlHTTP = require('express-graphql')
const cors = require('cors')
  • express и express-graphql позволят нам отвечать на HTTP-запросы
  • buildSchema используется для определения типов (скоро)
  • cors позволит нам делать запросы из нашего приложения Vue, которое будет работать на порту 8080, на сервер, работающий на порту 4000

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

const schema = buildSchema(`
  type Query {
    language: String
  }
`)

Мы определяем тип Query с именем language. Он возвращает String. GraphQL статически типизирован - поля имеют типы, и если что-то не совпадает, выдается ошибка.

В отличие от REST API, у Graph API есть только одна конечная точка, которая отвечает на все запросы. Это называется преобразователем . Я назову свой rootValue и включу реализацию для language запроса:

const rootValue = {
  language: () => 'GraphQL'
}

language просто возвращает String. Если мы вернем другой тип, например 1 или {}, будет выдана ошибка, поскольку, когда мы объявили language в схеме, мы указали, что будет возвращено String.

Последний шаг - создать экспресс-приложение и смонтировать резолвер, rootValue и schema.

const app = express()
app.use(cors())
app.use('/graphql', graphqlHTTP({
  rootValue, schema, graphiql: true
}))
app.listen(4000, () => console.log('Listening on 4000'))

Давайте теперь реализуем клиентское приложение Vue, которое будет делать запрос.

Сделать запрос

Перейдите к src/App.vue и удалите шаблон. Теперь это должно выглядеть так:

<template>
  <div id="app">
  </div>
</template>
<script>
import axios from 'axios'
export default {
  name: 'app'
}
</script>

Мы также импортируем axios, который мы будем использовать для выполнения HTTP-запросов.

По умолчанию graphqlHTTP ожидает POST запросов. Согласно рекомендациям по обслуживанию через HTTP, мы должны включать query и variables в тело запроса. Это приведет нас к следующему запросу:

axios.post('http://localhost:4000/graphql', {
  query: '{ language }'
})   

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

<template>
  <div id="app">
    <h3>Example 1</h3>
    <div>
      Data: {{ example1 }}
    </div>
    <button @click="getLanguage">Get Language</button>
    <hr> 
  </div>
</template>
<script>
import axios from 'axios'
export default {
  name: 'app',
  data () {
    return {
      example1: ''
    }
  },
  methods: {
    async getLanguage () {
      try {
        const res = await axios.post(
          'http://localhost:4000/graphql', {
          query: '{ language }'
        })
        this.example1 = res.data.data.language
      } catch (e) {
        console.log('err', e)
      }
    }
  }
}
</script>

Давай запустим это. В одном терминале запустите сервер GraphQL с node server. В другом - запустите приложение Vue, используя npm run serve. Посетите http://localhost:8080. Если все прошло хорошо, вы увидите:

Нажатие «Получить язык» должно вернуть и отобразить результат.

Ок, отлично. Пока мы:

  • определил схему
  • создал преобразователь, rootValue
  • сделать запрос, используя axios, который включал запрос

Что еще мы можем делать с GraphQL?

Пользовательские типы с моделями

GraphQL позволяет нам определять пользовательские типы и объекты для их представления на целевом языке - в данном случае JavaScript, но есть клиенты GraphQL для большинства языков на стороне сервера. Я определю тип Champion в схеме и соответствующий класс ES6 для хранения любых свойств и методов.

Во-первых, обновите схему:

const schema = buildSchema(`
  type Query {
    language: String
  }
  type Champion {
     name: String
     attackDamage: Float
   }
`)

Ничего особенного, кроме нового типа Float. Затем мы можем определить класс ES6 для представления этого типа и хранить любые методы экземпляра или дополнительные данные. Я определю это в новом файле server/champion.js.

class Champion {
  constructor(name, attackDamage) {
    this.name = name
    this.attackDamage = attackDamage
  }
}
module.exports = Champion

Ничего особенного, просто класс ES6. Обратите внимание, что у нас есть name и attackDamage - те же поля, которые определены в схеме для Champion.

Теперь давайте создадим еще один запрос, использующий тип Champion. Обновленный schema выглядит следующим образом:

const schema = buildSchema(`
  type Query {
    language: String
    getChampions: [Champion]
  }
  type Champion {
    name: String
    attackDamage: Float
  }
`)

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

const champions = [
  new Champion('Ashe', 100),
  new Champion('Vayne', 200)
]
const rootValue = {
  language: () => 'GraphQL',
  getChampions: () => champions
}

Перезапустите сервер, нажав ctrl+c в терминале, на котором запущен сервер, и снова запустите node server. Давайте проверим, работает ли это, отправив запрос от клиента.

Запрос определенных полей

Запросы getChampions немного интереснее, чем language. На этот раз результат будет содержать определенный пользователем Champion тип - и любые поля, которые мы запрашиваем. GraphQL требует, чтобы мы явно указывали, какие поля нам нужны. Например, следующий запрос:

{ 
  getChampions 
}

не будет работать. Необходимо указать хотя бы одно поле. Обновленный запрос:

{
  getChampions {
    name
  }
}

Возврат:

{
  "data": {
    "getChampions": [
      {
        "name": "Ashe"
      },
      {
        "name": "Vayne"
      }
    ]
  }
}

Обратите внимание, что возвращается только имя! Если бы мы включили attackDamage, мы бы тоже это получили. Запрос:

{
  getChampions {
    name
    attackDamage
  }
}

и ответ:

{
  "data": {
    "getChampions": [
      {
        "name": "Ashe"
        "attackDamage": 100
      },
      {
        "name": "Vayne"
        "attackDamage": 200
      }
    ]
  }
}

Реализовать это в приложении Vue также просто:

<template>
  <div id="app">
    <!-- ... -->
    <h3>Example 2</h3>
    <div>
      Data:
      <div v-for="champion in champions">
        {{ champion }}
      </div>
    </div>
    <button @click="getChampions">Get Champions</button>
  </div>
</template>
export default {
  name: 'app',
  data () {
    return {
      /* ... */,
      champions: []
    }
  },
  methods: {
    /* ... */
    async getChampions () {
      const res = await axios.post(
        'http://localhost:4000/graphql', {
        query: `{
          getChampions {
            name
          }
        }`
      })
      this.champions = res.data.data
    }
  }
}

Обязательно перезапустите сервер с node server, если вы еще этого не сделали. Нет необходимости перезапускать приложение Vue, так как горячая перезагрузка webpack будет автоматически обновляться при сохранении любых изменений.

Нажатие кнопки «Получить чемпионов» дает:

Передача аргументов

getChampions вернуть всех чемпионов. GraphQL также поддерживает передачу аргументов для возврата подмножества данных. Это требует:

  • дополнительный объект variables в теле POST
  • сообщая клиентскому запросу тип аргументов, которые вы будете анализировать для запроса из variables.

Давайте реализуем getChampionByName запрос. Как обычно, начнем с определения запроса:

const schema = buildSchema(`
  type Query {
    language: String
    getChampions: [Champion]
    getChampionByName(name: String!): Champion
  }
  type Champion {
    name: String
    attackDamage: Float
  }
`)

Обратите внимание, что мы объявляем аргумент name и тип String!. ! означает, что аргумент обязателен.

Далее реализация:

const rootValue = {
  language: () => 'GraphQL',
  getChampions: () => champions,
  getChampionByName: ({ name }) => {
    return champions.find(x => x.name === name)
  }
}

Ничего особенного - мы просто используем find, чтобы получить соответствующего чемпиона. Улучшение состояло бы в том, чтобы добавить некоторую обработку ошибок и сравнить name без учета регистра.

Теперь о реализации на стороне клиента. Здесь все становится немного интереснее. При передаче аргументов мы должны назвать запрос и объявить аргументы с соответствующим типом:

async getChampionByName () {
  const res = await axios.post('http://localhost:4000/graphql', {
    query: `
      query GetChampionByName($championName: String!) {
        getChampionByName(name: $championName) {
          name
          attackDamage
        }
      }`,
      variables: {
        championName: 'Ashe'
      }
  })
  this.champion = res.data.data.getChampionByName
}

Построчно:

  1. query GetChampionByName - это имя, которое мы даем запросу. Это может быть что угодно, но должно описывать, что делает запрос. В этом случае, поскольку мы вызываем только getChampionByName, я использовал соглашение, когда имя совпадает с именем запроса на стороне сервера, но первая буква заглавная. В реальном приложении один вызов API может включать в себя множество различных операций. Присвоение имени запросу кода может упростить понимание кода.
  2. ($championName: String!) означает, что variables должен содержать championName, и это не.
  3. getChampionByName(name: $championName) - это запрос, выполняемый на стороне сервера. Первый аргумент name должен использовать значение championName в объекте variables.
  4. Мы запрашиваем в ответе name и attackDamage.

Дополнительная разметка позволит нам отобразить результат в приложении Vue (не забудьте перезапустить сервер GraphQL):

<template>
  <div>
  <!-- ... -->
  <h3>Example 4</h3>
  Name: <input v-model="name">
  <div>
    Data:
    {{ champion }}
  </div>
  <button @click="getChampionByName">Get Champion</button>
  </div>
</template>
<script>
import axios from 'axios'
export default {
  data () {
    return { 
      /* ... */ 
      champion: {}
    }
  },
methods: {
    /* ... */
    async getChampionByName () {
      const res = await axios.post(
        'http://localhost:4000/graphql', {
        query: `
          query GetChampionByName($championName: String!) {
            getChampionByName(name: $championName) {
              name
              attackDamage
            }
          }`,
          variables: {
            championName: 'Ashe'
          }
      })
      this.champion = res.data.data.getChampionByName
    }
  }
}

Обновление записей

Пока что мы только что получали данные. Вы также часто хотите обновлять данные, поэтому GraphQL также предоставляет мутации. Синтаксис и реализация не так уж далеки от того, что мы рассмотрели до сих пор. Начнем с определения мутации:

const schema = buildSchema(`
  type Query {
    language: String
    getChampions: [Champion]
    getChampionByName(name: String!): Champion
  }
  type Mutation {
    updateAttackDamage(name: String!, attackDamage: Float): Champion
  }
  type Champion {
    name: String
    attackDamage: Float
  }
`)

Мутации относятся к 70-му типу. Остальной синтаксис к этому моменту должен быть вам знаком. Мы возвращаем обновленную запись типа Champion. Реализация также проста:

const rootValue = {
  language: () => 'GraphQL',
  getChampions: () => champions,
  getChampionByName: ({ name }) => {
    return champions.find(x => x.name === name)
  },
  updateAttackDamage: ({ name, attackDamage = 150 }) => {
    const champion = champions.find(x => x.name === name)
    champion.attackDamage = attackDamage
    return champion
  }
}

В более реалистичном примере вы можете выполнить SQL-запрос для обновления записи в базе данных или выполнить некоторую проверку. Мы должны вернуть тип Champion, поскольку мы указали это в объявлении мутации. GraphQL автоматически выберет правильные поля для возврата на основе запроса - мы запросим name и обновленный attackDamage, как показано ниже:

methods: {
  /* ... */
  async updateAttackDamage () {
    const res = await axios.post('http://localhost:4000/graphql', {
      query: `
        mutation UpdateAttackDamage(
          $championName: String!,  $attackDamage: Float) {
          updateAttackDamage(name: $championName, attackDamage: $attackDamage) {
            name
            attackDamage
          }
        }`,
        variables: {
          championName: this.name,
          attackDamage: this.attack
        }
    })
    this.updatedChampion = res.data.data.updateAttackDamage
  }
}

Единственная реальная разница здесь в том, что мы объявили имя операции как mutation тип, а не query.

Полностью обновленный пример выглядит следующим образом:

<template>
  <div>
  <!-- ... -->
  <h3>Example 4</h3>
    Name: <input v-model="name">
    Attack Damage: <input v-model.number="attack">
    <div>
      Data:
      {{ updatedChampion }}
    </div>
    <button @click="updateAttackDamage">Update Champion</button>
  </div>
</template>
<script>
import axios from 'axios'
export default {
  data () {
    return {
      /* ... */
      updatedChampion: {},
      attack: 5.5
    }
  },
  methods: {
    /* ... */
    async updateAttackDamage () {
      const res = await axios.post('http://localhost:4000/graphql', {
        query: `
        mutation UpdateAttackDamage($championName: String!, $attackDamage: Float) {
          updateAttackDamage(name: $championName, attackDamage: $attackDamage) {
            name
            attackDamage
          }
        }`,
        variables: {
          championName: this.name,
          attackDamage: this.attack
        }
      })
      this.updatedChampion = res.data.data.updateAttackDamage
    }
  }
}

Как обычно, перезапустите сервер GraphQL. Результат такой:

Вы можете нажать «Получить чемпиона» и посмотреть, правильно ли были сохранены данные (они должны вернуть недавно обновленный урон от атаки):

Тестирование

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

Заключение

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

Исходный код статьи находится здесь.