[Этап 10] Создание сервера GraphQL на основе Apollo и Koa

GraphQL
[Этап 10] Создание сервера GraphQL на основе Apollo и Koa

В этой статье предполагается, что читатели имеют определенное представление о NodeJS и Koa.

Какую проблему решает GraphQL

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

Мы работали с этим способом работы и постепенно обнаружили некоторые проблемы:

  • Выделенная документация по API становится бременем
  • Документация API и службы API часто развертываются в разных доменах, нам нужно помнить, где находится документация.
  • Мы всегда обнаруживаем, что фактическое поведение API не соответствует документации.
  • Значение перечисления внутренних данных API, всегда утекающих клиенту
  • Работа по проверке параметров API повторяется на клиенте и сервере.
  • Нам сложно увидеть структурную схему всех данных приложения
  • Нам приходилось поддерживать несколько версий API

Постепенно мы обнаружили, что спецификация описания данных должна быть разделена между сервером и клиентом:

  • Это описание данных является частью функциональности (обратите внимание, это не комментарий) и участвует в реализации функциональности API.
  • Это описание данных само по себе является документом, нам больше не нужно писать документ, не говоря уже о развертывании службы документов.
  • Когда мы изменяем детали описания данных, функциональность API меняется, и нам не нужно беспокоиться о несоответствиях в документации и поведении.
  • Описание данных само по себе поддерживает типы перечисления, ограничивая проблему утечки значений перечисления.
  • Само описание данных имеет систему типов, и нам не нужно повторять работу по проверке параметров на клиенте и сервере.
  • Само описание данных является структурной диаграммой всех данных приложения.
  • Описание данных может скрыть проблемы сопровождения версии.

GraphQL является такой спецификацией описания данных.

Что такое GraphQL

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

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

Вот описание данных на основе GraphQL:

type Query {
  book: Book
}

enum BookStatus {
  DELETED
  NORMAL
}

type Book {
  id: ID
  name: String
  price: Float
  status: BookStatus
}

Чтобы не быть привязанным к конкретной платформе, и упростить понимание функций сервера, GraphQL реализует удобочитаемый синтаксис схемы:Schema Definition Language (SDL)

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

SDLдолжен храниться как строка

SDLОпределены три типа входов, а именно:

  • Queryиспользуется для определенияоперация (которую можно понимать какCURDсерединаR)
  • Mutationиспользуется для определенияоперация (которую можно понимать какCURDсерединаCUD)
  • SubscriptionИспользуется для определения длинных ссылок (способы создания и поддержания подключений к серверам в реальном времени на основе событий).

С помощью приведенного выше кода мы объявляем запрос с именемbook, типBook

типBookИмеет четыре поля, а именно:

  • id, представляющий уникальный идентификатор каждой книги, тип ID
  • имя, представляющее название каждой книги, тип — строка
  • цена, представляющая цену каждой книги, тип - число с плавающей запятой
  • статус, представляющий статус каждой книги, типBookStatus

BookStatusпредставляет собой тип перечисления, который содержит:

  • DELETEDОт имени книга была удалена, ее ценность0
  • NORMALПредставительская книга находится в обычной продаже, ее стоимость1

Помимо определения собственных типов данных, в GraphQL есть несколько встроенных примитивов (скаляров):

  • Int: 32-битное целое число со знаком
  • Float: число с плавающей запятой двойной точности со знаком
  • String: последовательность символов UTF-8
  • Boolean: правда или ложь
  • ID: уникальный идентификатор, часто используемый для извлечения объекта или в качестве ключа кэша.

Здесь следует отметить, что GraphQL требует, чтобы тип поля конечной точки был скалярным. (Конечные точки здесь можно понимать как конечные узлы)

оGraphQLДля получения дополнительной информации см.:graphql.cn/learn/

Что такое Аполлон

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

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

мы можем поставитьApolloРассматриваемый как набор инструментов, он делится на две категории: одна ориентирована на сервер, а другая — на клиента.

Среди них клиентоориентированныйApollo ClientОхвачены следующие инструменты и платформы:

  • React + React Native
  • Angular
  • Vue
  • Meteor
  • Ember
  • IOS (Swift)
  • Android (Java)
  • ...

ориентированный на серверApollo ServerОхвачены следующие платформы:

  • Java
  • Scala
  • Ruby
  • Elixir
  • NodeJS
  • ...

