一種用於 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。後端
在此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的出現必定很是受人喜好,特別是在前端不斷變化的時代,它在將來的前景不可估量。
全部的項目代碼均可以在此下載。