Практика сервера Apollo GraphQL

GraphQL полный стек
Практика сервера Apollo GraphQL

Если вам понравилась наша статья, не забудьте нажать и подписаться на специальный выпуск Ali Nanjing Technology~ Эта статья воспроизведена изСпециальный выпуск Alibaba Nanjing Technology — Знание, приветствуем Даниэля и Маверикс для публикации вакансий, таких как разработка клиентского/внутреннего интерфейса Alibaba Nanjing, подробности см.Alibaba Nanjing искренне приглашает внешних партнеров присоединиться~.

В недавнем проекте мы выбрали GraphQL в качестве языка запросов API, чтобы заменить традиционный метод передачи параметров Restful для передачи данных через интерфейс и сервер. Сервер использует egg.js + Apollo graphql-tools, а фронтенд использует React.js + Apollo graphql-client. Этот архитектурный выбор значительно повысил скорость нашей итерации.

на основеGraphQLС точки зрения архитектуры служба API рассматривается как контроллер в MVC. Просто у него есть только один фиксированный маршрут для обработки всех запросов. Затем в сочетании с инфраструктурой MVC в преобразовании данных ( Convertor ), проверке параметров ( Validator ) и других функциях используйтеApollo GraphQLПриносит некоторые новые способы борьбы с ним. Ниже приведены некоторые преимущества использования Graphql в этих местах.

Что такое GraphQL:

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

Что такое Apollo GraphQL:

Apollo GraphQL — это набор полнофункциональных решений, основанных на GraphQL. Соответствующие библиотеки предоставляются от бэкэнда к интерфейсу, что упрощает разработку и использование GraphQL.

Type System

При описании типа данных GraphQL определяет тип с помощью ключевого слова type.GraphQL имеет два встроенных типа, Query и Mutation, которые используются для описания операций чтения и операций записи.

schema {
  query: Query
  mutation: Mutation
}

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

type Query {
  currentUser: User
}

type User {
  id: String!
  name: String
  avatar: String
  # user's messages
  messages(query: MessageQuery): [Message]
}

Interface & Union types

Когда одной из наших операций необходимо вернуть несколько форматов данных, GraphQL предоставляетinterfaceа такжеunion typesобрабатывать.

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

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

interface Message {
  content: String
}

type Notice implements Message {
  content: String
  noticeTime: Date
}

type Remind implements Message {
  content: String
  endTime: Date
}

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

union Notification = Message | Email

Проверка данных

В большинстве фреймворков Node.js mvc (express, koa) нет структуры данных и типов, определенных для параметров запроса и возвращаемых значений, и часто нам приходится выполнять преобразование типов самостоятельно. Например, параметры запроса, передаваемые знаком вопроса после URL-адреса запроса GET, по умолчанию являются строками, и нам может потребоваться преобразовать их в числа или другие типы.

Возьмем в качестве примера egg.js, контроллер будет написан следующим образом

// app/controller/message.js
const Controller = require('egg').Controller;
class MessageController extends Controller {
  async create() {
    const { ctx, service } = this;
    const { page, pageSize } = ctx.query;
    const pageNum = parseInt(page, 0) || 1;
    const pageSizeNum = parseInt(pageSize, 0) || 10;

    const res = await service.message.getByPage(pageNum, pageSizeNum);
    
    ctx.body = res;
  }
}
module.exports = MessageController;

Лучший подход — определить структуру JSON Schema + Validator для проверки и преобразования.

Проверка и преобразование типа GraphQL

Параметры GraphQL строго типизированы

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

# 加上 ! 表示必填参数
input MessageQuery {
  page: Int! 
  pageSize: Int!
}

Мы можем объявить, что page и pageSize имеют тип Int. Если значение, переданное в запросе, не является Int, об ошибке будет сообщено напрямую.

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

module.exports = {
  Query: {
    currentUser(parent, args, ctx) {
      return {
        id: 123,
        name: 'jack'
      };
    }
  },
  User: {
    messages(parent, {query: {page, pageSize}}, ctx) {
      return service.message.getByPage(page, pageSize);
    }
  }
};

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

пользовательский тип

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

  • Int: 32-битное целое число со знаком.
  • Float: A signed double-precision floating-point value.
  • String: последовательность символов UTF-8.
  • Boolean: true or false.
  • ID: The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache. The ID type is serialized in the same way as a String; however, defining it as an IDозначает, что он не предназначен для чтения человеком.