Мы будем использовать в этой статьеApolloсреда дляNodeJSСерверkoaобрамленныйapollo-server-koaбиблиотека

Для получения дополнительной информации о сервере apollo и apollo-server-koa см.:

Создайте серверную API-службу GraphQL.

Быстрая сборка

шаг 1:

Создайте новую папку, я создал новую папку здесь graphql-server-demo

mkdir graphql-server-demo

Инициализируйте проект внутри папки:

cd graphql-server-demo && yarn init

Установите зависимости:

yarn add koa graphql apollo-server-koa
шаг 2:

Создайте новый файл index.js и напишите в нем следующий код:

'use strict'

const path = require('path')
const Koa = require('koa')
const app = new Koa()
const { ApolloServer, gql } = require('apollo-server-koa')

/**
 * 在 typeDefs 里定义 GraphQL Schema
 *
 * 例如:我们定义了一个查询,名为 book,类型是 Book
 */
const typeDefs = gql`
  type Query {
    book: Book
    hello: String
  }

  enum BookStatus {
    DELETED
    NORMAL
  }

  type Book {
    id: ID
    name: String
    price: Float
    status: BookStatus
  }
`;

const BookStatus = {
  DELETED: 0,
  NORMAL: 1
}
/**
 * 在这里定义对应的解析器
 * 
 * 例如:
 *   针对查询 hello, 定义同名的解析器函数,返回字符串 "hello world!"
 *   针对查询 book,定义同名的解析器函数,返回预先定义好的对象(实际场景可能返回来自数据库或其他接口的数据)
 */
const resolvers = {

  // Apollo Server 允许我们将实际的枚举映射挂载到 resolvers 中(这些映射关系通常维护在服务端的配置文件或数据库中)
  // 任何对于此枚举的数据交换,都会自动将枚举值替换为枚举名,避免了枚举值泄露到客户端的问题
  BookStatus,

  Query: {

    hello: () => 'hello world!',

    book: (parent, args, context, info) => ({
      name:'地球往事',
      price: 66.3,
      status: BookStatus.NORMAL
    })

  }
};

// 通过 schema、解析器、 Apollo Server 的构造函数,创建一个 server 实例
const server = new ApolloServer({ typeDefs, resolvers })
// 将 server 实例以中间件的形式挂载到 app 上
server.applyMiddleware({ app })
// 启动 web 服务
app.listen({ port: 4000 }, () =>
  console.log(`🚀 Server ready at http://localhost:4000/graphql`)
)

Наблюдая за приведенным выше кодом, мы обнаруживаем, что:SDLзапрос, определенный вbook, есть одноименный парсерbookкак его реализация источника данных.

По факту,GraphQLКаждое поле должно иметь соответствующийresolver, для полей конечной точки, то есть полей скалярного типа, наиболееGraphQLРеализация библиотек позволяет опускать определения синтаксического анализатора для этих полей, и в этом случае свойство с тем же именем, что и у этого поля, автоматически считывается из родительского объекта.

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

Парсер — это функция, список параметров которой выглядит следующим образом:

  • parentОбъект верхнего уровня, если он в настоящее время является корневым полем, значение этого параметра равноundefined
  • argsсуществуетSDLПараметры, переданные в запросе
  • contextЭтот параметр предоставляется всем анализаторам и содержит важную контекстную информацию, такую ​​как текущий вошедший в систему пользователь или объект доступа к базе данных.
  • infoЗначение, которое содержит информацию о конкретном поле, связанную с текущим запросом, а также сведения о схеме.
шаг 3:

запустить службу

node index.js

На данный момент мы видим следующую информацию в терминале:

➜  graphql-server-demo git:(master) ✗ node index.js
🚀 Server ready at http://localhost:4000/graphql

Служба делегирования запущена

Откройте другой интерфейс терминала и запросите веб-сервис, который мы только что запустили:

curl 'http://localhost:4000/graphql' -H 'Content-Type: application/json' --data-binary '{"query":"{hello}"}'

или

curl 'http://localhost:4000/graphql' -H 'Content-Type: application/json' --data-binary '{"query":"{book{name price status}}"}'

См. следующую информацию:

{"data":{"hello":"Hello world!"}}

или

{"data":{"book":{"name":"地球往事","price":66.3,"status":"NORMAL"}}}

Указывает, что мы успешно создалиGraphQLAPI-сервис~

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

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

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

Далее посмотримApolloпредоставил намpalyground.

Playground

Начните то, что мы только что создалиGraphQLСерверная служба:

