Сегодня в веб-разработке мы научимся:

  • Легко настроить сервер GraphQL с помощью NodeJS
  • Макет данных без базы данных с помощью json-сервера
  • Создайте приложение CRUD, говорящее на GraphQL
  • Как Аполлон экономит время и силы

Если что-то из этого вас заинтересовало, читайте дальше! Обязательно ознакомьтесь с исходным кодом этого репо, если хотите сослаться на завершенный пример.

Нежное введение

Пару лет назад я запустил свой первый HTTP-сервер Node с помощью Express. На моем конце потребовалось всего 6 строк кода.

const express = require('express')
const app = express()

app.get('/', function(req, res) { 
  res.send({ hello: 'there' })
})

app.listen(3000, () => 'Listening at http://localhost:3000')

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

Шлюзы были открыты для бесчисленных руководств и видео по настройке сервера Node, обычно для создания своего рода CRUD REST API в рекордно короткие сроки.

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

Но это 2018 год, мы можем делать гораздо более крутые вещи.

Давайте заменим REST на GraphQL.

Введите GraphQL

GraphQL - это декларативный уровень выборки и обработки данных, который делает использование API более удобным для клиентов.

Некоторые преимущества использования данных через сервер GraphQL:

  • Вы получите именно те данные, которые запрашиваете, указав нужные поля.
  • Меньше запросов и меньше избыточной выборки. Запросы GraphQL обычно достаточно конкретны, чтобы избежать захвата ненужных записей или полей.
  • Строго типизированные схемы в отличие от необработанных полей JSON, которые не имеют мнения о типе возвращаемых данных.
  • Площадка GraphQL для исследования данных с автозаполнением и встроенной документацией. Если вам нравится работать с Почтальоном, вам будет удобно с этим интерфейсом.

Этот последний пункт, в частности, значительно упрощает привлечение новых разработчиков.

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

Подробнее об этом скоро, перейдем к кодированию.

Начало работы и установка зависимостей

Давайте начнем с создания каталога и инициализации файла package.json.

mkdir social-graphql && cd social-graphql && npm init -y

Наш технический стек будет выглядеть так:

  • JavaScript, работающий с Node (сегодня кода на стороне клиента нет)
  • Babel для написания современного ES6
  • Express для быстрой настройки HTTP-сервера
  • Apollo Server для всех полезных утилит GraphQL, которые помогают нам настраивать сервер и создавать схемы.
  • json-сервер для тестирования на поддельном наборе данных (намного проще, чем запрашивать реальную базу данных)
npm install -S express apollo-server-express graphql json-server axios

Кроме того, у нас есть некоторые зависимости от разработчиков, которые нам помогут.

npm install -D babel-cli babel-preset-env nodemon npm-run-all

Убрав зависимости, мы можем перейти к кодированию.

Начиная с базового HTTP-сервера

Давайте создадим HTTP-сервер, который обрабатывает индексный маршрут. То есть, если я запустил сервер и перейду к http: // localhost: 3500, я должен увидеть сообщение JSON, а не Cannot GET« / ».

Создайте index.js файл:

import express from 'express'

const PORT = process.env.PORT || 3500
const app = express()

app.get('/', function(req, res) {
  res.send({ hello: 'there!' })
})

app.listen(PORT, () => `Listening at http://localhost:${PORT}`)

Это очень похоже на код в начале статьи, за исключением синтаксиса импорта и настройки порта с помощью переменных среды.

Чтобы синтаксис импорта работал здесь, нам нужно воспользоваться нашей предустановкой babel. Создайте файл с именем .babelrc и:

{
  "presets": ["env"]
}

Наконец, чтобы запустить сервер, обновите сценарий запуска в package.json следующим образом:

"scripts": {
  "dev:api": "nodemon --exec 'babel-node index.js'"
}

А затем введите npm run dev:api в свой терминал. Перейдя по адресу http: // localhost: 3500, вы увидите ответ, в котором будет написано hello: there!.

В отличие от более типичного node index.js в npm start скрипте, мы используем команду dev вместе с nodemon, выполняющей babel-node.

Nodemon перезапускает ваш сервер разработки всякий раз, когда вы сохраняете файлы, поэтому вам не нужно этого делать. Обычно он выполняется с node, но мы говорим ему выполняться с babel-node, чтобы он обрабатывал наш причудливый импорт ES6.

Обновление до Apollo

Хорошо, мы собрали базовый HTTP-сервер, который может обслуживать конечные точки REST. Давайте обновим его, чтобы обслуживать GraphQL.

import express from 'express'
import { ApolloServer } from 'apollo-server-express'
import { resolvers, typeDefs } from './schema'

const PORT = process.env.PORT || 3500
const app = express()

