若是喜欢咱们的文章别忘了点击关注阿里南京技术专刊呦~ 本文转载自 阿里南京技术专刊-知乎,欢迎大牛小牛投递阿里南京前端/后端开发等职位,详见 阿里南京诚邀前端小伙伴加入~。前端
在最近的项目中,咱们选择了 GraphQL 做为 API 查询语言替代了传统的 Restful 传参的方法进行先后端数据传递。服务端选用了 egg.js + Apollo graphql-tools,前端使用了 React.js + Apollo graphql-client。这样的架构选择让咱们的迭代速度有了很大的提高。node
基于 GraphQL 的 API 服务在架构上来看算是 MVC 中的 controller。只是它只有一个固定的路由来处理全部请求。那么在和 MVC 框架结合使用时,在数据转换 ( Convertor )、参数校验 ( Validator ) 等功能上,使用 Apollo GraphQL 带来了一些新的处理方式。下面会介绍一下在这些地方使用 Graphql 带来的一些优点。git
GraphQL 是由 Facebook 创造的用于描述复杂数据模型的一种查询语言。这里查询语言所指的并非常规意义上的相似 sql 语句的查询语言,而是一种用于先后端数据查询方式的规范。sql
Apollo GraphQL 是基于 GraphQL 的全栈解决方案集合。从后端到前端提供了对应的 lib 使得开发使用 GraphQL 更加的方便express
在描述一个数据类型时,GraphQL 经过 type 关键字来定义一个类型,GraphQL 内置两个类型 Query 和 Mutation,用于描述读操做和写操做。npm
schema {
query: Query
mutation: Mutation
}
复制代码
正常系统中咱们会用到查询当前登陆用户,咱们在 Query 中定义一个读操做 currentUser ,它将返回一个 User 数据类型。后端
type Query {
currentUser: User
}
type User {
id: String!
name: String
avatar: String
# user's messages
messages(query: MessageQuery): [Message]
}
复制代码
当咱们的一个操做须要返回多种数据格式时,GraphQL 提供了 interface 和 union types 来处理。api
以上面的 Message 类型为例,咱们可能有多种消息类型,好比通知、提醒bash
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) 中是没有对请求的参数和返回值定义数据结构和类型的,每每咱们须要本身作类型转换。好比经过 GET 请求 url 后面问号转入的请求参数默认都是字符串,咱们可能要转成数字或者其余类型。
好比上面的获取当前用户的消息,以 egg.js 为例的话,Controller 会写成下面这样
// 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 的话,能够定义一个 Input 类型来描述请求的入参。好比上面的 MessageQuery
# 加上 ! 表示必填参数
input MessageQuery {
page: Int!
pageSize: Int!
}
复制代码
咱们能够声明 page 和 pageSize 是 Int 类型的,若是请求传入的值是非 Int 的话,会直接报错。
对于上面消息查询,咱们须要提供两个 resolver function。以使用 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);
}
}
};
复制代码
咱们上面定义的 User 的 id 为 String,这里返回的 id 是数字,这时候 Graphql 会帮咱们会转换,Graphql 的 type 默认都会有序列化与反序列化,能够参考下面的自定义类型。
GraphQL 默认定义了几种基本 scalar type (标量类型):
GraphQL 提供了经过自定义类型的方法,经过 scalar 申明一个新类型,而后在 resovler 中提供该类型的 GraphQLScalarType 的实例。
已最多见的日期处理为例,在咱们代码中的时间字段都是用的 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 相似与其余语言中的注解 (Annotation) 。能够经过 Directive 实现一些切面的事情,Graphql 内置了两个指令 @skip 和 @include ,用于在查询语句中动态控制字段是否须要返回。
在查询当前用户的时候,咱们可能不须要返回当前人的消息列表,咱们可使用 Directive 实现动态的 Query Syntax。
query CurrentUser($withMessages: Boolean!) {
currentUser {
name
messages @include(if: $withMessages) {
content
}
}
}
复制代码
最新的 graphql-js 中,容许自定义 Directive,就像 Java 的 Annotation 在建立的时候须要指定 Target 同样,GraphQL 的 Directive 也须要指定它能够用于的位置。
// 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 function 就像是一个 middleware ,它的第一个参数是 next,这样你能够在先后作拦截对数据进行处理。
对于入参和返回值,咱们有时候须要对它设定默认值,下面咱们建立一个 @Default 的directive。
directive @Default(value: Any ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
复制代码
next 是一个 Promise
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)
}
复制代码
GraphQL 简单的定义一组枚举使用 enum 关键字。相似于其余语言每一个枚举的 ordinal 值是它的下标。
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 有一个最大的优势就是在 Schema 定义中好全部数据后,经过一个请求能够获取全部想要的数据。可是当系统愈来愈庞大的时候,咱们须要对系统进行模块化拆分,演变成一个分布式微服务架构的系统。这样能够按照模块独立开发部署。
咱们经过 Apollo Link 能够远程记载 Schema ,而后在进行拼接 (Schema stitching)。
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,
});
复制代码
好比咱们对博客系统进行了模块化拆分,一个用户服务模块,一个文章服务模块,和咱们统一对外提供服务的 Gateway API 层。
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],
});
复制代码
在合并 Schemas 的时候,咱们能够对 Schema 进行扩展并添加新的 Resolver 。
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();
};
};
复制代码
这里能够看到咱们将 egg 的请求上下文传到来 GraphQL 的执行环境中,咱们在 resolver function 中能够拿到这个 context。
graphqlKoa 还有一些其余参数,咱们能够用来实现一些跟上下文相关的事情。
在上面咱们提到来如何实现基于 GraphQL 的分布式系统,那么全链路请求跟踪就是一个很是重要的事情。使用 Apollo GraphQL 只须要下面几步。
转眼已经 2018 年了,GraphQL 再也不是一个新鲜的名词。Apollo 做为一个全栈 GraphQL 解决方案终于在今年迎来了飞速的发展。咱们有幸在项目中接触并深度使用了 Apollo 的整套工具链。而且咱们感觉到了 Apollo 和 GraphQL 在一些方面的简洁和优雅,借此机会给你们分享它们的酸与甜。