➜  graphql-server-demo git:(master) ✗ node index.js
🚀 Server ready at http://localhost:4000/graphql

Открываем адрес в браузереhttp://localhost:4000/graphql

На этом этапе мы увидим следующий интерфейс:

Введите параметры запроса слева:

{
  book {
    name
    price
  }
}

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

Игровая площадка также предоставляет нам некоторые из следующих функций:

  • Создавайте несколько запросов и запоминайте их
  • Пользовательские поля заголовка запроса
  • Посмотреть всю документацию по API
  • Просмотр полной структуры схемы на стороне сервера

Как показано ниже:

в,DOCSиSCHEMAСодержимое находится черезGraphQLодин позвонил内省(introspection) предусмотрена функция.

内省функция позволяет нам запрашивать параметры клиентского запросаGraphQL SchemaКакие запросы поддерживаются, и игровая площадка будет отправлена ​​​​заранее при ее запуске.内省запросить, получитьSchemaинформационная и организованнаяDOCSиSCHEMAструктура содержания.

о内省Для получения более подробной информации см.:график вверх. способность /обучение/введение…

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

мы можем создатьApollo Serverнапример, через соответствующий переключатель (playgroundиintrospection), чтобы защитить производственную среду:

...
const isProd = process.env.NODE_ENV === 'production'

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: !isProd,
  playground: !isProd
})
...

Далее рассмотрим более распространенную проблему:

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

Далее мы смотрим наGraphQLСервер, как это сделать.

Mock

использовать на основеApollo ServerизGraphQLДля сервера очень просто реализовать фиктивную функцию API.

Нам просто нужно построитьApollo ServerПри создании экземпляра включите параметр mocks:

...
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: !isProd,
  playground: !isProd,
  mocks: true
})
...

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

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

На самом деле, чем точнее мы определяем наши типы, тем выше качество нашего мок-сервиса.

Проверка параметров и сообщение об ошибке

В предыдущем разделе мы увидели некоторую помощь по системе типов для фиктивных сервисов.

Другой сценарий, в котором может играть система типов:请求参数校验

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

Например, мы запрашиваемbookПоля, которых нет вGraphQLперехватить и вернуть ошибку:

Мы видим, что результат возврата запроса больше не содержитdataполе и только одноerrorполе, в которомerrorsКонкретные сведения об ошибке для каждой ошибки отображаются в поле массива.

На самом деле, когда мы набираем на игровой площадкеnoneКогда используется неправильное имя поля, playgorund уже обнаружил неправильный параметр и дал подсказку, обратите внимание на красный блок в левой части изображения выше, когда вы наводите указатель мыши на неправильное поле, игровая площадка выдает конкретную ошибку. отображается сообщение, которое соответствует содержимому ошибки, возвращаемому сервером:

Ошибку такого рода можно обнаружить, когда мы пишем параметры запроса без необходимости делать запрос, и это здорово, не так ли?

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

Таким образом, для ответа клиенту мы можем просто вернуть тип ошибки и краткое описание ошибки:

{
  "error": {
    "errors":[
      {
        "code":"GRAPHQL_VALIDATION_FAILED",
        "message":"Cannot query field \"none\" on type \"Book\". Did you mean \"name\"?"
      }
    ]
  }
}

мы можем построитьApollo Serverнапример, передать файл с именемformatErrorфункция для форматирования возвращаемого сообщения об ошибке:

...
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: !isProd,
  playground: !isProd,
  mocks: true,
  formatError: error => {
    // log detail of error here
    return {
      code: error.extensions.code,
      message: error.message
    }
  }
})
...

Перезапустив сервис, запросив еще раз, мы обнаруживаем, что сообщение об ошибке отформатировано так, как мы ожидали:

Организация схем и преобразователей

На данный момент мы построилиGraphQLСервер по-прежнему очень прост:

├── index.js
├── package.json
└── yarn.lock

Его также нельзя применить к реальному проектированию, потому что это слишком自由, нам нужно спроектировать некоторые规矩, чтобы помочь нам лучше справляться с практическими инженерными проблемами.

К этому разделу, я полагаю, читатели уже почувствовалиGraphQLКакие изменения ментальной модели это принесло:

  • Наша первоначальная организация路由работа, частью которой стала текущая организацияSchemaРабота
  • Наша первоначальная организация控制器работа, частью которой стала текущая организацияResolverРабота

