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 }
Построчно:
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 также предоставляет мутации. Синтаксис и реализация не так уж далеки от того, что мы рассмотрели до сих пор. Начнем с определения мутации:
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.
Исходный код статьи находится здесь.