当下,前后端分离是互联网应用程序开发的主流做法,如何设计合理且高效的前后端交互Web API是前端和后台开发人员日常开发工作的一大难点和痛点。回想我们在日常开发过程中经常会碰到的几个场景:
相信你对上面这个场景非常熟悉,因为我们每天都可能在反复经历着类似的场景。那么,如何解决这些问题呢?这就是今天我们要介绍的内容,我们将引入GraphQL这套全新的技术体系,它为我们解决上述问题提供了解决方案。
相比REST,GraphQL可以说是一项比较新的技术体系,2012年诞生在Facebook内部,并于2015年正式开源。顾名思义,GraphQL是一种基于图(Graph)的查询语言(Query Language,QL),从根本上改变了前后端交互API的定义和实现方式。
要想使用GraphQL,我们首先需要关注它发送请求的方式。这里我们可以举一个例子。假设系统中存在一个获取用户信息的场景,那么一个典型的请求示例如下所示。
{
user (id: "1") {
name
address
}
}
可以看到基于GraphQL的请求方式与使用传统的RESTful API有很大的不同。除了在请求体中指定了目标User对象的参数id值之外,我们还额外指定了“name”和“address”这两个参数,也就是告诉服务器端这次请求所希望获取的数据字段。
显然,这种请求方式完美解决了前端无法预判响应的数据格式问题,因为前端在请求的同时已经知道从服务端返回的数据字段就是请求中指定的字段,因此前端就不需要再对响应结果进行专门的判断和处理。
针对传统交互方式中存在的多次请求问题,GraphQL可以把多次请求合并成一次。例如,我们可以发送这样一个请求。
{
users {
name
address
family{
count
}
}
}
在该请求中,我们一方面指定了想要获取的User对象中的 “name”和“address” 字段,同时也指定了需要获取用户对应的家庭字段“family”以及它的子字段“count”。这样,通过一次请求,我们就可以同时获取用户信息和家庭信息,而不需要发送两次请求。
讲到这里,你可能已经注意到,通过GraphQL发起请求实际上只需要指定一个HTTP端点地址即可,因为我们可以基于同一个端点通过传入不同的参数而获取不同的结果,也就不需要专门设计一批HTTP端点来分别处理不同的请求了。
现在,我们已经了解了GraphQL的功能特性。那么,如何实施GraphQL呢?
首先明确,我们并不推荐在任何场景下都使用GraphQL。对于那些API定义与资源概念匹配度较高、也不需要实现类似用户信息内部嵌套家庭成员信息的复杂查询场景,传统的RESTful API仍然是首选,各个HTTP端点之间相互独立,职责非常明确。但对有些场景而言,GraphQL则更有优势。包括:
对于大多数系统而言,GraphQL的推行需要前后端进行紧密的配合,在这个配合过程中,前端的痛点往往是大于服务端的。所以,一般场景下前端对于引入GraphQL的述求要大于服务端。另一方面,如果采用和RESTful API一样的开发模式,那么实现GraphQL的工作量主要是在服务端。服务端同学需要基于GraphQL规范重新设计并实现API,通常都建议单独构建一个数据层来对外暴露GraphQL API。
在实施GraphQL的策略上,我们也可以总结几条最佳实践。首先,如果你已经实现了一部分RESTful 服务,那么可以让 GraphQL与这部分RESTful API并存发展。尤其是对于那些单一的RESTful服务,可以把GraphQL直接连接到已有的RESTful服务上。通过这种策略,RESTful服务中已经实现的业务逻辑层、数据访问层组件都可以得到复用,我们要做的只是开放一个新的GraphQL访问入口而已。而且,作为一项新技术的引入过程,GraphQL和RESTful服务在一段时间内并存发展也符合平滑过渡的客观需求。
如果你正在采用微服务架构,那么引入GraphQL的策略就可以是在所有后端微服务之前架设一层由GraphQL构建的数据层,并对后端服务提供的数据进行自由的组装之后开放给前端。这种实施策略类似于微服务架构中的API网关,一方面对前端请求进行适配和路由,一方面完成前后端之间的解耦。
我们知道,GraphQL是一种理念和规范,它并不直接提供开发工具和框架。而GraphQL Java是基于Java开发的一个GraphQL实现库,在实际的应用开发中,开发人员可以创建在自己的Controller层组件来与GraphQL完成整合。当然,我们也可以是对GraphQL Java进行一定的封装和扩展。因此,我们需要首先掌握GraphQL Java中所包含的核心编程组件。GraphQL Java中内置了一组核心的技术组件。
首先,我们需要引入一个核心组件,即Schema。所谓Schema,简单讲就是一种前后端交互的协议和规范,或者可以把它类比成RESTful API中的接口定义文档。在Schema中,开发人员需要指定两部分内容。一方面,我们需要明确定义前后端交互的数据结构,包括具体的字段名称、类型、是否为空等属性。另一方面,GraphQL规定每一个Schema中可以存在一个根Query和根Mutation,分别用于执行查询和更新操作。如下所示的就是一个典型的Schema定义。
type Query {
employees: [Employee]
}
type Mutation {
updateSalary(input: UpdateSalaryInput!): UpdateSalaryPayload
}
type Employee {
id: ID!
name: String
salary: String
}
input UpdateSalaryInput {
employeeId: ID!
salary: String!
}
type UpdateSalaryPayload {
success: Boolean!
employee: Employee
}
在上述Schema中,我们看到了ID、String、Boolean等基本数据类型,以及用来表明是否为空的!和数组的[]。
第二个要介绍的组件是DataFetcher。从命名上看,DataFetcher组件的作用就是在执行查询时获取字段对应的数据。DataFetcher是一个接口,只定义了一个方法,如下所示。
public interface DataFetcher
T get(DataFetchingEnvironment dataFetchingEnvironment) throws Exception;
}
开发人员可以从DataFetchingEnvironment中获取传入的参数,并根据该参数来执行具体的数据查询操作。至于数据查询操作的具体实现过程,DataFetcher并不关心。
创建DataFetcher只是开始,我们还要将它们应用在GraphQL服务器上,这就需要借助RuntimeWiring组件。通过Runtime Wiring(运行时组装)机制,我们可以把DataFetcher整合在GraphQL的运行环境中。创建RuntimeWiring的典型代码如下所示。
private UsersDataFetcher usersDataFetcher;
private UserDataFetcher userDataFetcher;
private ArticlesDataFetcher articlesDataFetcher;
private RuntimeWiring buildRuntimeWiring() {
return RuntimeWiring.newRuntimeWiring()
.type("Query", typeWiring -> typeWiring
.dataFetcher("users", usersDataFetcher)
.dataFetcher("user", userDataFetcher))
.type("User", typeWiring -> typeWiring
.dataFetcher("articles", articlesDataFetcher))
.build();
}
可以看到,这里通过RuntimeWiring的type方法将各个DataFetcher与对应的数据结构关联起来。
最后,基于Schema和RuntimeWiring,我们就可以创建GraphQL对象,如下所示。
File schemas = schemeResource.getFile();
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(schemas);
RuntimeWiring wiring = buildRuntimeWiring();
GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(typeRegistry, wiring);
GraphQL graphQL = GraphQL.newGraphQL(schema).build();
基于这个GraphQL对象,我们就可以使用它来完成具体的查询操作,最终面向业务层的代码如下所示。
String query = …;
ExecutionResult result = graphQL.execute(query);
可以看到,使用GraphQL Java进行开发的难度并不大,流程也比较固化。
在日常开发过程中,基于HTTP协议的RESTful API是我们目前主流的前后端开发模式,但并不一定是最合理的开发模式。今天我们所介绍的GraphQL具备的功能特性能够在一定程度上解决传统开发模式中所存在的一些问题。GraphQL更加具有灵活性和扩展性,并能显著减少前后端交互所需要的沟通和开发成本。在日常开发过程中,建议你根据自身业务发展的需求和变化,在合适的场景中引入GraphQL相关技术体系。