Давайте разработаем规矩, чтобы помочь нам организоватьSchemaиResolver:

  • новая папкаsrc, используемый для хранения большей части инженерного кода
  • существуетsrcновая папка вcomponents, используемый для хранения объектов данных
  • Каждый объект данных представляет собой папку, содержащую два файла:schema.jsиresolver.js, они соответственно хранят информацию о текущем объекте данныхSchemaиResolverописание
  • существуетsrc/componentsновая папка вbook, и создать в нем новыйschema.jsиresolver.jsдля храненияbookсвязанное описание
  • существуетsrcСоздать папкуgraphql, хранить всеGraphQLродственная логика
  • существуетgraphqlновый файл вindex.js, в видеGraphQLФайл запуска отвечает за сбор всех объектов данных при запуске серверного приложения, созданиеApollo Serverпример

После регулировки в соответствии с вышеуказанными шагами,graphql-server-demoВся структура выглядит следующим образом:

├── index.js
├── package.json
├── src
│   ├── components
│   │   └── book
│   │       ├── resolver.js
│   │       └── schema.js
│   └── graphql
│       └── index.js
└── yarn.lock

Далее корректируем код

Step 1

Первый взглядGraphQLвходной файлsrc/graphql/index.jsОбязанности:

  • Отвечает за чтение и слияние всех компонентовSchemaиResolver
  • ответственный за созданиеApollo Serverпример

входной файлsrc/graphql/index.jsОкончательный код выглядит следующим образом:

const fs = require('fs')
const { resolve } = require('path')
const { ApolloServer, gql } = require('apollo-server-koa')

const defaultPath = resolve(__dirname, '../components/')
const typeDefFileName = 'schema.js'
const resolverFileName = 'resolver.js'

/**
 * In this file, both schemas are merged with the help of a utility called linkSchema.
 * The linkSchema defines all types shared within the schemas.
 * It already defines a Subscription type for GraphQL subscriptions, which may be implemented later.
 * As a workaround, there is an empty underscore field with a Boolean type in the merging utility schema, because there is no official way of completing this action yet.
 * The utility schema defines the shared base types, extended with the extend statement in the other domain-specific schemas.
 *
 * Reference: https://www.robinwieruch.de/graphql-apollo-server-tutorial/#apollo-server-resolvers
 */
const linkSchema = gql`

  type Query {
    _: Boolean
  }

  type Mutation {
    _: Boolean
  }

  type Subscription {
    _: Boolean
  }
`

function generateTypeDefsAndResolvers () {
  const typeDefs = [linkSchema]
  const resolvers = {}

  const _generateAllComponentRecursive = (path = defaultPath) => {
    const list = fs.readdirSync(path)

    list.forEach(item => {
      const resolverPath = path + '/' + item
      const stat = fs.statSync(resolverPath)
      const isDir = stat.isDirectory()
      const isFile = stat.isFile()

      if (isDir) {
        _generateAllComponentRecursive(resolverPath)
      } else if (isFile && item === typeDefFileName) {
        const { schema } = require(resolverPath)

        typeDefs.push(schema)
      } else if (isFile && item === resolverFileName) {
        const resolversPerFile = require(resolverPath)

        Object.keys(resolversPerFile).forEach(k => {
          if (!resolvers[k]) resolvers[k] = {}
          resolvers[k] = { ...resolvers[k], ...resolversPerFile[k] }
        })
      }
    })
  }

  _generateAllComponentRecursive()

  return { typeDefs, resolvers }
}

const isProd = process.env.NODE_ENV === 'production'

const apolloServerOptions = {
  ...generateTypeDefsAndResolvers(),
  formatError: error => ({
    code: error.extensions.code,
    message: error.message
  }),
  introspection: !isProd,
  playground: !isProd,
  mocks: false
}

module.exports = new ApolloServer({ ...apolloServerOptions })

В приведенном выше коде мы видимlinkSchemaв стоимостиQuery,Mutation,SubscriptionТри записи типа определяют именованный_, типBooleanполе.

Это поле на самом деле является заполнителем, потому что несколько расширений пока официально не поддерживаются (extend) метод слияния типов, поэтому здесь мы можем сначала установить заполнитель для поддержки слияния расширений (extend)тип.

Step 2

Давайте определим объект данных:bookизSchemaиResolverСодержание:

// src/components/book/schema.js
const { gql } = require('apollo-server-koa')

