GraphQL的HelloWorld

517 阅读8分钟

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的出现一定非常受人喜爱,特别是在前端不断变化的时代,它在未来的前景不可估量。

所有的项目代码都可以在此下载