美文网首页
GraphQL的HelloWorld

GraphQL的HelloWorld

作者: 山天大畜 | 来源:发表于2018-12-09 18:53 被阅读30次

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

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

    相关文章

      网友评论

          本文标题:GraphQL的HelloWorld

          本文链接:https://www.haomeiwen.com/subject/tzekhqtx.html