前言
在介绍 GraphQL 之前,先讲一个故事:
前端同学: 你给我返回的接口里面客户列表全是 ID,能不能返回详细信息
后端同学: 你自己根据 ID 去调用客户详情接口好了
前端同学: 目前接口能带上客户详情吗
后端同学: 不能
后端同学: (卒)
实际开发中我们常常遇到这种情况,前后端常常因为接口字段,类型等等问题进行友好的交流。前端总是想一次性拿到所有的数据,避免多次请求,尤其是带有先后顺序的请求;后端不想跨不同数据源获取数据,希望保持每个数据源为独立的接口便于维护。
而 GraphQL 的出现就是为了解决以上问题。
什么是 GraphQL
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
它具有以下优点:
- 在前端准确描述想要的数据,不多也不少
- 获取多个资源只用一个请求,GraphQL 查询不仅能够获得资源的属性,还能沿着资源间引用进一步查询
- 类型系统保证了数据正确
现阶段的困难
虽然 GraphQL对于前端有很多价值,但是提供 GraphQL API 的工作量却在后端。
如何解决?
实际上,GraphQL 并不限制你运行的地方,我们完全可以把 GraphQL 网关层放在浏览器中。这样我们不需要后端配合,也可以等成熟后快速转换为后端服务。
实战
GraphQL网关
假设我们有两个接口,分别为获取订单列表接口和获取客户详情接口。
现在我们把它们转换为GraphQL。
首先我们需要定义GraphQL类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| # schema.graphqls # get /orders 返回 [Order] type Order { id: Int # 客户ID customerId: Int }
# get /customers/{id} 返回 Customer type Customer { id: Int name: String code: String }
type Query { order(page: Int, pageSize: Int): [Order] customer(id: Int): Customer }
|
然后我们定义GraphQL的schema
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { makeExecutableSchema } from "@graphql-tools/schema"; import typeDefs from './schema.graphqls'
const resolvers = { Query: { async order(obj: any, args: any, ctx: any, info: any) { const res = await fetch(`/orders?page=${args.page}&pageSize=${args.pageSize}`).then(res => res.json); return res; }, async customer(obj: any, args: any, ctx: any, info: any) { const res = await fetch(`/customer/${args.id}`).then(res => res.json); return res; }, } };
export default makeExecutableSchema({ typeDefs: typeDefs, resolvers, });
|
对这一步,和在nodejs上建立GraphQL的schema一模一样。然后我们需要将schema暴露出去。然后我们需要将schema暴露出去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| var express = require('express'); var { graphqlHTTP } = require('express-graphql'); var { buildSchema } = require('graphql');
var schema = buildSchema(` type Query { hello: String } `);
var root = { hello: () => { return 'Hello world!'; }, }; var app = express(); app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: root, graphiql: true, })); app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql');
|
1 2 3 4 5 6 7 8 9 10
| import schema from "./schema"; import { graphql } from "graphql";
export const graphqlClient = { async query(source: string) { const result = await graphql({ schema, source }); return result; }, };
|
1 2 3 4 5 6 7 8
| graphqlClient.query( `query getOrder{ order(page: 1, pageSize: 20) { id, customerId } }`)
|
扩展订单类型
我们知道GraphQL的优点之一就是将多个请求合并成一个请求,并且可以通过资源的字段属性进一步查询。
我们将在Order上添加Customer类型,以获取订单对应的客户名称。
1 2 3 4 5
| # schema.graphqls ... extend type Order { customer: Customer }
|
1 2 3 4 5 6 7 8 9 10 11 12
| const resolvers = { Query: { ... Order: { async customer: (order: any) { const res = await fetch(`/customer/${order.customerId}`).then(res => res.json); return res; } } } };
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| graphqlClient.query( `query getOrder{ order(page: 1, pageSize: 20) { id, customerId, customer { id name } } }`)
|
n+1问题
当我们执行一次订单查询的时候,会返回20个order,然后我们需要在调用20次客户查询接口,总计调用了 20+1次。
我们知道GraphqlQL会沿着链路依次查询,order->customer->xxx,如果链路很长,会导致GraphQL响应变慢。
目前GraphQL通用的解决方案是Facebook提供的Dataloader, 他的核心思想是Batch Query和Cached。
假设我们有一个接口,可以通过id列表查询所有的客户/customers?id=123,456,789
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import DataLoader from "dataloader"; import keyBy from "lodash/fp/keyBy"
async function batchGetCustomers(keys: string[]) { const result = await fetch("/customers?id=" + keys.join(",")).then(res => res.json()); const resultMap = keyBy("id", result); return keys.map((x) => resultMap[x] || null); }
const customerLoader = new DataLoader(batchGetCustomers);
const resolvers = { Query: { ... Order: { async customer: (order: any) { return customerLoader.load(order.customerId) } } } };
|
最终我们把查询从20+1次降低到1+1次
总结
虽然我们解决了复杂数据的组织问题,但是难以避免会产生各种性能问题。在以后的计划里,我们将更进一步:
- 使用独立后端提供GraphQL网关服务
- 使用持久层解决数据缓存问题
- 使用低代码实现任意数据的组织