GraphQL在react中的應(yīng)用示例詳解
什么是 GraphQL
GraphQL由Facebook發(fā)起,其手機(jī)客戶端自2012年起,就全面采用了GraphQL查詢語(yǔ)言, 2015年, Facebook全面開(kāi)源了第一份GraphQL規(guī)范。 GraphQL 對(duì)你的 API 中的數(shù)據(jù)提供了一套易于理解的完整描述,是一種規(guī)范,使得客戶端能夠準(zhǔn)確地獲得它需要的數(shù)據(jù),而且沒(méi)有任何冗余.
GraphQL出現(xiàn)的意義
傳統(tǒng)API存在的主要問(wèn)題:
- 接口數(shù)量眾多維護(hù)成本高:接口的數(shù)量通常由業(yè)務(wù)場(chǎng)景的數(shù)量決定,為了盡量減少接口數(shù)量,服務(wù)端工程師通常會(huì)對(duì)業(yè)務(wù)做抽象,首先構(gòu)建粒度較小的數(shù)據(jù)接口,再根據(jù)業(yè)務(wù)場(chǎng)景對(duì)數(shù)據(jù)接口進(jìn)行組合,對(duì)外暴露業(yè)務(wù)接口,即便這樣,服務(wù)端對(duì)前端暴露的接口數(shù)量還是非常多,因?yàn)闃I(yè)務(wù)總是多變的。
- 接口擴(kuò)展成本高:出于帶寬的考慮移動(dòng)端我們要求接口返回盡量少的字段,PC 端通常要展現(xiàn)更多字段;考慮首屏性能,我們又要求對(duì)接口做合并;傳統(tǒng) API 應(yīng)對(duì)這些需求,前后端都面臨改造,成本較高。
- 接口響應(yīng)的數(shù)據(jù)格式無(wú)法預(yù)知:由于接口文檔幾乎總是不能及時(shí)更新,前端工程師無(wú)法預(yù)知接口響應(yīng)的數(shù)據(jù)格式,影響前端開(kāi)發(fā)進(jìn)度。
GraphQL 如何解決問(wèn)題
請(qǐng)求參數(shù)在發(fā)送到服務(wù)端之前會(huì)先經(jīng)過(guò) GraphQL Client 轉(zhuǎn)換成客戶端 Schema,這段 Schema 其實(shí)是一段 query 開(kāi)頭的字符串,描述了客戶端的對(duì)數(shù)據(jù)的述求:調(diào)用哪個(gè)方法,傳遞什么樣的參數(shù),返回哪些字段。服務(wù)端拿到這段 Schema 之后,通過(guò)事先定義好的服務(wù)端 Schema 接收請(qǐng)求參數(shù)并執(zhí)行對(duì)應(yīng)的 resolve 函數(shù)提供數(shù)據(jù)服務(wù)。
GraphQL基本語(yǔ)法
參考 [GraphQL][1] 官網(wǎng)文檔
標(biāo)量類型
GraphQL 自帶一組默認(rèn)標(biāo)量類型: Int:有符號(hào) 32 位整數(shù)。 Float:有符號(hào)雙精度浮點(diǎn)值。 String:UTF‐8 字符序列。 Boolean:true 或者 false。 ID:ID 標(biāo)量類型表示一個(gè)唯一標(biāo)識(shí)符,通常用以重新獲取對(duì)象或者作為緩存中的鍵。ID 類型使用和 String 一樣的方式序列化。
對(duì)象類型
一個(gè) GraphQL schema 中的最基本的組件是對(duì)象類型,它就表示你可以從服務(wù)上獲取到什么類型的對(duì)象,以及這個(gè)對(duì)象有什么字段
type Character { name: String! list: [Episode!]! }
myField: [String!] myField: null myField: [] myField: ['a', 'b'] myField: ['a', null, 'b'] myField: [String]! myField: null myField: [] myField: ['a', 'b'] myField: ['a', null, 'b']
GraphQL 對(duì)象類型上的每一個(gè)字段都可能有零個(gè)或者多個(gè)參數(shù),
type Starship { id: ID! name: String! length(unit: LengthUnit = METER): Float }
枚舉類型
enum Episode { NEWHOPE EMPIRE JEDI }
這表示無(wú)論我們?cè)?schema 的哪處使用了 Episode,都可以肯定它返回的是 NEWHOPE、EMPIRE 和 JEDI 之一。
對(duì)象類型、標(biāo)量以及枚舉是 GraphQL 中你唯一可以定義的類型種類。但是當(dāng)你在 schema 的其他部分使用這些類型時(shí),或者在你的查詢變量聲明處使用時(shí),你可以給它們應(yīng)用額外的類型修飾符來(lái)影響這些值的驗(yàn)證。
type Character { name: String! list: [Episode]! }
GraphQL 內(nèi)置指令
GraphQL 中內(nèi)置了兩款邏輯指令,指令跟在字段名后使用。
@include 當(dāng)條件成立時(shí),查詢此字段
query { search { actors @include(if: $queryActor) { name } } }
@skip 當(dāng)條件成立時(shí),不查詢此字段
query { search { comments @skip(if: $noComments) { from } } }
- 操作類型:指定本請(qǐng)求體要對(duì)數(shù)據(jù)做什么操作,類似與 REST 中的 GET POST。GraphQL 中基本操作類型有 query 表示查詢,mutation 表示對(duì)數(shù)據(jù)進(jìn)行操作,例如增刪改操作,subscription 訂閱操作。
- 操作名稱:操作名稱是個(gè)可選的參數(shù),操作名稱對(duì)整個(gè)請(qǐng)求并不產(chǎn)生影響,只是賦予請(qǐng)求體一個(gè)名字,可以作為調(diào)試的依據(jù)。
- 變量定義:在 GraphQL 中,聲明一個(gè)變量使用符號(hào)開(kāi)頭,冒號(hào)后面緊跟著變量的傳入類型。如果要使用變量,直接引用即可,例如上面的movie就可以改寫(xiě)成movie(name:符號(hào)開(kāi)頭,冒號(hào)后面緊跟著變量的傳入類型。如果要使用變量,直接引用即可,例如上面的 movie 就可以改寫(xiě)成 movie(name: 符號(hào)開(kāi)頭,冒號(hào)后面緊跟著變量的傳入類型。如果要使用變量,直接引用即可,例如上面的movie就可以改寫(xiě)成movie(name:name)。
query Hero($episode: Int!, $withFriends: Boolean!) { hero(episode: $episode) { name friends @include(if: $withFriends) { name } } }
什么是 Apollo
Meteor 團(tuán)隊(duì)有著很豐富的數(shù)據(jù)流控制經(jīng)驗(yàn),他們發(fā)現(xiàn)了 Relay 的不便之處,引領(lǐng)業(yè)界通過(guò)使用他們開(kāi)發(fā)的 Apollo 享受到更簡(jiǎn)潔的接口,Apollo 是基于GraphQL的全棧解決方案集合。包括了 apollo-client 和 apollo-server ;從后端到前端提供了對(duì)應(yīng)的 lib ,使開(kāi)發(fā)使用 GraphQL 更加的方便。
apollo-server
apollo-server是一個(gè)在nodejs上構(gòu)建grqphql服務(wù)端的web中間件。支持express,koa 等框架。 參考 [apollo-server][2] 官網(wǎng)文檔
處理流程
主要是通過(guò)官方graphql-js庫(kù)進(jìn)行處理
1.解析階段
為了識(shí)別客戶端 Schema, graphql-js 定義了一系列的特征標(biāo)識(shí)符:
export const TokenKind = Object.freeze({ BANG: '!', DOLLAR: '$', PAREN_L: '(', PAREN_R: ')', SPREAD: '...', COLON: ':', EQUALS: '=', BRACKET_L: '[', BRACKET_R: ']', ... });
并定義了 AST 語(yǔ)法樹(shù)規(guī)范,規(guī)定語(yǔ)法樹(shù)支持以下節(jié)點(diǎn):
export const Kind = Object.freeze({ // Name NAME: 'Name', // Document DOCUMENT: 'Document', OPERATION_DEFINITION: 'OperationDefinition', VARIABLE_DEFINITION: 'VariableDefinition', VARIABLE: 'Variable', // Values INT: 'IntValue', FLOAT: 'FloatValue', STRING: 'StringValue', BOOLEAN: 'BooleanValue', ... });
有了特征字符串與 AST 語(yǔ)法樹(shù)規(guī)范,GraphQL Server 對(duì)客戶端 Schema 進(jìn)行逐字符掃描,如果客戶端 Schema 不符合服務(wù)端定義的 AST 規(guī)范,解析過(guò)程會(huì)直接拋出語(yǔ)法異常。
2.校驗(yàn)階段
校驗(yàn)階段用于驗(yàn)證客戶端 Schema 是否按照服務(wù)端 Schema 定義好的方式獲取數(shù)據(jù),比如:獲取數(shù)據(jù)的方法名是否有誤,必填項(xiàng)是否有值等等,校驗(yàn)范圍一共有幾十種,不一一舉例。
{ "errors":[ { "message":"Cannot query field "getU" on type "Query". Did you mean "getUser"?", "locations":[ { "line":3, "column":9 } ] } ] }
不僅返回結(jié)構(gòu)化的報(bào)錯(cuò)信息,還非常人性化的告訴你正確的調(diào)用方式是什么。校驗(yàn)階段通過(guò)之后會(huì)進(jìn)入執(zhí)行階段.
3.執(zhí)行階段
執(zhí)行階段依賴的輸入為:解析階段的產(chǎn)出物 document ,服務(wù)端 Schema;其中 document 準(zhǔn)確描述了客戶端對(duì)數(shù)據(jù)的述求:請(qǐng)求哪個(gè)方法,參數(shù)是什么,需要哪些字段;服務(wù)端 Schema 描述了提供數(shù)據(jù)的方式。執(zhí)行服務(wù)端 Schema 中的 resolve 函數(shù),得到執(zhí)行階段的輸出。每個(gè)類型的每個(gè)字段都由一個(gè) resolver 函數(shù)支持,該函數(shù)由 GraphQL 服務(wù)器開(kāi)發(fā)人員提供。
Schema
Schema可以說(shuō)是GraphQL最具核心的部分,其描述了整個(gè)接口向外暴露的形式;像Restful API,我們會(huì)定義一個(gè)查詢所有人的接口url定義為:/api/v1/user/getUsers,而查詢?nèi)司唧w信息的接口url為:/api/v1/user/getUserById,前端人員調(diào)用起來(lái)很直觀。但是graphql是完全不一樣的使用方式,其向前端暴露的url就一個(gè)像/api/graphql之類的,那這么多接口怎么區(qū)分呢?
一個(gè)graphql接口都有一個(gè)Schema定義,其定義三種操作方式:query(查詢),mutation(變更)和subscription(監(jiān)聽(tīng))。再往下延伸,一個(gè)查詢中包含多個(gè)field,也就是多種不同的查詢,比如query user查詢?nèi)?,query message查詢消息,query weather查詢天氣,通過(guò)這些就實(shí)現(xiàn)了Restful API使用多個(gè)url來(lái)達(dá)到不同操作的效果。
給server端帶來(lái)的便利性
由于 GraphQL 通過(guò)客戶端 Schema 而不是通過(guò) URL 描述數(shù)據(jù)述求,所以理論上服務(wù)端只需要對(duì)客戶端暴露一個(gè)地址即可, 解決了接口數(shù)量眾多維護(hù)成本高的問(wèn)題; 同時(shí),服務(wù)端提供的是全量字段,客戶端可按需獲取,面對(duì)接口擴(kuò)展的需求,服務(wù)端沒(méi)有開(kāi)發(fā)成本;
import express from 'express'; import { graphiqlExpress, graphqlExpress } from 'apollo-server-express'; const app = express(); app.use('/graphql', graphqlExpress({ schema, })); app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }));
apollo-client
參考 [apollo-client][3] 官網(wǎng)文檔
創(chuàng)建client
import ApolloClient from "apollo-boost"; const client = new ApolloClient({ uri: "https://48p1r2roz4.sse.codesandbox.io" });
在我們將Apollo Client連接到React之前,讓我們先嘗試發(fā)送查詢。記住首先導(dǎo)入gql用于將查詢字符串解析為查詢文檔。
import gql from "graphql-tag"; ... client.query({ query: gql` { rates(currency: "USD") { currency } } ` }) .then(result => console.log(result));
將client注入到react
react-apollo提供ApolloProvider組件,ApolloProvider類似于redux的provider。它會(huì)把a(bǔ)pollo客戶端放入到React app的上下文里,以便在組件樹(shù)的任何地方都是可以獲取到apollo客戶端。
import React from "react"; import { render } from "react-dom"; import { ApolloProvider } from "react-apollo"; const App = () => ( <ApolloProvider client={client}> <div> <h3>My first Apollo app ??</h3> </div> </ApolloProvider> ); render(<App />, document.getElementById("root"));
數(shù)據(jù)請(qǐng)求
1.通過(guò)react-apollo提供的graphql函數(shù)獲取數(shù)據(jù),并connect到組建中,就可以在組件的this.props中看到多了個(gè)data對(duì)象,
import React, { Component } from 'react'; import { graphql } from 'react-apollo'; import gql from 'graphql-tag'; export const USERS_QUERY = gql` query UserQuery($pageNum: Int,$pageSize:Int){ users(pageNum:$pageNum,pageSize:$pageSize ) { pageNum pageSize total data { userName } } } `; const withQuery = graphql(USERS_QUERY, { options: () => ({ variables: { pageNum: 3, pageSize: 8, }, }), }); class List extends Component { constructor(props) { super(props); this.state = {}; } render() { const { data: { loading, error, users } } = this.props; if (loading) { return <div className="loading">Loading...</div>; } if (error) return `Error! ${error.message}`; const { total } = users; }; return ( <div> <p className="total">總共<span>{total}</span>人</p> </div> ); } } export default withQuery(List);
data中包含loading, error, users等字段
當(dāng)React安裝Query組件時(shí),Apollo Client會(huì)自動(dòng)觸發(fā)查詢。如果想延遲觸發(fā)查詢,直到用戶執(zhí)行操作(例如單擊按鈕),該怎么辦?對(duì)于這種情況,可以使用ApolloConsumer組件并直接調(diào)用client.query()。
import React, { Component } from 'react'; import { ApolloConsumer } from 'react-apollo'; class DelayedQuery extends Component { state = { dog: null }; onDogFetched = dog => this.setState(() => ({ dog })); render() { return ( <ApolloConsumer> {client => ( <div> {this.state.dog && <img src={this.state.dog.displayImage} />} <button onClick={async () => { const { data } = await client.query({ query: GET_DOG_PHOTO, variables: { breed: "bulldog" } }); this.onDogFetched(data.dog); }} > Click me! </button> </div> )} </ApolloConsumer> ); } }
2.通過(guò)apollo提供的組件獲取
import gql from "graphql-tag"; import { Query } from "react-apollo"; const GET_DOG_PHOTO = gql` query Dog($breed: String!) { dog(breed: $breed) { id displayImage } } `; const DogPhoto = ({ breed }) => ( <Query query={GET_DOG_PHOTO} variables={{ breed }} pollInterval={500}> {({ loading, error, data, startPolling, stopPolling }) => { if (loading) return null; if (error) return `Error!: ${error}`; return ( <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} /> ); }} </Query> );
通過(guò)設(shè)置pollInterval為500,每隔0.5秒看到一個(gè)新的小狗圖像。 當(dāng)我們從Query組件中獲取數(shù)據(jù)時(shí),看看Apollo Client幕后發(fā)生的事情。
1.當(dāng)Query組件安裝時(shí),Apollo Client會(huì)為我們的查詢創(chuàng)建一個(gè)observable。我們的組件通過(guò)Apollo Client緩存訂閱查詢結(jié)果。
2.首先,我們嘗試從Apollo緩存加載查詢結(jié)果。如果它不在那里,我們將請(qǐng)求發(fā)送到服務(wù)器。
3.數(shù)據(jù)恢復(fù)后,我們將其標(biāo)準(zhǔn)化并將其存儲(chǔ)在Apollo緩存中。由于Query組件訂閱了結(jié)果,因此它會(huì)自動(dòng)更新數(shù)據(jù)。
數(shù)據(jù)緩存
創(chuàng)建本地緩存
import { ApolloClient } from 'apollo-client' import { withClientState } from 'apollo-link-state' import { HttpLink } from 'apollo-link-http' import { InMemoryCache } from 'apollo-cache-inmemory' import { resolvers, typeDefs, defaults } from '../client/index' const cache = new InMemoryCache() const client = new ApolloClient({ cache, // 本地?cái)?shù)據(jù)存儲(chǔ) link: withClientState({ resolvers, defaults, cache, typeDefs }).concat( new HttpLink({ uri: 'http://localhost:4001/graphql', opts: { credentials: 'cross-origin', }, }) ), })
要直接與緩存交互,可以使用Apollo Client方法readQuery,readFragment,writeQuery和writeFragment。
總結(jié)
如果使用 GraphQL,那么后端將不再產(chǎn)出 API,而是將 Controller 層維護(hù)為 Resolver,和前端約定一套 Schema,這個(gè) Schema 將用來(lái)生成接口文檔,前端直接通過(guò) Schema 或生成的接口文檔來(lái)進(jìn)行自己期望的請(qǐng)求。
GraphQL 的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
所見(jiàn)即所得:所寫(xiě)請(qǐng)求體即為最終數(shù)據(jù)結(jié)構(gòu) 減少網(wǎng)絡(luò)請(qǐng)求:復(fù)雜數(shù)據(jù)的獲取也可以一次請(qǐng)求完成 Schema 即文檔:定義的 Schema 也規(guī)定了請(qǐng)求的規(guī)則 類型檢查:嚴(yán)格的類型檢查能夠消除一定的認(rèn)為失誤
缺點(diǎn)
增加了服務(wù)端實(shí)現(xiàn)的復(fù)雜度:一些業(yè)務(wù)可能無(wú)法遷移使用 GraphQL,雖然可以使用中間件的方式將原業(yè)務(wù)的請(qǐng)求進(jìn)行代理,這無(wú)疑也將增加復(fù)雜度和資源的消耗
以上就是GraphQL在react中的應(yīng)用示例詳解的詳細(xì)內(nèi)容,更多關(guān)于GraphQL react應(yīng)用的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
VSCode配置react開(kāi)發(fā)環(huán)境的步驟
本篇文章主要介紹了VSCode配置react開(kāi)發(fā)環(huán)境的步驟,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12react實(shí)現(xiàn)簡(jiǎn)單的拖拽功能
這篇文章主要為大家詳細(xì)介紹了react實(shí)現(xiàn)簡(jiǎn)單的拖拽功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03使用 Rails API 構(gòu)建一個(gè) React 應(yīng)用程序的詳細(xì)步驟
這篇文章主要介紹了使用 Rails API 構(gòu)建一個(gè) React 應(yīng)用程序的詳細(xì)步驟,主要包括后端:Rails API部分,前端:React部分及React組件的相關(guān)操作,具有內(nèi)容詳情跟隨小編一起看看吧2021-08-08react-router-dom 嵌套路由的實(shí)現(xiàn)
這篇文章主要介紹了react-router-dom 嵌套路由的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05React報(bào)錯(cuò)Type '() => JSX.Element[]&apos
這篇文章主要為大家介紹了React報(bào)錯(cuò)Type '() => JSX.Element[]' is not assignable to type FunctionComponent解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12React+CSS 實(shí)現(xiàn)繪制橫向柱狀圖
這篇文章主要介紹了React+CSS 實(shí)現(xiàn)繪制橫向柱狀圖,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09React中獲取數(shù)據(jù)的3種方法及優(yōu)缺點(diǎn)
這篇文章主要介紹了React中獲取數(shù)據(jù)的3種方法及優(yōu)缺點(diǎn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02