предисловие
Изначально эту статью планировалось опубликовать во время 51-го праздника, но из-за небольшой проблемы с моим ноутбуком она была отложена до сих пор😂. Чтобы лучше изучить GraphQL для всех, я написал демонстрацию GraphQL для передней и задней части, включая некоторые распространенные операции входа в систему, добавления данных и получения данных. Интерфейс использует Vue и TypeScript, а сервер — Koa и GraphQL.
Это адрес предварительного просмотра:GraphQLDeomПользователь root по умолчанию, пароль root
Это адрес исходного кода:learn-graphql
Начало работы с GraphQL и связанными понятиями
Что такое GraphQL?
Согласно определению, данному в официальной документации,«GraphQL — это и язык запросов для API, и среда выполнения, которая удовлетворяет вашим запросам данных. GraphQL предоставляет полное и простое для понимания описание данных в вашем API, позволяя клиентам получать именно те данные, которые им нужны, и без какой-либо избыточности. , это также упрощает развитие API с течением времени, а также может использоваться для создания мощных инструментов разработчика».. Но после его использования я обнаружил, что gql требует слишком много бэкенда, система типов отлично подходит для фронтенда, но для бэкенда это может означать несколько запросов к базе данных. Хотя gql реализует оптимизацию HTTP-запросов, следует также учитывать производительность внутреннего ввода-вывода.
Запросы и изменения
Типы операций в GraphQL в основном делятся на запрос и изменение (и подписку на подписку), которые соответствуют ключевым словам запроса и мутации соответственно. Имя операции запроса и мутации можно не указывать. Но добавление имени действия позволяет избежать двусмысленности. Операция может передавать различные параметры, такие как параметр пейджинга в getHomeInfo и параметр атрибута заметки в AddNote. Далее мы в основном остановимся на запросе и мутации.
query getHomeInfo {
users(pagestart: ${pagestart}, pagesize: ${pagesize}) {
data {
id
name
createDate
}
}
}
mutation AddNote {
addNote(note: {
title: "${title}",
detail: "${detail}",
uId: "${uId}"
}) {
code
}
}
Schema
Полное название — язык определения схем. GraphQL реализует удобочитаемый синтаксис схемы, аналогичный SDL и JavaScript, который должен храниться в формате String. Нам нужно различать разницу между схемой GraphQL и схемой Mongoose. Схема GraphQL объявляет возвращаемые данные и структуру. Схема Mongoose объявляет структуру хранения данных.
система типов
Скалярный тип
GraphQL предоставляет некоторые скалярные типы по умолчанию: Int, Float, String, Boolean, ID. GraphQL поддерживает пользовательские скалярные типы, о которых мы поговорим позже.
тип объекта
Типы объектов являются наиболее распространенными типами в схеме, что позволяет использовать вложенные и циклические ссылки.
type TypeName {
fieldA: String
fieldB: Boolean
fieldC: Int
fieldD: CustomType
}
тип запроса
Тип запроса используется для получения данных, аналогично REST GET. Запрос — это отправная точка схемы, одного из типов корневого уровня. Запрос описывает данные, которые мы можем получить. В следующем примере определяются два типа запросов: getBooks, getAuthors.
type Query {
getBooks: [Book]
getAuthors: [Author]
}
- getBooks, получить список книг
- getAuthors, получить список авторов
В традиционном REST API, если вы хотите получить два списка, вам нужно сделать два http-запроса, но в gql вы можете запрашивать одновременно в одном запросе.
query {
getBooks {
title
}
getAuthors {
name
}
}
тип мутации
Типы мутаций аналогичны POST, PUT, DELETE в REST API. Подобно типам запросов, Mutation является отправной точкой для всех указанных операций с данными. Мутация addBook определена в приведенном ниже примере. Он принимает два параметра title, автор имеет тип String, а мутация вернет результат типа Book. Если для мутации или запроса требуется объект в качестве параметра, нам нужно определить тип ввода.
type Mutation {
addBook(title: String, author: String): Book
}
Следующая операция мутации вернет название книги и имя автора после операции сложения.
mutation {
addBook(title: "Fox in Socks", author: "Dr. Seuss") {
title
author {
name
}
}
}
тип ввода
Типы ввода позволяют передавать объекты в качестве параметров в Query и Mutation. Тип ввода — это общий тип объекта, который определяется с помощью ключевого слова input. Типы ввода также можно использовать, когда одни и те же параметры требуются для разных параметров.
input PostAndMediaInput {
title: String
body: String
mediaUrls: [String]
}
type Mutation {
createPost(post: PostAndMediaInput): Post
}
Как описать тип? (Примечание)
Стили комментариев, поддерживающие многострочный и однострочный текст в схеме
type MyObjectType {
"""
Description
Description
"""
myField: String!
otherField(
"Description"
arg: Int
)
}
🌟 Пользовательские скалярные типы
Как настроить скалярные типы? Мы добавляем следующую строку в строку Scheme. MyCustomScalar — это имя нашего пользовательского скаляра. Затем нужно пройти в распознавательGraphQLScalarTypeЭкземпляр пользовательского скалярного поведения.
scalar MyCustomScalar
Давайте рассмотрим пример использования типа Date в качестве скаляра. Сначала добавьте скаляр даты в схему
const typeDefs = gql`
scalar Date
type MyType {
created: Date
}
`
Далее нам нужно определить поведение скаляра в интерпретаторе распознавателей. Проблема в том, что в документе приведен только простой пример и не объясняется конкретная роль некоторых параметров. я здесьstackoverlfowУвидел хорошее объяснение выше.
serialize — это метод, который будет вызываться при отправке значения клиенту. parseValue и parseLiteral — это методы, которые принимают клиентские значения и вызывают. parseLiteral будет обрабатывать параметры Graphql, а параметры будут проанализированы и преобразованы в абстрактное синтаксическое дерево AST. parseLitera примет ast и вернет проанализированное значение типа. parseValue будет обрабатывать переменную.
const { GraphQLScalarType } = require('graphql')
const { Kind } = require('graphql/language')
const resolvers = {
Date: new GraphQLScalarType({
name: 'Date',
description: 'Date custom scalar type',
// 对来自客户端的值进行处理, 对变量的处理
parseValue(value) {
return new Date(value)
},
// 对返回给客户端的值进行处理
serialize(value) {
return value.getTime()
},
// 对来自客户端的值进行处理,对参数的处理
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
return parseInt(ast.value, 10)
}
return null
},
}),
}
интерфейс
Интерфейс — это абстрактный тип, который содержит некоторые поля.Если объектному типу необходимо реализовать этот интерфейс, он должен содержать эти поля.
interface Avengers {
name: String
}
type Ironman implements Avengers {
id: ID!
name: String
}
резольверы резольверы
Резолверы обеспечивают поведение, которое преобразует операции gql (запрос, изменение или подписка) в данные, и они возвращают данные, которые мы указываем в схеме, или обещание для этих данных. Парсер имеет четыре параметра: parent, args, context, info.
- parent, результат синтаксического анализа родительского типа
- args, аргументы для операции
- контекст, контекст синтаксического анализатора, включая статус запроса и информацию об аутентификации и т. д.
- info, Информация о состоянии выполнения операции, которую следует использовать только в сложных случаях.
парсер по умолчанию
Мы не писали синтаксические анализаторы для всех полей в схеме, но запрос все равно будет успешным. gql имеет парсер по умолчанию. Если у родительского объекта есть свойство с таким же именем, интерпретатор для поля писать не нужно. Он будет читать одноименное свойство из верхнего объекта.
преобразователь типов
Мы можем написать парсеры для любого поля в схеме, а не только для запросов и мутаций. Именно это делает GraphQL таким гибким.
В следующем примере мы пишем отдельный синтаксический анализатор для поля «гендер» и возвращаем выражения эмодзи. Первый параметр синтаксического анализатора пола — это результат синтаксического анализа родительского типа.
const typeDefs = gql`
type Query {
users: [User]!
}
type User {
id: ID!
gender: Gender
name: String
role: Role
}
enum Gender {
MAN
WOMAN
}
type Role {
id: ID!
name: String
}
`
const resolves = {
User: {
gender(user) {
const { gender } = user
return gender === 'MAN' ? '👨' : '👩'
}
}
}
ApolloServer
Что такое АполлоСервер?
ApolloServer — это фреймворк GraphQL с открытым исходным кодом в ApolloServer 2. ApolloServer можно использовать только как сервер, а ApolloServer также можно использовать в качестве подключаемого модуля для сред Node, таких как Express и Koa.
Стройте быстро
Как мы уже говорили. В ApolloServer2 ApolloServer может самостоятельно построить сервер GraphQL (подробности см. в документации Apollo). Однако в моем личном демонстрационном проекте, учитывая активность сообщества и богатство промежуточного программного обеспечения, я, наконец, выбрал Koa2 в качестве среды разработки и ApolloServer в качестве подключаемого модуля. Ниже приведен простой пример того, как Koa2 создает сервис с помощью Apollo.
const Koa = require('koa')
const { ApolloServer } = require('apollo-server-koa')
const typeDefs = require('./schemas')
const resolvers = require('./resolvers')
const app = new Koa()
const mode = process.env.mode
// KOA的中间件
app.use(bodyparser())
app.use(response())
// 初始化REST的路由
initRouters()
// 创建apollo的实例
const server = new ApolloServer({
// Schema
typeDefs,
// 解析器
resolvers,
// 上下文对象
context: ({ ctx }) => ({
auth: ctx.req.headers['x-access-token']
}),
// 数据源
dataSources: () => initDatasource(),
// 内省
introspection: mode === 'develop' ? true : false,
// 对错误信息的处理
formatError: (err) => {
return err
}
})
server.applyMiddleware({ app, path: config.URL.graphql })
module.exports = app.listen(config.URL.port)
Схема сборки
Экспорт функций gql из ApolloServer. И через функцию gql создать typeDefs. typeDefs — это то, что мы называем SDL. TypeDefs содержат все типы данных в gql, а также запросы и мутации. Можно рассматривать как план для всех типов данных и их взаимосвязей.
const { gql } = require('apollo-server-koa')
const typeDefs = gql`
type Query {
# 会返回User的数组
# 参数是pagestart,pagesize
users(pagestart: Int = 1, pagesize: Int = 10): [User]!
}
type Mutation {
# 返回新添加的用户
addUser(user: User): User!
}
type User {
id: ID!
name: String
password: String
createDate: Date
}
`
module.exports = typeDefs
Так как нам нужно записать все типы данных в строку схемы. Помещение всех этих типов данных в один файл является препятствием для дальнейшего обслуживания. мы можем использоватьmerge-graphql-schemas, разбить схему.
const { mergeTypes } = require('merge-graphql-schemas')
// 多个不同的Schema
const NoteSchema = require('./note.schema')
const UserSchema = require('./user.schema')
const CommonSchema = require('./common.schema')
const schemas = [
NoteSchema,
UserSchema,
CommonSchema
]
// 对Schema进行合并
module.exports = mergeTypes(schemas, { all: true })
Подключить источник данных
После того, как мы построим схему, нам нужноисточник данныхПодключитесь к API схемы. В моем демонстрационном примере я накладываю GraphQL API поверх REST API (эквивалентно выполнению агрегации в REST API). Источник данных Apollo инкапсулирует логику доступа ко всем данным. В источнике данных операции могут выполняться непосредственно над базой данных, либо запросы могут выполняться через REST API. Далее рассмотрим, как создать источник данных для REST API.
// 安装apollo-datasource-rest
// npm install apollo-datasource-rest
const { RESTDataSource } = require('apollo-datasource-rest')
// 数据源继承RESTDataSource
class UserAPI extends RESTDataSource {
constructor() {
super()
// baseURL是基础的API路径
this.baseURL = `http://127.0.0.1:${config.URL.port}/user/`
}
/**
* 获取用户列表的方法
*/
async getUsers (params, auth) {
// 在服务内部发起一个http请求,请求地址 baseURL + users
// 我们会在KoaRouter中处理这个请求
let { data } = await this.get('users', params, {
headers: {
'x-access-token': auth
}
})
data = Array.isArray(data) ? data.map(user => this.userReducer(user)) : []
// 返回格式化的数据
return data
}
/**
* 对用户数据进行格式化的方法
*/
userReducer (user) {
const { id, name, password, createDate } = user
return {
id,
name,
password,
createDate
}
}
}
module.exports = UserAPI
Теперь источник данных построен, это очень просто 😊. Затем мы добавляем источник данных в ApolloServer. Позже мы можем получить источник данных об использовании в распознавателе Resolve.
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ ctx }) => ({
auth: ctx.req.headers['x-access-token']
}),
// 添加数据源
dataSources: () => {
UserAPI: new UserAPI()
},
introspection: mode === 'develop' ? true : false,
formatError: (err) => {
return err
}
})
писать распознаватели
В настоящее время мы не можем выполнять запросы или изменения. Теперь нам нужно написать парсер. В предыдущем введении мы узнали о парсереОбеспечивает поведение для преобразования операций gql (запрос, изменение или подписка) в данные.. Существует три основных типа синтаксических анализаторов: синтаксические анализаторы запросов, синтаксические анализаторы мутаций и синтаксические анализаторы типов. Ниже приведен пример синтаксического анализатора запросов и синтаксического анализатора мутаций, которые расположены в полях Запрос и Мутация объекта синтаксического анализатора соответственно. Поскольку это корневой преобразователь, первый родитель пуст. Второй параметр — это параметр, переданный нам запросом или изменением. Третий параметр — объект контекста нашего аполлона, мы можемПолучите источник данных, который мы добавили ранее, из объекта контекста.. Синтаксический анализатор должен возвращать данные, соответствующие схеме Schema, или Promise для этих данных. В синтаксическом анализаторе мутаций поля в синтаксическом анализаторе запросов должны соответствовать типу запроса в схеме и полям типа мутации.
module.exports = {
// 查询解析器
Query: {
users (_, { pagestart, pagesize }, { dataSources, auth }) {
// 调用UserAPI数据源的getUsers方法, 返回User的数组
return dataSources.UserAPI.getUsers({
pagestart,
pagesize
}, auth)
}
},
// 突变解析器
Mutation: {
// 调用UserAPI数据源的addUser方法
addUser (_, { user }, { dataSources, auth }) {
return dataSources.UserAPI.addUser(user, auth)
}
}
}
Затем мы подключаем парсер к AppleServer.
const server = new ApolloServer({
// Schema
typeDefs,
// 解析器
resolvers,
// 添加数据源
dataSources: () => {
UserAPI: new UserAPI()
}
})
Что ж, пока мы в основном завершили слой graphql, и наш слой graphql в конечном итоге будет вызывать интерфейс REST API в источнике данных. Следующая операция — набор традиционных MVC. Я считаю, что друзья, знакомые с Koa или Express, должны быть знакомы с ним. Если у вас есть незнакомые друзья, вы можете обратиться кисходный кодв папке маршрутов и папке контроллера. Блок-схема запроса ниже.
разное
Об аутентификации
Об аутентификации Apollo предоставляет множестворешение.
Аутентификация по схеме
Аутентификация по схеме подходит для служб, которые не являются общедоступными для внешнего мира.Это метод аутентификации по принципу «все или ничего». Если вам нужно добиться такого рода аутентификации, вам нужно только изменить контекст
const server = new ApolloServer({
context: ({ req }) => {
const token = req.headers.authorization || ''
const user = getUser(token)
// 所有的请求都会经过鉴权
if (!user) throw new AuthorizationError('you must be logged in');
return { user }
}
})
Аутентификация парсера
В большинстве случаев нам нужно предоставить некоторые API (например, интерфейсы входа), которые не требуют аутентификации. В настоящее время нам нужен более детальный контроль разрешений, и мы можем поместить контроль разрешений в синтаксический анализатор.
Сначала добавьте информацию о разрешении в объект контекста.
const server = new ApolloServer({
context: ({ ctx }) => ({
auth: ctx.req.headers.authorization
})
})
Управление разрешениями для определенных парсеров запросов или мутаций
const resolves = {
Query: {
users: (parent, args, context) => {
if (!context.auth) return []
return ['bob', 'jake']
}
}
}
Авторизация за пределами GraphQL
Я использую решение для авторизации вне GraphQL. Я буду использовать промежуточное ПО для операций аутентификации в REST API. Но нам нужно передать информацию о разрешениях, содержащуюся в request.header, в REST API.
// 数据源
async getUserById (params, auth) {
// 将权限信息传递给REST API
const { data } = await this.get('/', params, {
headers: {
'x-access-token': auth
}
})
data = this.userReducer(data)
return data
}
// *.router.js
const Router = require('koa-router')
const router = new Router({ prefix: '/user' })
const UserController = require('../controller/user.controller')
const authentication = require('../middleware/authentication')
// 适用鉴权中间件
router.get('/users', authentication(), UserController.getUsers)
module.exports = router
// middleware authentication.js
const jwt = require('jsonwebtoken')
const config = require('../config')
const { promisify } = require('util')
const redisClient = require('../config/redis')
const getAsync = promisify(redisClient.get).bind(redisClient)
module.exports = function () {
return async function (ctx, next) {
const token = ctx.headers['x-access-token']
let decoded = null
if (token) {
try {
// 验证jwt
decoded = await jwt.verify(token, config.jwt.secret)
} catch (error) {
ctx.throw(403, 'token失效')
}
const { id } = decoded
try {
// 验证redis存储的jwt
await getAsync(id)
} catch (error) {
ctx.throw(403, 'token失效')
}
ctx.decoded = decoded
// 通过验证
await next()
} else {
ctx.throw(403, '缺少token')
}
}
}