const schema = gql`

  enum BookStatus {
    DELETED
    NORMAL
  }

  type Book {
    id: ID
    name: String
    price: Float
    status: BookStatus
  }

  extend type Query {
    book: Book
  }
`

module.exports = { schema }

здесь нам не нужноhelloэтот запрос, поэтому мы корректируемbookсвязанный код, удаленhello

Через приведенный выше код мы видим, что черезextendключевые слова, мы можем определить индивидуально дляbookтип запроса

// src/components/book/resolver.js
const BookStatus = {
  DELETED: 0,
  NORMAL: 1
}

const resolvers = {

  BookStatus,

  Query: {

    book: (parent, args, context, info) => ({
      name: '地球往事',
      price: 66.3,
      status: BookStatus.NORMAL
    })

  }
}

module.exports = resolvers

Приведенный выше код определяетbookисточник данных запроса,resolverФункция поддерживает возвратPromise

Step 3

Наконец, давайте настроим содержимое файла запуска сервисного приложения:

const Koa = require('koa')
const app = new Koa()
const apolloServer = require('./src/graphql/index.js')

apolloServer.applyMiddleware({ app })

app.listen({ port: 4000 }, () =>
  console.log(`🚀 Server ready at http://localhost:4000/graphql`)
)

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

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

Итак, что нам делать, когда существующие скалярные типы не соответствуют нашим потребностям?

Далее давайте посмотрим, как реализовать пользовательский скаляр

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

мыBookДобавьте новое поле с именемcreated, типDate

...
  type Book {
    id: ID
    name: String
    price: Float
    status: BookStatus
    created: Date
  }
...
    book: (parent, args, context, info) => ({
      name: '地球往事',
      price: 66.3,
      status: BookStatus.NORMAL,
      created: 1199116800000
    })

GraphQLне в стандартеDateТипа давайте реализуем кастомыDateтип:

Step 1

Во-первых, мы устанавливаем сторонний инструмент датыmoment:

yarn add moment
Step 2

Далее, вsrc/graphqlновая папка вscalars

mkdir src/graphql/scalars

мы вscalarsВ этой папке хранятся пользовательские скаляры

существуетscalarsНовый файл в:index.jsиdate.js

src/graphql/
├── index.js
└── scalars
    ├── date.js
    └── index.js

документscalars/index.jsОтвечает за экспорт пользовательских скаляровDate

module.exports = {
  ...require('./date.js')
}

документscalars/date.jsОтвечает за реализацию пользовательских скаляровDate

const moment = require('moment')
const { Kind } = require('graphql/language')
const { GraphQLScalarType } = require('graphql')

const customScalarDate = new GraphQLScalarType({
  name: 'Date',
  description: 'Date custom scalar type',
  parseValue: value => moment(value).valueOf(),
  serialize: value => moment(value).format('YYYY-MM-DD HH:mm:ss:SSS'),
  parseLiteral: ast => (ast.kind === Kind.INT)
    ? parseInt(ast.value, 10)
    : null
})

module.exports = { Date: customScalarDate }

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

при созданииGraphQLScalarTypeПри создании экземпляра мы можем указать:

  1. Имя пользовательского скаляра, т.е.name
  2. Введение в пользовательские скаляры, т.е.description
  3. Функция обработчика, когда пользовательское скалярное значение передается от клиента к серверу, то естьparseValue
  4. Функция обработчика, когда пользовательское скалярное значение возвращается с сервера клиенту, то естьserialize
  5. Для пользовательских скаляров вastБуквальная функция обработчика в , то естьparseLiteral(Это потому, что вastЗначения в всегда форматируются как строки)

astТо есть абстрактное синтаксическое дерево.Подробнее об абстрактном синтаксическом дереве см.:en.wikipedia.org/wiki/Абстрактное-Синтаксическое-Дерево

Step 3

Наконец, давайте добавим пользовательский скалярDateподняться наGraphQLВ файле запуска:

...

const allCustomScalars = require('./scalars/index.js')

...

const linkSchema = gql`

  scalar Date

  type Query {
    _: Boolean
  }

  type Mutation {
    _: Boolean
  }

  type Subscription {
    _: Boolean
  }
`
...