GraphQL предоставляет методы для передачи пользовательских типов черезscalarОбъявите новый тип, затем предоставьте экземпляр GraphQLScalarType этого типа в resovler.

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

# schema.graphql 中申明类型
scalar Date
// resovler.js
const { GraphQLScalarType } = require('graphql');
const { Kind } = require('graphql/language');
const _ = require('lodash');

module.exports = {
  Date: new GraphQLScalarType({
      name: 'Date',
      description: 'Date custom scalar type',
      parseValue(value) {
        return new Date(value);
      },
      serialize(value) {
        if (_.isString(value) && /^\d*$/.test(value)) {
          return parseInt(value, 0);
        } else if (_.isInteger(value)) {
          return value;
        }
        return value.getTime();
      },
      parseLiteral(ast) {
        if (ast.kind === Kind.INT) {
          return new Date(parseInt(ast.value, 10));
        }
        return null;
      }
    });
}

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

type Comment {
  id: Int!
  content: String
  creator: CommonUser
  feedbackId: Int
  gmtCreate: Date
  gmtModified: Date
}

Директивы

Директивы GraphQL аналогичны аннотациям на других языках. Некоторые аспекты могут быть достигнуты с помощью Directive.Graphql имеет две встроенные директивы @skip и @include, которые используются для динамического управления необходимостью возврата полей в операторе запроса.

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

query CurrentUser($withMessages: Boolean!) {
  currentUser {
    name
    messages @include(if: $withMessages) {
      content
    }
  }
}

В последних версиях graphql-js разрешены пользовательские директивы.Точно так же, как аннотация Java должна указывать цель при ее создании, директива GraphQL также должна указывать, где ее можно использовать.

DirectiveLocation enum

// Request Definitions -- in query syntax
QUERY: 'QUERY',
MUTATION: 'MUTATION',
SUBSCRIPTION: 'SUBSCRIPTION',
FIELD: 'FIELD',
FRAGMENT_DEFINITION: 'FRAGMENT_DEFINITION',
FRAGMENT_SPREAD: 'FRAGMENT_SPREAD',
INLINE_FRAGMENT: 'INLINE_FRAGMENT',
// Type System Definitions -- in type schema
SCHEMA: 'SCHEMA',
SCALAR: 'SCALAR',
OBJECT: 'OBJECT',
FIELD_DEFINITION: 'FIELD_DEFINITION',
ARGUMENT_DEFINITION: 'ARGUMENT_DEFINITION',
INTERFACE: 'INTERFACE',
UNION: 'UNION',
ENUM: 'ENUM',
ENUM_VALUE: 'ENUM_VALUE',
INPUT_OBJECT: 'INPUT_OBJECT',
INPUT_FIELD_DEFINITION: 'INPUT_FIELD_DEFINITION'

Directive Resolver

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

Для входных параметров и возвращаемых значений нам иногда нужно установить для него значения по умолчанию.Давайте создадим директиву @Default.

