Главная » Статьи » Основы языка запросов GraphQL и практические примеры с Vue

Основы языка запросов GraphQL и практические примеры с Vue

Основы языка запросов GraphQL и практические примеры с Vue

От автора: GraphQL – это язык запросов для вашего API и серверный runtime для выполнения запросов. Одна точка может вернуть данные о множестве ресурсов, из-за чего он отлично подходит для одностраничных приложений 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 с помощью нового vue-cli.

Начало

Установите vue-cli командой:

npm install -g @vue/cli@latest

Создайте новый проект командой:

vue create graphql-example

Оставьте настройки по умолчанию командой default (babel, eslint). Будет установлена масса модулей node. Также необходимо создать папку для сервера 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.

Далее необходимо определить schema  — типы запросов и типы сервера, которые будем использовать. Наша первая схема – просто hello world от GraphQL:

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

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

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

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

Language просто возвращает String. Если вернуть другой тип, например, 1 или {}, будет выброшена ошибка, так как при объявлении language в схеме мы указали, что возвращается String. Последний шаг – создать приложение express и подключить resolver, 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. Если все пройдет хорошо, вы увидите:

Основы языка запросов GraphQL и практические примеры с Vue

Клик по Get Language должен вернуть и отрендерить результат. Хорошо. До сих пор мы:

определили схему

создали resolver, rootValue

сделали запрос с помощью axios, который включает строку запроса

Что еще можно делать с помощью GraphQL?

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

GraphQL позволяет определять пользовательские типы и объекты для их представления в целевом языке. У нас это JS, но 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" } ] }
}

Заметьте, вернулось только name! Если включить 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 при сохранении.

Клик по Get Champions даст:

Основы языка запросов GraphQL и практические примеры с Vue

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

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
}

Построчно:

query GetChampionByName – имя, присвоенное запросу. Это может быть что угодно, но оно должно описывать, что делает запрос. В нашем случае мы вызываем getChampionByName, поэтому я решил сделать имя запроса и запрос на сервере одинаковыми за исключением первой заглавной буквы. В реальном приложении один вызов API может содержать множество операций. Имя запроса упростит понимание кода.

($championName: String!) значит, что variables должен содержать championName, и он обязательный.

getChampionByName(name: $championName) – выполняемый на сервере запрос. Первый аргумент name должен использовать значение championName в объекте variables.

Мы запрашиваем 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 и практические примеры с Vue

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

До сих пор мы только вытаскивали данные. Часто хочется обновить данные. У 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 }
`)

Мутации идут с типом Mutation. Остальной синтаксис должен быть знаком. Мы возвращаем обновленную запись типа 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 сервер. Результат:

Основы языка запросов GraphQL и практические примеры с Vue

Можете кликнуть на Get Champion и проверить сохранение данных (должно вернуться обновленное attack damage).

Основы языка запросов GraphQL и практические примеры с Vue

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

Я не проводил тестирование. Однако тестировать серверные точки легко, так как это просто чистый JS. Просто экспортируйте объект rootValue и тестируйте функции, как обычно. Я расскажу о тестировании GraphQL API в следующей статье.

Заключение

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

Автор: Lachlan Miller

Источник: https://medium.com/

Редакция: Команда webformyself.