function generateTypeDefsAndResolvers () {
  const typeDefs = [linkSchema]
  const resolvers = { ...allCustomScalars }
...

Наконец, давайте проверим, перезапустим службу и запросимbookизcreatedполе, мы обнаружили, что сервер уже поддерживаетDateВведи это:

Пользовательские инструкции для достижения функции проверки входа

В этом разделе мы узнаем, какGraphQLНа сервере реализована функция проверки входа

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

GraphQLОн разрушает представление о том, что маршруты соответствуют ресурсам, и защищаетSchemaВнутренне отметьте, какие поля защищены, чтобы обеспечить защиту ресурсов.

мы хотимGraphQLДля реализации функции проверки входа в сервис необходимы следующие инструменты:

  • промежуточное ПО коа
  • контекст в распознавателе
  • пользовательская директива
Step 1

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

существуетsrcновая папка вmiddlewares, используется для хранения всего промежуточного ПО koa

mkdir src/middlewares

в папкеsrc/middlewaresновый файл вauth.js, как промежуточное ПО для монтирования пользовательской информации:

touch src/middlewares/auth.js
async function main (ctx, next) {

  // 注意,在真实场景中,需要在这里获取请求头部的用户签名,比如:token
  // 并根据用户 token 获取用户信息,然后将用户信息挂载到 ctx 上
  // 这里为了简单演示,省去了上述步骤,挂载了一个模拟的用户信息
  ctx.user = { name: 'your name', age: Math.random() }

  return next()
}

module.exports = main

Установите это промежуточное ПО в приложение:

...

app.use(require('./src/middlewares/auth.js'))

apolloServer.applyMiddleware({ app })


app.listen({ port: 4000 }, () =>
  console.log(`🚀 Server ready at http://localhost:4000/graphql`)
)

Здесь следует отметить одну деталь,authМонтаж промежуточного программного обеспечения должен быть вapolloServerкрепление спереди, это потому чтоkoaЗапросы проходят через стек промежуточного ПО в порядке монтирования, и мы ожидаем, что вapolloServerПеред обработкой запроса вctxИнформация о пользователе уже смонтирована на

Step 2

Через параметр контекста парсера, переданныйctxобъект, через который удобно получать информацию о пользователе (в предыдущем разделе мы ввели список формальных параметров парсера, а третий параметр называетсяcontext)

при созданииApollo Serverнапример, мы также можем указатьcontextвариант, значение может быть функцией

когдаcontextКогда значением является функция, объект контекста запроса приложенияctxбудет передан как атрибут первого формального параметра этой функции в текущийcontextфункция; иcontextВозвращаемое значение функции будет использоваться какcontextАргументы, передаваемые каждой функции парсера

Итак, нам нужно только написать так, мы можем поместить запрошенный объект контекстаctx, передается каждому парсеру:

...
const apolloServerOptions = {
  ...generateTypeDefsAndResolvers(),
  formatError: error => ({
    code: error.extensions.code,
    message: error.message
  }),
  context: ({ ctx }) => ({ ctx }),
  introspection: !isProd,
  playground: !isProd,
  mocks: false
}
...

Таким образом, каждую функцию парсера можно получить, просто получив третий параметрctx, так что черезctxполучить на этоuserСвойства (информация о пользователе)

Step 3

Затем мы разрабатываем пользовательскую директивуauth(этоauthenticationсокращение от )

существуетsrc/graphqlновая папка вdirectives, используемый для хранения всех пользовательских директив:

mkdir src/graphql/directives

мы вdirectivesВ этой папке хранятся пользовательские инструкции

существуетdirectivesНовый файл в:index.jsиauth.js

src/graphql
├── directives
│   ├── auth.js
│   └── index.js
├── index.js
└── scalars
    ├── date.js
    └── index.js

документdirectives/index.jsОтвечает за экспорт пользовательских директивauth

module.exports = {
  ...require('./auth.js')
}

документdirectives/auth.jsОтвечает за реализацию пользовательских директивauth

const { SchemaDirectiveVisitor, AuthenticationError } = require('apollo-server-koa')
const { defaultFieldResolver } = require('graphql')

class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition (field) {
    const { resolve = defaultFieldResolver } = field

    field.resolve = async function (...args) {
      const context = args[2]
      const user = context.ctx.user

      console.log('[CURRENT USER]', { user })

      if (!user) throw new AuthenticationError('Authentication Failure')

      return resolve.apply(this, args)
    }
  }
}

module.exports = {
  auth: AuthDirective
}

С приведенным выше кодом мы видимApollo ServerВ дополнение к предоставлению класса посетителя базовой директивыSchemaDirectiveVisitorКроме того, также предоставляется класс ошибки аутентификации.AuthenticationError

