Tec‧士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 2
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:
- 補貨
- 新增、刪除、更新零食飲品
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 Playground見到一堆Queries,看唔明?下一編Blog詳細介紹。
References
- JavaScript Everywhere: Building Cross-Platform Applications with Graphql, React, React Native, and Electron
- https://graphql.org/graphql-js/basic-types/
- https://graphql.org/graphql-js/mutations-and-input-types/
- https://github.com/the-road-to-graphql/the-road-to-graphql-chinese/blob/master/manuscript/08-apollo-server/index.md
留言
閱讀更多
零基礎.10分鐘輕鬆製作STEM教材
2020-07-03
因為疫情學校停課,造就「網上教學」興起。但是教師們多數只是用視訊會議軟體進行網上「授課」,但是並不是真正既「網上教學」。根據範式轉移:網上教學的迷思,eLearning(網上教學)應該是:
0成本!分析「保就業計劃」數據,超方便隨時網上更新分享分析結果
2020-08-20
根據工貿署2020年3月數字,中小企是佔本港商業單位總數98%以上,但是從「保就業」數據當中得知,中小企只是佔13.6%。可以等第二期結果出爐再統計一下。從數據分析得知,就算派得最多錢唔代表可保就業。成功申請的中小企比較小;大部分資助落入0人企業和微型企業手上,造成分配不公。成效成疑。同時從中反映出成個計畫審批唔透明,無完善監管機制阻止濫用。政府需要好好檢討完善成個計畫。
分析「保就業計劃」數據,超方便隨時網上更新分享分析結果 (Microsoft Power BI 版本)
2020-09-02
根據工貿署2020年3月數字,中小企是佔本港商業單位總數98%以上,但是從「保就業」數據當中得知,中小企只是佔13.6%。可以等第二期結果出爐再統計一下。從數據分析得知,就算派得最多錢唔代表可保就業。成功申請的中小企比較小;大部分資助落入0人企業和微型企業手上,造成分配不公。成效成疑。同時從中反映出成個計畫審批唔透明,無完善監管機制阻止濫用。政府需要好好檢討完善成個計畫。
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)。