В этой статье предполагается, что читатели имеют определенное представление о 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"}}}
Указывает, что мы успешно создалиGraphQL
API-сервис~
Используйте команду в терминале для отладкиGraphQL
API, чего явно не хочет большинство из нас.
Нам нужен 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
При создании экземпляра мы можем указать:
- Имя пользовательского скаляра, т.е.
name
- Введение в пользовательские скаляры, т.е.
description
- Функция обработчика, когда пользовательское скалярное значение передается от клиента к серверу, то есть
parseValue
- Функция обработчика, когда пользовательское скалярное значение возвращается с сервера клиенту, то есть
serialize
- Для пользовательских скаляров в
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
Напишите логику аутентификации, которая должна выполняться для каждого захваченного поля в
Логика аутентификации очень проста, на основе исходного парсера поля можно обернуть слой логики аутентификации:
- мы пытаемся из
field
извлекает свой парсер в , и временно сохраняет его в локальной переменной для последующего использования. Если недоступен, назначьте парсер по умолчаниюdefaultFieldResolver
- мы покрываем
field
Атрибут парсера — это наша пользовательская функция внутри функции, которую мы передаемargs[2]
Получил доступ к третьему параметру функции парсера, получил его от него и смонтировал вctx
информация о пользователе на - Выдает, если информация о пользователе не существует
AuthenticationError
Ошибка - Возвращает исходный результат выполнения анализатора поля
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