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的出現必定很是受人喜好,特別是在前端不斷變化的時代,它在將來的前景不可估量。

全部的項目代碼均可以在此下載

相關文章
相關標籤/搜索