Мы объявляем обычайAuthDirectiveкласс, наследованиеSchemaDirectiveVisitor, а в его методе классаvisitFieldDefinitionНапишите логику аутентификации, которая должна выполняться для каждого захваченного поля в

Логика аутентификации очень проста, на основе исходного парсера поля можно обернуть слой логики аутентификации:

  1. мы пытаемся изfieldизвлекает свой парсер в , и временно сохраняет его в локальной переменной для последующего использования. Если недоступен, назначьте парсер по умолчаниюdefaultFieldResolver
  2. мы покрываемfieldАтрибут парсера — это наша пользовательская функция внутри функции, которую мы передаемargs[2]Получил доступ к третьему параметру функции парсера, получил его от него и смонтировал вctxинформация о пользователе на
  3. Выдает, если информация о пользователе не существуетAuthenticationErrorОшибка
  4. Возвращает исходный результат выполнения анализатора поля
Step 4

при созданииApollo Serverчерез экземплярschemaDirectivesВозможность монтировать пользовательские директивы:

...

const allCustomDirectives = require('./directives/index.js')

...

const apolloServerOptions = {
  ...generateTypeDefsAndResolvers(),
  formatError: error => ({
    code: error.extensions.code,
    message: error.message
  }),
  schemaDirectives: { ...allCustomDirectives },
  context: ({ ctx }) => ({ ctx }),
  introspection: !isProd,
  playground: !isProd,
  mocks: false
}

...

глобальноlinkSchemaобъявить директиву в объекте данныхSchemaДля каждого поля, которое необходимо защитить, отметьте@auth(Делегаты должны войти в систему, чтобы получить доступ к этому полю)

...
const linkSchema = gql`

  scalar Date

  directive @auth on FIELD_DEFINITION

  type Query {
    _: Boolean
  }

  type Mutation {
    _: Boolean
  }

  type Subscription {
    _: Boolean
  }
`
...

В приведенном выше кодеFIELD_DEFINITIONУказывает, что эта команда действует только на определенное поле

Здесь мы единственныеbookДобавьте нашу пользовательскую директиву в поле запроса@auth

...
const schema = gql`

  enum BookStatus {
    DELETED
    NORMAL
  }

  type Book {
    id: ID
    name: String
    price: Float
    status: BookStatus
    created: Date
  }

  extend type Query {
    book: Book @auth
  }
`
...

мыbookдобавлено поле запроса@authограничение

Далее перезапускаем сервис, запрашиваяbook, мы обнаруживаем, что терминал выводит:

[CURRENT USER] { user: { name: 'your name', age: 0.30990570160950015 } }

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

Далее мы закомментируемauthОлицетворенный пользовательский код в промежуточном программном обеспечении:

async function main (ctx, next) {
  // 注意,在真实场景中,需要在这里获取请求头部的用户签名,比如:token
  // 并根据用户 token 获取用户信息,然后将用户信息挂载到 ctx 上
  // 这里为了简单演示,省去了上述步骤,挂载了一个模拟的用户信息
  // ctx.user = { name: 'your name', age: Math.random() }

  return next()
}

module.exports = main

Перезапустите службу и повторите запросbook,Мы видели:

появилось в результатахerrors, кодовое значение которогоUNAUTHENTICATED, что означает, что наша директива успешно перехватила незарегистрированный запрос

запрос на слияние

Наконец, давайте посмотрим наGraphQLПроблема, вызванная конструкцией:不必要的请求

мы вgraphql-server-demoДобавьте новый объект данных в:cat

Окончательная структура каталогов выглядит следующим образом:

src
├── components
│   ├── book
│   │   ├── resolver.js
│   │   └── schema.js
│   └── cat
│       ├── resolver.js
│       └── schema.js
├── graphql
│   ├── directives
│   │   ├── auth.js
│   │   └── index.js
│   ├── index.js
│   └── scalars
│       ├── date.js
│       └── index.js
└── middlewares
    └── auth.js

вsrc/components/cat/schema.jsКод выглядит следующим образом:

const { gql } = require('apollo-server-koa')

const schema = gql`

  type Food {
    id: Int
    name: String
  }

  type Cat {
    color: String
    love: Food
  }

  extend type Query {
    cats: [Cat]
  }
`

module.exports = { schema }

Мы определяем два типа данных:CatиFood

и определить запрос:cats, этот запрос возвращает набор кошек

src/components/cat/resolver.jsКод выглядит следующим образом:

const foods = [
  { id: 1, name: 'milk' },
  { id: 2, name: 'apple' },
  { id: 3, name: 'fish' }
]

const cats = [
  { color: 'white', foodId: 1 },
  { color: 'red', foodId: 2 },
  { color: 'black', foodId: 3 }
]

const fakerIO = arg => new Promise((resolve, reject) => {
  setTimeout(() => resolve(arg), 300)
})

const getFoodById = async id => {
  console.log('--- enter getFoodById ---', { id })
  return fakerIO(foods.find(food => food.id === id))
}

const resolvers = {
  Query: {
    cats: (parent, args, context, info) => cats
  },
  Cat: {
    love: async cat => getFoodById(cat.foodId)
  }
}

module.exports = resolvers

Согласно вышеприведенному коду мы видим:

  • У каждого кота естьfoodIdПоле, значение - id любимой еды
  • Мы передаем функциюfakerIOдля имитации асинхронного ввода-вывода
  • Мы реализуем функциюgetFoodByIdПредоставляет функцию получения информации о еде на основе идентификатора еды при каждом ее вызове.getFoodByIdфункция, распечатает журнал на терминал

перезапустить службу, запроситьcats, мы видим, что результат возвращается нормально:

Посмотрим на вывод терминала и найдем:

--- enter getFoodById --- { id: 1 }
--- enter getFoodById --- { id: 2 }
--- enter getFoodById --- { id: 3 }

getFoodByIdФункция вызывается три раза по отдельности.

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

Пакет запросов, когда он связан с другими объектами данных, каждая конечная точка вызовет IO.

Это不必要的请求, потому что вышеуказанные запросы можно объединить в один запрос.

как мы совмещаем эти不必要的请求Шерстяная ткань?

Мы можем использоватьdataLoaderинструмент для объединения этих запросов.

dataLoaderПредусмотрены две основные функции:

  • Batching
  • Caching

В этой статье мы используем только егоBatchingФункции

оdataLoaderДля получения дополнительной информации см.:GitHub.com/graphup/datang…

Step 1

Сначала мы устанавливаемdataLoader

yarn add dataloader
Step 2

Далее мыsrc/components/cat/resolver.jsсередина:

  • обеспечить массовое получениеfoodФункцияgetFoodByIds
  • вводитьdataLoader, УпаковкаgetFoodByIdsфункция, которая возвращает обернутую функциюgetFoodByIdBatching
  • существуетloveиспользуется в функции парсераgetFoodByIdBatchingполучитьfood
const DataLoader = require('dataloader')

...

const getFoodByIds = async ids => {
  console.log('--- enter getFoodByIds ---', { ids })
  return fakerIO(foods.filter(food => ids.includes(food.id)))
}

const foodLoader = new DataLoader(ids => getFoodByIds(ids))

const getFoodByIdBatching = foodId => foodLoader.load(foodId)

const resolvers = {
  Query: {
    cats: (parent, args, context, info) => cats
  },
  Cat: {
    love: async cat => getFoodByIdBatching(cat.foodId)
  }
}

...

Перезапустите службу и повторите запросcats, мы все равно видим, что возвращается правильный результат, в это время заходим в терминал и находим:

--- enter getFoodByIds --- { ids: [ 1, 2, 3 ] }

Исходные три запроса ввода-вывода были успешно объединены в один.

Наконец, нашgraphql-server-demoСтруктура каталогов следующая:

├── index.js
├── package.json
├── src
│   ├── components
│   │   ├── book
│   │   │   ├── resolver.js
│   │   │   └── schema.js
│   │   └── cat
│   │       ├── resolver.js
│   │       └── schema.js
│   ├── graphql
│   │   ├── directives
│   │   │   ├── auth.js
│   │   │   └── index.js
│   │   ├── index.js
│   │   └── scalars
│   │       ├── date.js
│   │       └── index.js
│   └── middlewares
│       └── auth.js
└── yarn.lock

заключительные замечания

Прочитав это, я считаю, что вы очень заинтересованы в созданииGraphQLНа стороне сервера у меня общее впечатление.

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

На этом статья закончена, надеюсь, эта статья поможет вам в работе и жизни в будущем.

Ссылаться на

оApollo ServerПолный список параметров конструктора см.График Woohoo.Apollo up.com/docs/Apollo…


Front-end команда Shuidihuzhu набирает партнеров.Присылайте свое резюме на почтовый ящик: fed@shuidihuzhu.com