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就可以改寫成movie(name:符號(hào)開(kāi)頭,冒號(hào)后面緊跟著變量的傳入類型。如果要使用變量,直接引用即可,例如上面的 movie 就可以改寫成 movie(name: 符號(hào)開(kāi)頭,冒號(hào)后面緊跟著變量的傳入類型。如果要使用變量,直接引用即可,例如上面的movie就可以改寫成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ǔ)法樹規(guī)范,規(guī)定語(yǔ)法樹支持以下節(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ǔ)法樹規(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)耍琿uery 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的上下文里,以便在組件樹的任何地方都是可以獲取到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)即所得:所寫請(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)文章
React-Router v6實(shí)現(xiàn)頁(yè)面級(jí)按鈕權(quán)限示例詳解
這篇文章主要介紹了使用 reac+reactRouter來(lái)實(shí)現(xiàn)頁(yè)面級(jí)的按鈕權(quán)限功能,這篇文章分三部分,實(shí)現(xiàn)思路、代碼實(shí)現(xiàn)、踩坑記錄,有需要的朋友可以借鑒參考下,希望能夠有所幫助2023-10-10
React Native基礎(chǔ)入門之初步使用Flexbox布局
React中引入了flexbox概念,flexbox是屬于web前端領(lǐng)域CSS的一種布局方案,下面這篇文章主要給大家介紹了關(guān)于React Native基礎(chǔ)入門之初步使用Flexbox布局的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2018-07-07
react項(xiàng)目升級(jí)報(bào)錯(cuò),babel報(bào)錯(cuò),.babelrc配置兼容等問(wèn)題及解決
這篇文章主要介紹了react項(xiàng)目升級(jí)報(bào)錯(cuò),babel報(bào)錯(cuò),.babelrc配置兼容等問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
React Native項(xiàng)目中使用Lottie動(dòng)畫的方法
這篇文章主要介紹了React Native 實(shí)現(xiàn)Lottie動(dòng)畫的相關(guān)知識(shí),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-10-10
為react組件庫(kù)添加typescript類型提示的方法
這篇文章主要介紹了為react組件庫(kù)添加typescript類型提示,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
React實(shí)現(xiàn)阿里云OSS上傳文件的示例
這篇文章主要介紹了React實(shí)現(xiàn)阿里云OSS上傳文件的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
ReactDOM.render在react源碼中執(zhí)行原理
這篇文章主要為大家介紹了ReactDOM.render在react源碼中執(zhí)行原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

