Part 1 講完理論部分。Part 2就開始實作了。
用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, 在VS Code Terminal執行以下Command。
node .
是瀏覽器輸入以下網址打開GraphQL Playground:
http://localhost:4000/graphql
取得所有飲品零食。
取得所有飲品。
新增一款新零食。
一款零食補貨。
刪除一款零食。
是GraphQL Playground見到一堆Queries,看唔明?下一編Blog詳細介紹。