directive @Default(value: Any ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION

следующее обещание

const _ = require('lodash');

module.exports = {
  Default: (next, src, { value }, ctx, info) => next().then(v => _.defaultTo(v, value))
};

Затем, когда предыдущему MessageQuery требуется значение по умолчанию, вы можете использовать @Default

input MessageQuery {
  page: Int @Default(value: 1)
  pageSize: Int @Default(value: 15)
}

Enumeration types

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

enum Status {
  OPEN      # ordinal = 0
  CLOSE     # ordinal = 1
}

При использовании перечислений нам часто нужно передавать все перечисления на стойку регистрации для выбора. Затем нам нужно создать объект GraphQLEnumType для определения перечисления, а затем получить все определения через метод getValues ​​объекта.

// enum resolver.js

const { GraphQLEnumType } = require('graphql');
const status = new GraphQLEnumType({
  name: 'StatusEnum',
  values: {
    OPEN: {
      value: 0,
      description: '开启'
    },
    CLOSE: {
      value: 1,
      descirption: '关闭'
    }
  }
});

module.exports = {
  Status: status,
  Query: {
    status: status.getValues()
  }
};

модульный

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

Remote Schema

Мы можем удаленно записать схему через Apollo Link, а затем выполнить сшивку схемы.

import { HttpLink } from 'apollo-link-http';
import fetch from 'node-fetch';

const link = new HttpLink({ uri: 'http://api.githunt.com/graphql', fetch });

const schema = await introspectSchema(link);

const executableSchema = makeRemoteExecutableSchema({
  schema,
  link,
});

Merge Schema

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

import { HttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import fetch from 'node-fetch';

const userLink = new HttpLink({ uri: 'http://user-api.xxx.com/graphql', fetch });
const blogLink = new HttpLink({ uri: 'http://blog-api.xxx.com/graphql', fetch });

const userWrappedLink = setContext((request, previousContext) => ({
  headers: {
    'Authentication': `Bearer ${previousContext.graphqlContext.authKey}`,
  }
})).concat(userLink);

const userSchema = await introspectSchema(userWrappedLink);
const blogSchema = await introspectSchema(blogLink);

const executableUserSchema = makeRemoteExecutableSchema({
  userSchema,
  userLink,
});
const executableBlogSchema = makeRemoteExecutableSchema({
  blogSchema,
  blogLink,
});

const schema = mergeSchemas({
  schemas: [executableUserSchema, executableBlogSchema],
});

resolvers between schemas

При объединении схем мы можем расширить схему и добавить новые преобразователи.

const linkTypeDefs = `
  extend type User {
    blogs: [Blog]
  }

  extend type Blog {
    author: User
  }
`;
mergeSchemas({
  schemas: [chirpSchema, authorSchema, linkTypeDefs],
  resolvers: mergeInfo => ({
    User: {
      blogs: {
        fragment: `fragment UserFragment on User { id }`,
        resolve(parent, args, context, info) {
          const authorId = parent.id;
          return mergeInfo.delegate(
            'query',
            'blogByAuthorId',
            {
              authorId,
            },
            context,
            info,
          );
        },
      },
    },
    Blog: {
      author: {
        fragment: `fragment BlogFragment on Blog { authorId }`,
        resolve(parent, args, context, info) {
          const id = parent.authorId;
          return mergeInfo.delegate(
            'query',
            'userById',
            {
              id,
            },
            context,
            info,
          );
        },
      },
    },
  }),
});

контекст выполнения

Apollo Server предоставляет промежуточное программное обеспечение, которое интегрируется с различными платформами для выполнения обработки запросов GraphQL. Например, в Egg.js, поскольку Egg.js основан на koa, мы можем выбрать apollo-server-koa.

npm install --save apollo-server-koa

Мы можем обрабатывать запросы graphql, предоставляя промежуточное программное обеспечение.

const { graphqlKoa, graphiqlKoa } = require('apollo-server-koa');

module.exports = (_, app) => {
  const options = app.config.graphql;
  const graphQLRouter = options.router;

  return async (ctx, next) => {
    if (ctx.path === graphQLRouter) {
      return graphqlKoa({
        schema: app.schema,
        context: ctx,
      })(ctx);
    }
    await next();
  };
};

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

У graphqlKoa есть и другие параметры, которые мы можем использовать для реализации некоторых контекстно-зависимых вещей.

  • schema: the GraphQLSchema to be used
  • context: the context value passed to resolvers during GraphQL execution
  • rootValue: the value passed to the first resolve function
  • formatError: a function to apply to every error before sending the response to clients
  • validationRules: additional GraphQL validation rules to be applied to client-specified queries
  • formatParams: a function applied for each query in a batch to format parameters before execution
  • formatResponse: a function applied to each response after execution
  • tracing: when set to true, collect and expose trace data in the Apollo Tracing format

Распределенное отслеживание запросов на полную ссылку

Выше мы упомянули, как реализовать распределенную систему на основе GraphQL, тогда очень важна полносвязная отслеживание запросов. Использование Apollo GraphQL — это всего несколько шагов.

  1. Включите трассировку в каждой модульной системе, то есть установите для параметра трассировки graphqlKoa выше значение true.
  2. Создайте глобально уникальный tracingId в записи запроса и передайте его контексту каждого модуля через context и apollo-link-context.
  3. В конце запроса каждый модуль сообщает свои данные трассировки
  4. Затем используйте graphql, чтобы создать платформу запросов для сообщаемых данных мониторинга.

напиши в конце

Перенесемся в 2018 год, и GraphQL больше не будет новым термином. В этом году Apollo, как полнофункциональное решение GraphQL, наконец-то начало активно развиваться. Нам повезло, что мы имеем доступ ко всей цепочке инструментов Apollo и активно используем ее в наших проектах. И мы чувствуем простоту и элегантность Apollo и GraphQL в некоторых аспектах, и пользуемся случаем, чтобы поделиться с вами их кислым и сладким.