Skip to Content

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

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

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

實作GraphQL Server


Tec。士多開發日記系列:第一篇:GraphQL簡介既example示範GraphQL Server使用Schema同Resolver。

import {ApolloServer,gql} from 'apollo-server-express';
import Express from 'express';

class Person{
    public name:string;
    public age: number;
    public email: string;
    constructor(name:string,age:number,email:string){
        this.name = name;
        this.age = age;
        this.email = email;
    }
}

const personList:Person[] = [];
personList.push(new Person("Tom",34,"tom@gmail.com"));
personList.push(new Person("Peter",30,"peter@gmail.com"));
personList.push(new Person("Ken",25,"ken@gmail.com"));

const app = Express();
const typeDefs = gql`
    type Person{
        name:String,
        age:Int,
        email:String
    }
    type Query{
        person:[Person]
    }
`;
const resolvers= {
    Query:{person:()=>personList},
    Person:{
        name:(parent:Person)=>parent.name,
        age:(parent:Person)=>parent.age,
        email:(parent:Person)=>parent.email
    }
};

const server = new ApolloServer({typeDefs,resolvers});
server.applyMiddleware({app,path:'/graphql'});
app.listen(4000,()=>{
    console.log("GraphQL Server is started at http://localhost:4000/graphql");
});

typeDefs變數就是Schema。resolvers變數就是Resolver。呢個變數會被抽起變成獨立模組(Module), 所以backend會被簡化。

import {ApolloServer,gql} from 'apollo-server-express';
import {typeDefs} from './schema';
import {resolvers} from './resolvers';
import Express from 'express';

const app = Express();
const server = new ApolloServer({typeDefs,resolvers});
server.applyMiddleware({app,path:'/graphql'});
app.listen(4000,()=>{
    console.log("GraphQL Server is started at http://localhost:4000/graphql");
});

題目是Tec。士多開發日記,所以Schema同Resolver不再用Person做例子。改用零食飲品做例子。就由零食飲品物件開始。往後日子慢慢加入新成員。

//schema.ts
export const typeDefs = gql`
    type Commodity{
				id:ID!
        name:String!
				
    }
    type Query{
        commodities:[Commodity]
    }
`;

是Schema當中,定義一個物件是用type關鍵字開頭。例如,定義一個Commodity Object,就用如下寫法:

type Commodity{
	id:ID!
  name:String!			
}

定義Query物件是必需。因為Frontend需要透過Query物件進行查詢物件,Mutation如是,所以所有物件(例如,Commodity物件)必需放入Query物件。

每一個物件有不同特性/屬性,例如,Commodity物件。id及name就是特性/屬性名。ID及String就是資料類型。!是代表不可以NULL,就是必需要有值。

id:ID!
name:String!

再望一望Query物件,其實大同小異。不過 [ ] 是咩意思呢?[ ] 是指特性/屬性(Field)的資料類型Commodity物件Array,即是Commodity[]。

 commodities:[Commodity]

講完Schema就到Resolver。要幫Schema入面已定義的**物件、特性/屬性(Field)**定義Resolver。

//resolvers.ts
import {Commodity} from './commodity';

const commodityList:Commodity[] = [];
commodityList.push(new Commodity(1,"240ml經典橙汁"));
commodityList.push(new Commodity(2,"朱古力夾心餅"));
commodityList.push(new Commodity(3,"XX杯麵。五香牛肉"));

export const resolvers = {
    Query:{
        Commodities:()=>commodityList
    },
    Commodity:{
        id:(commodity:Commodity)=>commodity.getID(),
        name:(commodity:Commodity)=>commodity.getName()
    }
}

首先為Query物件入面既Field定義Resolver,通常都是使用Closure寫法。每一個Closure最後會返回一個物件/值。例如,Commodities,會返回一個Commodity物件Array。

 Query:{
        Commodities:()=>commodityList
 }

做完?id及name唔駛定義返回值咩?答案是。。。已經做完。

GraphQL可以根據物件key名map返去GraphQL Field名。

Commodity:{
	id:(commodity:Commodity)=>commodity.getID(),
  name:(commodity:Commodity)=>commodity.getName()
}

