Tec。士多開發日記系列:第三篇:實作Tec記士多Frontend(GraphQL Client入門)- Part 2
2022-02-22
上集已經詳細講解如何寫Query/Mutation Statement及如何是Apollo GraphQL提供的Playground做測試。下一步就是請解如何是Client Side Application(以React Application為例)中使用Query/Mutation Statement。我會分開兩個版本示範,實作GraphQL Client (基本版)及GraphQL Client (應用版 - Apollo Client)。
實作GraphQL Client (基本版)
大家請是VS Code下create左React App先。由於本節目的只是示範Client及Server之間用GraphQL溝通的基本原理,所以當中的Code會被簡化及無定義Data Type。以下Code只用作示範,敬請留意!
左邊圖片是GraphQL React Client(基本版)的Project Files的樹狀結構。 由於只是示範性質,一切從簡,所以我只會加/更新以下files。 * 更新App.tsx - 顯示由GraphQL Server取得的資料 * 加入query.ts - 儲存query statement,例如,CommoditiesByCategory * 加入handler.ts - 負責傳送query/mutation statement給GraphQL Server並根據statement要求取得相關資料。 |
---|
首先,請準備要給GraphQL Server的query/mutation statement,所以請新增一個檔案query.ts是src folder下面。並export一個常數(const)。呢個常數是儲存左query/mutation statement備用。
//query.ts - 儲存所有query statements。今次用CommoditiesByCategory field做例子。 export const query = ` query($categoryName:CATEGORY) { CommoditiesByCategory(category: $categoryName) { name category unit stockQuantity } } `;
重點嚟la ! 重點嚟la ! 重點嚟la ! 重要的事講3次。 如何同GraphQL Server溝通呢?就是handler.ts處理溝通部分。
export type Query = { variables: any query: string } export const loadDataHandler = async (query:Query) => { const result = await fetch(process.env.REACT_APP_BACKEND_SERVER_URL as string, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(query) }); const {data} = await result.json(); return data; }
是Tec ‧ 士多開發日記系列:第一篇:GraphQL簡介曾經提過GraphQL溝通方法都是以REST為基礎。GraphQL是用POST方法加入固定endpoint名,例如,POST /graphql。/graphql就是endpoint。最後,query/mutation statement及variables是會被放入Request Body當中。
要同GraphQL Server溝通,Client必須根據以下要求砌一個JSON物件,並放入Request Body。
{ variables: { //query statement當中需要的參數(Arguments) } query:`query{....}` //query statement } //OR { variables: { //mutation statement當中需要的參數(Arguments) } query:`mutation{....}` //mutation statement }
例子如下:
{ variables:{ {categoryName:"DRINK"} } query: `query($categoryName:CATEGORY) { CommoditiesByCategory(category: $categoryName) { name category unit stockQuantity } }` }
{ variables:{ commodityInput:{ name:"Item#4", unit:"CAN", category:"DRINK", stockQuantity:1 } } query: ` mutation($commodityInput:CommodityInput!){ addCommodity(newCommodity:$commodityInput){ ...CommodityInfo } } fragment CommodityInfo on Commodity{ name category unit stockQuantity }` }
為方便示範及簡化Coding,用Query Type代表它。免得大家要是VS Code中scrolling,跳嚟跳去太亂跟唔到。 GraphQL Server會用以下格式返回的資料:
{ "data": { //query/mutation statement要求的物件 } }
所以, 是handler.ts最後直接抽出data的結果出嚟, 再俾返UI Component結果顯示。
//handler.ts const {data} = await result.json();
是App.tsx當中問經handler.ts問GraphQL Server取得資料,並顯示結果。相信大家有一程度programming經驗,不作解釋。
//App.tsx import './App.css'; import {useEffect,useState} from 'react'; import {query} from './query'; import {loadDataHandler} from './handler'; function App() { const [data,setData] = useState<any>([]); useEffect(()=>{ const variables = {categoryName:"DRINK"}; //俾Variable const graphQLQuery = {variables,query}; //俾query statement (async()=>{ //結果是{CommoditiesByCategory{....} const {CommoditiesByCategory} = await loadDataHandler(graphQLQuery); setData(CommoditiesByCategory); })(); },[]) return ( <div> {data.map((commodity:any)=>( <> <div>Commodity Name: {commodity.name}</div> <div>Category: {commodity.category}</div> <div>State in Store: {commodity.stockQuantity} {commodity.unit}</div> </> ))} </div> ); } export default App;
GraphQL Client(基本版) Source Code
實作GraphQL Client (應用版 - Apollo Client)
使用Apollo Client的好處:
- 提供一個簡單React Hook輕易完成query/mutation。
- 提供強大Local Cache管理方法,方便同GraphQL Server上資料同步同並快速更新React Component。
首先,大家請是VS Code下create左React App先。
左邊圖片是GraphQL React Client(應用版 - Apollo Client)的Project Files的樹狀結構。 * 更新App.tsx - 載入Commodity List Component * 加入query.ts - 儲存query statement,例如,CommoditiesByCategory * 加入mutation.ts - 儲存mutation statement,例如,addCommodity * 加入CommodityList.ts - 負責傳送query/mutation statement給GraphQL Server並根據statement要求取得相關資料並載入UI。 |
---|
下一步,就是安裝Apollo Client Library及GraphQL Library:
yarn add @apollo/client graphql
步驟 3 : 打開App.tsx, 由 @apollo/client 載入需要的工具:
import {ApolloClient,InMemoryCache,ApolloProvider} from '@apollo/client';
ApolloClient, 負責產生Apollo Client 物件。
InMemoryCache,由GraphQL Server取得的資料是Client存放的位置(快取,Cache)。InMemoryCache,存放是記憶體(memory)。
ApolloProvider,是
步驟 3 :建立Apollo Client 物件
const client = new ApolloClient({ uri:process.env.REACT_APP_BACKEND_SERVER_URL,//你的GraphQL Server URL cache: new InMemoryCache() });
步驟 4 : 傳送Apollo Client 物件給ApolloProvider。並放入Component中。
<ApolloProvider client={client}> <div></div> </ApolloProvider>
完整Source Code:
import React from 'react'; import {ApolloClient,InMemoryCache,ApolloProvider} from '@apollo/client'; const client = new ApolloClient({ uri:process.env.REACT_APP_BACKEND_SERVER_URL, cache: new InMemoryCache(), connectToDevTools:true }); function App() { return ( <ApolloProvider client={client}> <div>To be implemented</div> </ApolloProvider> ); } export default App;
步驟 5 : 建立query statement,並以export module形式交俾其他module使用。
//query.ts,儲存所有query statements。 import {gql} from '@apollo/client'; export const COMMODITIES_BY_CATEGORY_QUERY=gql` query($categoryName:CATEGORY) { CommoditiesByCategory(category: $categoryName) { name category unit stockQuantity } }`;
步驟 6 : 建立mutation statement,並以export module形式交俾其他module使用。
//mutation.ts,儲存所有mutation statements。 import {gql} from '@apollo/client'; export const ADD_COMMODITY = gql` mutation($commodityInput:CommodityInput!){ addCommodity(newCommodity:$commodityInput){ ...CommodityInfo } } fragment CommodityInfo on Commodity{ name category unit stockQuantity }`;
步驟 7 : 建立一個Component, CommodityList.tsx,負責問GraphQL Server提取資料及顯示資料。
import React from 'react'; export function CommodityList(props:{categoryName:string}){ return <div></div> }
步驟 8 : 由GraphQL Server取得售賣貨品清單,例如,飲品清單。
import React from 'react'; import {COMMODITIES_BY_CATEGORY_QUERY} from './query'; import {useQuery} from '@apollo/client'; export function CommodityList(props:{categoryName:string}){ const {data,loading,error} = useQuery(COMMODITIES_BY_CATEGORY_QUERY,{variables:props}); if(loading) return <p>Loading...</p>; if(error) return <p>Error!</p>; return <div></div> }
useQuery,是一個React Hook,專門負責處理GraphQL query statement。當建立useQuery object的時候,useQuery object**即時向GraphQL Server取得query statement要求的資料。有另一個React Hook不會即時**向GraphQL Server取得query statement要求的資料。為免混亂, 我會開另一個side track section解釋。
const {data,loading,error} = useQuery(COMMODITIES_BY_CATEGORY_QUERY,{variables:props});
data,返回query statement要求的資料。
loading,是顯示目前是否仍然是載入狀態。
error,如果載入過程有錯誤,返回一個錯誤資訊。
useQuery(<query statement>,<variables>) //useQuery需要兩個參數
步驟 9 :加入新增貨品,並通知GraphQL Server。
import React from 'react'; import {COMMODITIES_BY_CATEGORY_QUERY} from './query'; import {useQuery} from '@apollo/client'; //新增部分 - 示範Only import {ADD_COMMODITY} from './mutation'; import {useMutation} from '@apollo/client'; export function CommodityList(props:{categoryName:string}){ const {data,loading,error} = useQuery(COMMODITIES_BY_CATEGORY_QUERY,{variables:props}); if(loading) return <p>Loading...</p>; if(error) return <p>Error!</p>; //新增部分 const [newCommunityMutation] = useMutation(ADD_COMMODITY,{ refetchQueries:[{query:COMMODITIES_BY_CATEGORY_QUERY,variables:props}], }); return <div></div> }
先載入mutation statement。
import {ADD_COMMODITY} from './mutation';
建立一個Mutation Object負責執行該mutation statement。⚠️注意: 此Mutation Object**不會即時**執行。
const [newCommunityMutation] = useMutation( ADD_COMMODITY,//mutation statement { /* 如果成功執行mutation,透過refetchQueries更新指定query的cache。是某D情況下唔駛用refetchQueries。 為免又話要跳嚟跳去太亂跟唔到,我會開另一個side track section解釋。 */ refetchQueries:[ /* const {data,loading,error} = useQuery(COMMODITIES_BY_CATEGORY_QUERY,{variables:props}); <-- 記唔記得佢。 */ {query:COMMODITIES_BY_CATEGORY_QUERY,variables:props} ], } );
步驟 10 :建立Button,並執行Mutation Object。
import React from 'react'; import {COMMODITIES_BY_CATEGORY_QUERY} from './query'; import {ADD_COMMODITY} from './mutation'; import {useQuery,useMutation} from '@apollo/client'; export function CommodityList(props:{categoryName:string}){ const {data,loading,error} = useQuery(COMMODITIES_BY_CATEGORY_QUERY,{variables:props}); const [newCommunityMutation] = useMutation(ADD_COMMODITY,{ refetchQueries:[{query:COMMODITIES_BY_CATEGORY_QUERY,variables:props}], }); if(loading) return <p>Loading...</p>; if(error) return <p>Error!</p>; return (<> <button onClick={()=>{ newCommunityMutation({ variables: { commodityInput: { name: "Coffee", unit: "CAN", category: "DRINK", stockQuantity: 1 } } }) }}>Add</button> </>) }
當執行Mutation Object時提供需要的變數。
//newCommunityMutation({ variables:{...} }) newCommunityMutation({ variables: { commodityInput: { name: "Coffee", unit: "CAN", category: "DRINK", stockQuantity: 1 } } });
完整Source Code(連顯示售賣貨品)。
import React from 'react'; import {COMMODITIES_BY_CATEGORY_QUERY} from './query'; import {ADD_COMMODITY} from './mutation'; import {useQuery,useMutation} from '@apollo/client'; export function CommodityList(props:{categoryName:string}){ const {data,loading,error} = useQuery(COMMODITIES_BY_CATEGORY_QUERY,{variables:props}); const [newCommunityMutation] = useMutation(ADD_COMMODITY,{ refetchQueries:[{query:COMMODITIES_BY_CATEGORY_QUERY,variables:props}], }); if(loading) return <p>Loading...</p>; if(error) return <p>Error!</p>; return (<> {data.CommoditiesByCategory.map((commodity:any,index:number)=>(<div key={index}> <span>Name : {commodity.name}</span>{' '}, <span>Category : {commodity.category}</span> <div>Store State : {commodity.stockQuantity} {commodity.unit}</div> </div>))} <button onClick={()=>{ newCommunityMutation({ variables: { commodityInput: { name: "Coffee", unit: "CAN", category: "DRINK", stockQuantity: 1 } } }) }}>Add</button> </>) }
Side Track Section
Side Track#1 : 如何不在建立useQuery之後即時問GraphQL Server取得資料?
Apollo Client提供另一個React Hook,useLazyQuery達成目的。
import React from 'react'; import {COMMODITIES_BY_CATEGORY_QUERY} from './query'; import {ADD_COMMODITY} from './mutation'; import {useQuery,useMutation,useLazyQuery} from '@apollo/client'; export function CommodityList(props:{categoryName:string}){ const [query,{called,loading,data}] = useLazyQuery(COMMODITIES_BY_CATEGORY_QUERY,{variables:props}); //<-- "useQuery" is changed to "useLazyQuery" const [newCommunityMutation] = useMutation(ADD_COMMODITY,{ refetchQueries:[{query:COMMODITIES_BY_CATEGORY_QUERY,variables:props}], }); if (loading) return <p>Loading...</p>; if (called && loading) return <p>Loading ...</p> if (!called){ return <button onClick={()=>query()}>Load Data</button>; } return (<> {data.CommoditiesByCategory.map((commodity:any,index:number)=>(<div key={index}> <span>Name : {commodity.name}</span>{' '}, <span>Category : {commodity.category}</span> <div>Store State : {commodity.stockQuantity} {commodity.unit}</div> </div>))} <button onClick={()=>{ newCommunityMutation({ variables: { commodityInput: { name: "Coffee", unit: "CAN", category: "DRINK", stockQuantity: 1 } } }) }}>Add</button> </>) }
const [query,{called,loading,data}] = useLazyQuery( COMMODITIES_BY_CATEGORY_QUERY, {variables:props} );
query, 即將被執行的query function。
called,顯示目前query function執行左未。執行後會從新 render component。
loading,是顯示目前是否仍然是載入狀態。
data,返回query statement要求的資料。
Side Track#2 : 超方便貼心!Apollo Client會自動更新對應Query的Cache !
首先是query.ts中的CommoditiesByCategory加入id field。
export const COMMODITIES_BY_CATEGORY_QUERY=gql` query($categoryName:CATEGORY) { CommoditiesByCategory(category: $categoryName) { id name category unit stockQuantity } } `;
步驟 2 :是mutation.ts加入以下mutation statement。
export const UPDATE_COMMODITY = gql` mutation($id:ID!,$updateCommodity:CommodityInput!){ updateCommodity(id:$id,updateCommodity:$updateCommodity){ id name category unit stockQuantity } } `;
步驟 3 :是CommodityList.tsx中建立UPDATE_COMMODITY useMutation Object。
const [updateCommunityMutation] = useMutation(UPDATE_COMMODITY);
步驟 4 :建立Button執行UPDATE_COMMODITY useMutation Object。
//.... return (<> {data.CommoditiesByCategory.map((commodity:any,index:number)=>(<div key={index}> .... <button onClick={ ()=>{ updateCommunityMutation({ //更新對應Community做以下資料 variables:{ id:commodity.id, updateCommodity:{ name: "Drink", unit: "CAN", category: "DRINK", stockQuantity: 1 } } }) } }>Update</button> </div>))} //.... </>)
大家打開React Application按Update,react會自動render component(就算無加refetchQueries@useMutation)。因為CommoditiesByCategory@useQuery當中的Community Object(必須包括id)被cache低。當UPDATE_COMMODITY@useMutation被執行並返回更新左的Community Object(必須包括id), Apollo Client 發現呢個對應Community Object有更新,因為Apollo Client check到cache中的id 同 更新左的Community Object的id是一樣。
Apollo Client會自動更新cache並render component。
GraphQL Client (應用版 - Apollo Client) Source Code
總結
大家應該對GraphQL Server同Client設計有基本概念。我會暫時離開一下GraphQ呢個topic, 俾大家沉澱下先。下一編blog講D輕鬆的topic。敬請留意。
References
https://www.apollographql.com/docs/react/data/mutations/ JavaScript Everywhere: Building Cross-Platform Applications with Graphql, React, React Native, and Electron
留言
閱讀更多
Tec ‧ 士多開發日記系列:第一篇:GraphQL簡介
2021-05-21
話說Tecky入面有間「Tec ‧ 士多」,大家攞完零食飲品,放低錢入錢箱就可以了。因為小弟最近研究緊GraphQL,所以小弟就諗可唔可以用GraphQL開發一個網上版「Tec。士多」。
Tec ‧ 士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 1
2021-06-21
上一編Blog已經詳細講解GraphQL Frontend既實作方法。今編Blog就深入講解GraphQL Server運作模式及如何實作。開始講解GraphQL Server運作原理及實作之前,有三個概念一定要了解左先,模式(Schema)、解析器(Resolver)及資訊源(Data Sources)。
Tec‧士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 2
2021-06-22
Part 1 講完理論部分。Part 2就開始實作了。
Tec。士多開發日記系列:第三篇:實作Tec記士多Frontend(GraphQL Client入門)- Part 1
2022-02-21
上一編Blog已經簡單講解如何實作GraphQL Server。今編Blog會深入講解Frontend如何同GraphQL Server進行溝通。Frontend最常做兩類GraphQL Query Statement,查詢(Query)及 變更(Mutation)。還有其他類型,不過坊間仍未普遍使用,所以在此不作介紹。