GraphQL的HelloWorld

GraphQL

一种用于 API 的查询语言。前端

GraphQL是一种新的API标准,它提供了一种更高效、强大和灵活的数据提供方式。它是由Facebook开发和开源,目的是为了解决因前端交互不断变化,与后端接口须要同步修改的痛点。java

通常开发中,后端服务为前端提供接口会有两种考虑方式:git

  • 一种是根据前端页面的展现来设计接口,一个接口尽可能知足一个页面所须要的全部数据
  • 一种是从数据实体的维度设计,一个接口只提供一个实体相关的信息

对于第一种状况,前端的体验是比较好的,一个页面只须要等待请求一次接口的时间,但当页面发生变化的时候,后端接口的维护成本是比较高的,并且随之带来的新老接口的兼容也是不能忽视的问题。 对于第二种状况,后端的接口是相对固定的,可是前端每每就须要一个页面请求不少个接口,才能知足页面展现的须要,用户须要为此等待较长的时间,用户体验不高。github

为了解决上面的问题,GraphQL是一种很是好的解决方案。GraphQL由后端按照定义好的标准Schema的方式提供接口,就能够不用再改变。而前端根据本身页面的须要,自行构造json查询相应数据,服务端也只会为前端返回json里所描述的信息。当前端页面发生变化的时候,前端只须要修改本身的查询json便可,后端能够彻底无感。这就是GraphQL所带来的好处,双方只依赖标准的Schema进行开发,再也不依赖于彼此。sql

服务端的例子

所有代码均可以在此下载数据库

能够先按照官方的开发文档进行学习,里面提到的代码片断并不彻底,Github上面有完整的代码,能够做为补充。编程

首先是开发服务端,我参照了官方文档中的例子。第一步须要先定义好咱们的全部实体类,放入schema中,我项目中文件名为myschema.graphqls,放在java的resource目录下。json

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表明着写入类型。 咱们须要把咱们所用到的全部实体类都定义在此处(枚举和接口也是支持的),这个文件就是未来要交给前端去理解的内容,是咱们全部接口的生成依据。swift

定义好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的出现必定很是受人喜好,特别是在前端不断变化的时代,它在将来的前景不可估量。

全部的项目代码均可以在此下载

相关文章
相关标签/搜索