不過,都可以針對每一個Field定義(或者稱Override#)一個Resolver。目的是可以做資料處理,因為Closure都是一個Function。Closure是返回數值前可以做編程。以下例子純粹示範,實際情況是無需要。

#因為GraphQL會為每一個Field自動加一個Default Resolver。

Commodity:{
	id:(commodity:Commodity)=>{
    const id = commodity.getID();
    if (id >= 0){
      return id;
    }else{
      return 0;
    }
  },
  name:(commodity:Commodity)=>{
    let name = commodity.getName();
    //For example, test -> Test
    name = name[0].toUpperCase() + name.substring(1);
    return name;
  }
}

以下是Commodity物件既源碼參考。因為typescript要俾type,所以俾大家參考用。不作詳細介紹。

//commodity.ts
export class Commodity{
    private id:number;
    private name:string;
    constructor(id:number,name:string){
        this.id = id;
        this.name = name;
    }
    getID = ()=>this.id;
    getName= ()=>this.name;
}

Tec。記士多有多款零食供大家選擇。如果學生只是想買飲品,但是每一次開站網都要問Backend攞一條完整零食清單。咁是好浪費頻寬,造成資訊過度獲取(REST是唔是一種完美既溝通形式? Point#3 )。其實可以透過使用**參數(Parameters)**解決問題。

//schema.ts
export const typeDefs = gql`
    type Commodity{
				id:ID!
        name:String!
    }
		enum CATEGORY{
			DRINK
      SNACK
      NOODLES
		}
    type Query{
        commodities:[Commodity],
				commodities(category:CATEGORY!):[Commodity]
    }
`;

為左令大家做Project唔會餓親,Tec。記士多會定時補貨。而且會不定是轉換零食飲品,迎合不同學生需要。例如,加入樽裝涼茶,幫做Project捱左幾晚通宵的學生們下火🤯。哈!所以Tec。記士多剛剛多左兩個Requirements:

  1. 補貨
  2. 新增、刪除、更新零食飲品

Query物件可以取得物件。如果要做新增、移除、更新物件得動作,例如,補貨、新增、刪除、更新零食飲品,就需要用Mutation物件幫手。根據以上既要求,schema.ts新增左Mutation物件:

//schema.ts
import {gql} from 'apollo-server';
export const typeDefs = gql`
type Commodity{
    id:ID!
    name:String!
    unit:UNIT!
    category:CATEGORY!
}

enum UNIT{
    CAN
    BOTTLE
    BOX
    PACK
    CUP
}

enum CATEGORY{
    DRINK,
    SNACK,
    NOODLES
}

type Query{
    Commodities:[Commodity],
    CommoditiesByCategory(category:CATEGORY):[Commodity]
}

type Mutation{
    addCommodity(name:String!,unit:UNIT!,category:CATEGORY!,stockQuantity:Int = 0):Commodity,
    updateCommodity(id:ID!,name:String!,unit:UNIT!,category:CATEGORY!,stockQuantity:Int = 0):Commodity,
    deleteCommodity(id:ID!):Commodity
}
`;

因應以上要求,commodity.ts做左相應更新。

//commodity.ts
export enum CATEGORY{
    DRINK = 0,
    SNACK,
    NOODLES
}
export enum UNIT{
    CAN = 0,
    BOTTLE,
    BOX,
    PACK,
    CUP
}
export class Commodity{
    private id:number;
    private name:string;
    private unit:UNIT;
    private category:CATEGORY;
    private stockQuantity:number;
    constructor(id:number,name:string,unit:UNIT,category:CATEGORY,stockQuantity:number = 0){
        this.id = id;
        this.name = name;
        this.unit = unit;
        this.category = category;
        this.stockQuantity = stockQuantity;
    }
    getID = ()=>this.id;
    getName= ()=>this.name;
    getUnit= ()=> UNIT[this.unit as number];
    getCategory = ()=> CATEGORY[this.category as number];
    getStockQuantity = ()=> this.stockQuantity;

    setName= (name:string)=>this.name = name;
    setUnit= (unit:UNIT)=>this.unit =unit;
    setCategory= (category:CATEGORY)=>this.category = category;
    setStockQuantity= (stockQuantity:number)=>this.stockQuantity = stockQuantity;
}

但是大家有無覺得schema.ts好冗長嗎?Mutation物件中有好多參數是重複。其實有方法簡化。使用Input Object Type集合所有會重用的參數。而該Input Object Type只可在Mutation物件中當作物件類型(Object Type)使用。

//建議所有Input Type,Type Name用Input結尾,因為方便分辨「真。資料類型」及「Input Type」
input CommodityInput{
    name:String!
    unit:UNIT!
    category:CATEGORY!
    stockQuantity:Int = 0
}

更新了的schema.ts如下。

//schema.ts
import {gql} from 'apollo-server';
export const typeDefs = gql`
type Commodity{
    id:ID!
    name:String!
    unit:UNIT!
    category:CATEGORY!
    stockQuantity:Int
}

enum UNIT{
    CAN
    BOTTLE
    BOX
    PACK
    CUP
}

enum CATEGORY{
    DRINK,
    SNACK,
    NOODLES
}

type Query{
    Commodities:[Commodity],
    CommoditiesByCategory(category:CATEGORY):[Commodity]
}

input CommodityInput{
    name:String!
    unit:UNIT!
    category:CATEGORY!
    stockQuantity:Int = 0
}

type Mutation{
    addCommodity(newCommodity:CommodityInput!):Commodity,
    updateCommodity(id:ID!,updateCommodity:CommodityInput!):Commodity,
    deleteCommodity(id:ID!):Commodity
}
`;

更新了的resolvers.ts如下。

//resolvers.ts
import {Commodity,UNIT,CATEGORY} from './commodity';

const commodityList:Commodity[] = [];
commodityList.push(new Commodity(0,"240ml經典橙汁",UNIT.CAN,CATEGORY.DRINK));
commodityList.push(new Commodity(1,"朱古力夾心餅",UNIT.PACK,CATEGORY.SNACK));
commodityList.push(new Commodity(2,"XX杯麵。五香牛肉",UNIT.CUP,CATEGORY.NOODLES));

export const resolvers = {
    Query:{
        Commodities:()=>commodityList,
        CommoditiesByCategory:(root:any,parameters:any)=>commodityList.filter(
          commodity=>commodity.getCategory() === parameters.category)
    },
    Commodity:{
        id:(commodity:Commodity)=>commodity.getID(),
        name:(commodity:Commodity)=>commodity.getName(),
        unit:(commodity:Commodity)=>commodity.getUnit(),
        category:(commodity:Commodity)=>commodity.getCategory(),
    },
    Mutation:{
        addCommodity:(root:any,parameters:any)=>{
            const {newCommodity:{name,unit,category,stockQuantity}} = parameters;
            const commodity = new Commodity(commodityList.length,
                                            name,
                                            UNIT[unit as string],
                                            CATEGORY[category as string],
                                            stockQuantity);
            commodityList.push(commodity);
            console.log(commodityList);
            return commodity;
        },
        updateCommodity:(root:any,parameters:any)=>{
            const {id,updateCommodity:{name,unit,category,stockQuantity}} = parameters;
            const commodity = commodityList[id];
            commodity.setName(name);
            commodity.setUnit(UNIT[unit as string]);
            commodity.setCategory(CATEGORY[category as string]);
            commodity.setStockQuantity(stockQuantity);
            return commodity;
        },
        deleteCommodity:(root:any,parameters:any)=>{
            const {id} = parameters;
            const delCommodities = commodityList.splice(id,1);
            return delCommodities[0];
        }
    }
}

測試GraphQL Server


要啟動GraphQL Server, 在VS Code Terminal執行以下Command。

node .

是瀏覽器輸入以下網址打開GraphQL Playground:

http://localhost:4000/graphql

取得所有飲品零食。

取得所有飲品零食 GraphQL 示範

取得所有飲品。

取得所有飲品 GraphQL 示範

新增一款新零食。

新增一款新零食 GraphQL 示範

一款零食補貨。

一款零食補貨 GraphQL 示範

刪除一款零食。

刪除一款零食 GraphQL 示範

是GraphQL Playground見到一堆Queries,看唔明?下一編Blog詳細介紹。

Tec。記士 Demo Source

References


Comments

Read More

零基礎.10分鐘輕鬆製作STEM教材

零基礎.10分鐘輕鬆製作STEM教材

零基礎.10分鐘輕鬆製作STEM教材
Andrew Shek
2020-07-03

因為疫情學校停課,造就「網上教學」興起。但是教師們多數只是用視訊會議軟體進行網上「授課」,但是並不是真正既「網上教學」。根據範式轉移:網上教學的迷思,eLearning(網上教學)應該是:


0成本!分析「保就業計劃」數據,超方便隨時網上更新分享分析結果

0成本!分析「保就業計劃」數據,超方便隨時網上更新分享分析結果

0成本!分析「保就業計劃」數據,超方便隨時網上更新分享分析結果
Andrew Shek
2020-08-20

根據工貿署2020年3月數字,中小企是佔本港商業單位總數98%以上,但是從「保就業」數據當中得知,中小企只是佔13.6%。可以等第二期結果出爐再統計一下。從數據分析得知,就算派得最多錢唔代表可保就業。成功申請的中小企比較小;大部分資助落入0人企業和微型企業手上,造成分配不公。成效成疑。同時從中反映出成個計畫審批唔透明,無完善監管機制阻止濫用。政府需要好好檢討完善成個計畫。


分析「保就業計劃」數據,超方便隨時網上更新分享分析結果 (Microsoft Power BI 版本)

分析「保就業計劃」數據,超方便隨時網上更新分享分析結果 (Microsoft Power BI 版本)

分析「保就業計劃」數據,超方便隨時網上更新分享分析結果 (Microsoft Power BI 版本)
Andrew Shek
2020-09-02

根據工貿署2020年3月數字,中小企是佔本港商業單位總數98%以上,但是從「保就業」數據當中得知,中小企只是佔13.6%。可以等第二期結果出爐再統計一下。從數據分析得知,就算派得最多錢唔代表可保就業。成功申請的中小企比較小;大部分資助落入0人企業和微型企業手上,造成分配不公。成效成疑。同時從中反映出成個計畫審批唔透明,無完善監管機制阻止濫用。政府需要好好檢討完善成個計畫。


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)。


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