GraphQL的HelloWorld
GraphQL
一种用于 API 的查询语言。
GraphQL是一种新的API标准,它提供了一种更高效、强大和灵活的数据提供方式。它是由Facebook开发和开源,目的是为了解决因前端交互不断变化,与后端接口需要同步修改的痛点。
一般开发中,后端服务为前端提供接口会有两种考虑方式:
- 一种是根据前端页面的展示来设计接口,一个接口尽量满足一个页面所需要的所有数据
- 一种是从数据实体的维度设计,一个接口只提供一个实体相关的信息
对于第一种情况,前端的体验是比较好的,一个页面只需要等待请求一次接口的时间,但当页面发生变化的时候,后端接口的维护成本是比较高的,而且随之带来的新老接口的兼容也是不能忽视的问题。
对于第二种情况,后端的接口是相对固定的,但是前端往往就需要一个页面请求很多个接口,才能满足页面展示的需要,用户需要为此等待较长的时间,用户体验不高。
为了解决上面的问题,GraphQL是一种非常好的解决方案。GraphQL由后端按照定义好的标准Schema的方式提供接口,就可以不用再改变。而前端根据自己页面的需要,自行构造json查询相应数据,服务端也只会为前端返回json里所描述的信息。当前端页面发生变化的时候,前端只需要修改自己的查询json即可,后端可以完全无感。这就是GraphQL所带来的好处,双方只依赖标准的Schema进行开发,不再依赖于彼此。
服务端的例子
全部代码都可以在此下载。
可以先按照官方的开发文档进行学习,里面提到的代码片段并不完全,Github上面有完整的代码,可以作为补充。
首先是开发服务端,我参照了官方文档中的例子。第一步需要先定义好我们的所有实体类,放入schema中,我项目中文件名为myschema.graphqls
,放在java的resource目录下。
schema {
query: QueryType
mutation: MutationType
}
type QueryType {
hero(episode: Episode): Character
human(id : String) : Human
droid(id: ID!): Droid
}
type MutationType {
wirte(text: String!): String!
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
homePlanet: String
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
schema中QueryType代表了查询类型,MutationType代表着写入类型。
我们需要把我们所用到的所有实体类都定义在此处(枚举和接口也是支持的),这个文件就是将来要交给前端去理解的内容,是我们所有接口的生成依据。
定义好schema之后,第二步就是编写DataFetcher和Resolver。
- DataFetcher我理解是获取数据的方法,Demo中我只是简单用了几个静态写死的数据作为提供,在实际项目中,我们可以通过Repository层,从数据库拿到数据并提供。
- Resolver我理解为解析数据查询格式的方法,比如schema中如果定义了接口,那么在前端查询的时候如果有数据类型为接口,则需要此方法来提供信息,找到具体的实现类。
在此Demo中,因为Character是一个接口,所以需要提供一个Character的Resolver:
val characterTypeResolver: TypeResolver = TypeResolver { env ->
val id = env.getObject<Map<String, Any>>()["id"]
when {
// humanData[id] != null -> StarWarsSchema.humanType
// droidData[id] != null -> StarWarsSchema.droidType
humanData[id] != null -> env.schema.getType("Human") as GraphQLObjectType
droidData[id] != null -> env.schema.getType("Droid") as GraphQLObjectType
else -> null
}
}
这里的逻辑比较简单粗暴,是判断humanData里是否能找到这个id,如果找到,就认为是humanData,否则去droidData中找。实际项目中我们的逻辑应该要更严谨一些。
因为我们第一步定义了schema,所以没有歧义的类型都可以从schema中进行推断,只有像接口这种不能推断的类型才需要Resolver。如果我们没有schema文件,那么就需要为每个实体类都编写Resolver,项目中StarWarsSchema这个文件就是定义了所有的类型以及解析方式。具体项目中,这两种方式可以二选其一,我个人推荐是用myschema.graphqls
这样的方式去定义,毕竟语义清晰,便于维护。
接下来就是如何提供接口了。
读取graphql的schema文件:
@Throws(IOException::class)
private fun readSchemaFileContent(): String {
val classPathResource = ClassPathResource("myschema.graphqls")
classPathResource.inputStream.use { inputStream -> return CharStreams.toString(InputStreamReader(inputStream, Charsets.UTF_8)) }
}
提供Fetcher和Resolver:
private fun buildRuntimeWiring(): RuntimeWiring {
return RuntimeWiring.newRuntimeWiring()
// this uses builder function lambda syntax
.type("QueryType") { typeWiring ->
typeWiring
.dataFetcher("hero", StaticDataFetcher (StarWarsData.artoo))
.dataFetcher("human", StarWarsData.humanDataFetcher)
.dataFetcher("droid", StarWarsData.droidDataFetcher)
.dataFetcher("field", StarWarsData.fieldFetcher)
}
.type("Human") { typeWiring ->
typeWiring
.dataFetcher("friends", StarWarsData.friendsDataFetcher)
}
// you can use builder syntax if you don't like the lambda syntax
.type("Droid") { typeWiring ->
typeWiring
.dataFetcher("friends", StarWarsData.friendsDataFetcher)
}
// or full builder syntax if that takes your fancy
.type(
newTypeWiring("Character")
.typeResolver(StarWarsData.characterTypeResolver)
.build()
)
.type(
newTypeWiring("Episode")
.enumValues(StarWarsData.episodeResolver)
.build()
)
.build()
}
生成GraphQLSchema:
@Throws(IOException::class)
fun graphQLSchema(): GraphQLSchema {
val schemaParser = SchemaParser()
val schemaGenerator = SchemaGenerator()
val schemaFileContent = readSchemaFileContent()
val typeRegistry = schemaParser.parse(schemaFileContent)
val wiring = buildRuntimeWiring()
return schemaGenerator.makeExecutableSchema(typeRegistry, wiring)
}
提供查询接口:
@RequestMapping("/api")
@ResponseBody
fun api(@RequestBody body: String): String {
val turnsType = object : TypeToken<Map<String, Any>>() {}.type
var map: Map<String, Any> = Gson().fromJson(body, turnsType)
var query = map["query"]?.toString()
var params = map["variables"] as? Map<String, Any>
var build: GraphQL? = null
try {
build = GraphQL.newGraphQL(graphQLSchema()).build()
} catch (e: IOException) {
e.printStackTrace()
}
var input = ExecutionInput.newExecutionInput().query(query)
if (params != null) {
input = input.variables(params!!)
}
val executionResult = build!!.execute(input.build())
// Prints: {hello=world}
var result = mutableMapOf<String, Any>()
result["data"] = executionResult.getData<Any>()
return Gson().toJson(result)
}
完成以上几步,前端就可以通过/api接口来请求数据了。其中query是放我们的查询json,variables是放json里面需要用到的一些参数。
我们可以看到,graphql的类帮我们做了很多事,我们只需要写好schema,提供好数据的解析方式和查询结果即可。前端的任何方式组合查询,graphql都会分别调用我们写好的fetcher,自动组装数据并返回。
为了测试我们的接口,可以通过浏览器访问一些测试的json来检验,Github上面的单元测试代码可以方便的拿到我们想要的json进行测试。
前端的例子
我仅用iOS写了一个Demo,Android用法应该类似,就不再赘述。
第一步是先安装Apollo的Pod。
pod 'Apollo', '~> 0.9.4'
然后是生成schema.json,这个schema.json就是根据之前服务端定义的schema和各种Resolver的信息,自动生成的一个json文件,专门给前端使用。首先服务端还需要新写以下接口:
@RequestMapping("/graphql")
@ResponseBody
fun graphql(): String {
var ghql = IntrospectionQuery.INTROSPECTION_QUERY
var build: GraphQL? = null
try {
build = GraphQL.newGraphQL(graphQLSchema()).build()
} catch (e: IOException) {
e.printStackTrace()
}
val executionResult = build!!.execute(ghql)
// Prints: {hello=world}
return Gson().toJson(executionResult.getData<Any>())
}
然后浏览器请求该接口,可以得到一个json,该json就是schema.json的所有内容。需要注意的是,json中有一句:
defaultValue":"\"No longer supported\""
里面的两个转义的引号一定不能去掉。
然后在项目的Build Phases中加入以下自动执行的脚本:
APOLLO_FRAMEWORK_PATH="$(eval find $FRAMEWORK_SEARCH_PATHS -name "Apollo.framework" -maxdepth 1)"
if [ -z "$APOLLO_FRAMEWORK_PATH" ]; then
echo "error: Couldn't find Apollo.framework in FRAMEWORK_SEARCH_PATHS; make sure to add the framework to your project."
exit 1
fi
cd "${SRCROOT}/${TARGET_NAME}"
$APOLLO_FRAMEWORK_PATH/check-and-run-apollo-cli.sh codegen:generate --passthroughCustomScalars --queries="$(find . -name '*.graphql')" --schema=schema.json API.swift
随后我们可以把想查询的json也放入文件中,例如simpleQuery.graphql:
query HeroNameQuery {
hero {
name
}
}
切记要把它和schema.json放在同一个目录下。
之后只需要编译,我们便能在这个目录下看到新生成一个API.swift文件,把它引入工程。这个文件包含了graphql为我们生成的所有查询所要用到的类。
在想要查询的地方只需要这么使用即可:
let query1 = HeroNameQueryQuery()
apollo.fetch(query: query1) { result, error in
let hero = result?.data?.hero
print(hero?.name ?? "")
}
还有什么
GraphQL带来的好处是服务端与客户端的接口解耦,当然也有一些局限,例如对性能的影响。如果全是内存级的数据查询还好,否则如果是SQL数据库,并且结构与结构之间有关联,就比较吃性能了。例如产品和订单,订单关联一个产品,如果是普通接口,一个sql的join就可以查出产品和订单两个实体的所有信息。但用GraphQL,就会有两个查询sql需要执行,一个是根据id查产品,一个是根据id查订单,再把二者的数据组合返回给前端。
当然,如果这样类似的数据做一级缓存,也是可以解决的,但是毕竟给服务端还是带来了不少的麻烦,在写数据查询接口的时候,就并不能只考虑某一个实体了,而是要思考这个实体和其他实体之间可能的联系,是否要做缓存,是否会有和其他实体同时被查询的可能性。
另外,要服务端人员把接口全都转变成GraphQL的方式也是一个很大的挑战,不仅是对编程的思维上,对整个服务端架构都是会有很大的影响的,需要慎重评估。
但毋庸置疑的是,GraphQL的出现一定非常受人喜爱,特别是在前端不断变化的时代,它在未来的前景不可估量。
所有的项目代码都可以在此下载。