Skip to Content

Tec。士多開發日記系列:第三篇:實作Tec記士多Frontend(GraphQL Client入門)- Part 2

Tec。士多開發日記系列:第三篇:實作Tec記士多Frontend(GraphQL Client入門)- Part 2
Andrew Shek
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只用作示範,敬請留意!

VSCode 的樹狀目錄截圖左邊圖片是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先。

VSCode 的樹狀目錄截圖左邊圖片是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,是入面所有的Components可以分享同一個Apollo Client Object。

步驟 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

Comments

Read More

Tec ‧ 士多開發日記系列:第一篇:GraphQL簡介

Tec ‧ 士多開發日記系列:第一篇:GraphQL簡介

Tec ‧ 士多開發日記系列:第一篇:GraphQL簡介
Andrew Shek
2021-05-21

話說Tecky入面有間「Tec ‧ 士多」,大家攞完零食飲品,放低錢入錢箱就可以了。因為小弟最近研究緊GraphQL,所以小弟就諗可唔可以用GraphQL開發一個網上版「Tec。士多」。


Tec ‧ 士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 1

Tec ‧ 士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 1

Tec ‧ 士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 1
Andrew Shek
2021-06-21

上一編Blog已經詳細講解GraphQL Frontend既實作方法。今編Blog就深入講解GraphQL Server運作模式及如何實作。開始講解GraphQL Server運作原理及實作之前,有三個概念一定要了解左先,模式(Schema)、解析器(Resolver)及資訊源(Data Sources)。


 Tec‧士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 2

Tec‧士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 2

 Tec‧士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 2
Andrew Shek
2021-06-22

Part 1 講完理論部分。Part 2就開始實作了。


Tec。士多開發日記系列:第三篇:實作Tec記士多Frontend(GraphQL Client入門)- Part 1

Tec。士多開發日記系列:第三篇:實作Tec記士多Frontend(GraphQL Client入門)- Part 1

Tec。士多開發日記系列:第三篇:實作Tec記士多Frontend(GraphQL Client入門)- Part 1
Andrew Shek
2022-02-21

上一編Blog已經簡單講解如何實作GraphQL Server。今編Blog會深入講解Frontend如何同GraphQL Server進行溝通。Frontend最常做兩類GraphQL Query Statement,查詢(Query)及 變更(Mutation)。還有其他類型,不過坊間仍未普遍使用,所以在此不作介紹。


Request Syllabus
Please check your email after submissions.
hello@tecky.iot.me/tecky_hub+852 9725 6400
green_org
Caring Company 2019-2022
TQUK Approved Centre
aws_partner
Reimagine Education Challenge Award
B Corp™ Certified B Corporation
Web Content Accessibility Guidelines (WCAG) 2.1 at Level AA
Web Accessibility Gold Award
© 2024 Tecky Academy Limited