const server = new ApolloServer({
  typeDefs,
  resolvers,
  playground: true
})

server.applyMiddleware({ app })

app.get('/', (req, res) => {
  res.send({ hello: 'there!' })
})

app.listen(PORT, () =>
  console.log(`Listening at http://localhost:${PORT}/graphql`)
)

Затем в новый файл, который я назову schema.js, вставьте:

import { gql } from 'apollo-server-express'

export const typeDefs = gql`
  type Query {
    users: String
  }
`

export const resolvers = {
  Query: {
    users() {
      return "This will soon return users!"
    }
  }
}

Решатели и схема (определения типов)

Здесь, если вы новичок в работе с GraphQL, вы увидите забавный синтаксис, который мы назначаем для typeDefs.

В ES6 JavaScript мы можем вызывать функцию, используя обратные кавычки, как в случае с gql. С точки зрения ванильного JavaScript, вы можете прочитать это так:

gql.apply(null, ["type Query {\n users: String \n }"])

По сути, он вызывает gql с массивом аргументов. Так уж получилось, что написание многострочных строк удобно при выражении запроса, подобного JSON.

Если вы все еще используете сервер, перейдите по адресу http: // localhost: 3500 / graphql. Здесь вы увидите фантастический интерфейс для тестирования наших запросов.

Правильно, больше не нужно бросать cURL в непонятную конечную точку, мы можем тестировать наши запросы с помощью автозаполнения, уточнения и встроенной документации. Он также готов к работе с Apollo, поэтому вам не нужно устанавливать дополнительные пакеты или приложения.

А теперь давайте сделаем этот запрос немного интереснее.

Реализация реального запроса GraphQL: список пользователей

Прежде чем углубляться в этот раздел, обязательно скопируйте db.json из этого репозитория в свой рабочий каталог вместе с index.js и schema.js.

Затем обновите скрипты в package.json:

"scripts": {
  "dev": "npm-run-all --parallel dev:*",
  "dev:api": "nodemon --exec 'babel-node index.js' --ignore db.json",
  "dev:json": "json-server --watch db.json"
}

Повторно запустите сервер с npm run dev и нажмите.

На сервере GraphQL существует концепция корневого запроса. Этот тип запроса является точкой входа для любых запросов на выборку данных в нашу схему GraphQL. Для нас это выглядит так:

type Query {
  users: String
}

Если мы обслуживаем пользователей, сообщения или самолеты, клиент, запрашивающий данные, должен сделать это, пройдя корневой запрос.

type Query {
  users: [User] # here the "[]"s mean these are returning lists
  posts: [Post]
  airplanes: [Airplane]
}

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

  1. Добавьте запрос под типом запроса в наши определения типов.
  2. Добавьте функцию преобразователя в объект запроса в нашем объекте преобразователя.

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

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

Обновите schema.js следующим образом:

export const typeDefs = gql`
  type User {
    id: ID
    name: String
    age: Int
    email: String
    friends: [User]
  }

  type Query {
    users: [User]
  }
`

Отлично, у нас есть тип пользователя и корневой запрос, который возвращает некоторый список пользователей.

Обновим резолвер:

export const resolvers = {
  Query: {
    users() {
      return userModel.list()
    }
  }
}

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

Внутри нового файла с именем models.js добавьте следующее:

import axios from 'axios'

class User {
  constructor() {
    this.api = axios.create({
      baseURL: 'http://localhost:3000' // json-server endpoint
    })
  }

  list() {
    return this.api.get('/users').then(res => res.data)
  }
}

export default new User()

Этот класс формирует слой абстракции над логикой, которая напрямую обрабатывает наши данные.

Наконец, вверху schema.js добавьте этот импорт:

import userModel from './models'

Вернитесь к http: // localhost: 3500 / graphql, вставьте и запустите этот запрос:

query Users {
  users {
    id
    name
    email
  }
}

Пользовательский запрос теперь выглядит немного интереснее! Для каждого пользователя в нашем db.json файле мы вернули его идентификатор, имя и адрес электронной почты.

Поскольку мы используем json-сервер, размещенный на локальном порту, мы используем модель, как если бы она извлекала данные из удаленного API.

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

Однако с точки зрения клиента они понятия не имеют, как модель извлекает данные - они знают только о форме данных.

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

Друзья друзей: более сложный вопрос

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

Что, если вы хотите получить пользователей, а также всех друзей для определенного пользователя? Мы хотим выполнить такой запрос:

query UsersAndFriends {
  users {
    id
    name
    friends {
      id
      name
    }
  }
}

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

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

Это похоже на интенсивное вычисление?

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

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

В models.js и этот find метод для класса User:

class User {
  constructor() {
    this.api = axios.create({
      baseURL: 'http://localhost:3000' // json-server endpoint
    })
  }

  list() {
    return this.api.get('/users').then(res => res.data)
  }

  find(id) {
    return this.api.get(`/users/${id}`).then(res => res.data)
  }
}

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

В противном случае запрос не знал бы, как разрешить список пользователей, когда увидит friends.

export const resolvers = {
  Query: {
    users() {
      return userModel.list()
    }
  },
  User: {
    friends(source) {
      if (!source.friends || !source.friends.length) {
        return
      }

      return Promise.all(
        source.friends.map(({ id }) => userModel.find(id))
      )
    }
  },
}

В методе друзей источник - это родительское значение, с которым вызывается функция преобразователя. То есть для пользователя с идентификатором 0, Peck Montoya, значением source является весь объект со списком идентификаторов друзей.

Для корневых запросов источник чаще всего не определен, потому что корневой запрос не разрешается из определенного источника.

Метод друзей возвращается, когда все запросы на поиск отдельных пользователей удовлетворены.

Теперь попробуйте выполнить этот запрос, если вы не пробовали раньше:

query UsersAndFriends {
  users {
    id
    name
    friends {
      id
      name
    }
  }
}

Мутации: создание пользователя

Пока мы только получаем данные. Что, если бы мы захотели изменить данные?

Начнем с создания пользователя с именем и возрастом.

Взгляните на эту мутацию:

mutation CreateUser($name: String!, $email: String, $age: Int) {
  createUser(name: $name, email: $email, age: $age) {
    name
    email
    age
  }
}

Некоторые отличия на первый взгляд:

  • мы обозначаем этот код словом «мутация», а не «запрос»
  • мы передаем два набора похожих аргументов

Аргументы - это в основном объявления типов для переменных, ожидаемых нашим запросом.

Если существует несоответствие между этими типами и типами, переданными клиентом, например веб-приложением или мобильным приложением, сервер GraphQL выдаст ошибку.

Чтобы этот запрос заработал сейчас, давайте сначала обновим класс User в model.js:

create(data) {
  data.friends = data.friends 
    ? data.friends.map(id => ({ id })) 
    : []

  return this.api.post('/users', data).then(res => res.data)
}

Когда мы запускаем этот запрос, json-server добавит нового пользователя с данными, которые мы передали.

Теперь обновите schema.js до следующего:

export const typeDefs = gql`

  # other types...

  type Mutation {
    createUser(name: String!, email: String, age: Int): User
  }
`

export const resolvers = {
  // other resolvers...
  Mutation: {
    createUser(source, args) {
      return userModel.create(args)
    }
  }
}

На этом этапе запрос должен работать. Но мы можем сделать немного лучше.

Упрощение запросов и аргументов мутации

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

export const typeDefs = gql`

  # other types...

  input CreateUserInput {
    id: Int
    name: String
    age: Int
    email: String
    friends: [Int]
  }

  type Mutation {
    createUser(input: CreateUserInput!): User
  }
`

export const resolvers = {
  // other resolvers...
  Mutation: {
    createUser(source, args) {
      return userModel.create(args.input)
    }
  }
}

Обратите внимание: если бы мы хотели реализовать мутацию UpdateUser, мы, вероятно, могли бы воспользоваться этим новым типом ввода.

А теперь попробуйте эту мутацию:

mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    name
    email
    age
    friends {
      id
      name
    }
  }
}

Чтобы заполнить переменные, которые входят в запрос, щелкните и разверните вкладку с надписью «Переменные запроса» в нижнем левом углу игровой площадки GraphQL.

Затем введите этот JSON:

{
  "input": {
    "name": "Indigo Montoya",
    "email": "[email protected]",
    "age": 29,
    "id": 13,
    "friends": [1,2]
  }
}

Если все прошло хорошо, вы должны увидеть ответ только что созданного пользователя. Вы также должны увидеть двух пользователей с идентификаторами 1 и 2.

Теперь наш метод создания не полностью завершен - друзья нашего недавно созданного пользователя понятия не имеют, что наш новый пользователь - их друзья.

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

Я оставлю это в качестве упражнения для читателей, если они того пожелают.

Соединяя точки (заключение)

Обязательно ознакомьтесь с исходным кодом этого репо, если хотите увидеть, как я реализовал мутации deleteUser и updateUser.

Использование GraphQL с Apollo в моих собственных проектах произвело фурор. Я могу честно сказать, что разрабатывать схемы и преобразователи GraphQL гораздо интереснее, чем реализовывать обработчики маршрутов HTTP.

Если вы хотите узнать больше о GraphQL, ознакомьтесь со следующими публикациями на Medium:

Если вам понравилась эта статья и вы хотите увидеть больше в будущем, дайте мне знать в комментариях и подпишитесь на Twitter и Medium!