GraphQL入門到實踐

NO IMAGE

介紹

GraphQL 既是一種用於 API 的查詢語言也是一個滿足你數據查詢的運行時。如何理解呢?GraphQL 作為通用的 REST 架構的替代方案而被開發出來 ,通俗的講,在架構中他屬於和REST處於同一個層次的東西。對於REST,GraphQL的優勢在於:

  • REST接口的數據由後端定義,如果返回了前端不期望的數據結構就需要和後端溝通修改或者自己適配;
  • GraphQL向你的 API 發出一個 GraphQL 請求,客戶端就能準確獲得你想要的數據,不多不少;
  • GraphQL 可以通過一次請求就獲取你應用所需的所有數據,而REST API則需要請求多個URL;
  • GraphQL 查詢的結構和結果非常相似,因此即便不知道服務器的情況,你也能預測查詢會返回什麼結果。

Facebook 開源了 GraphQL 標準和其 JavaScript 版本的實現。後來主要編程語言也實現了標準。

Hello World

const Koa = require('koa');
const { ApolloServer, gql } = require('apollo-server-koa');
// 定義一個查詢
const typeDefs = gql`
type Query {
hello: String
}
`;
// 為上面定義的查詢提供數據
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const app = new Koa();
server.applyMiddleware({ app });
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`),
);

GraphQL入門到實踐

標量類型(Scalar Types)

上面例子中,我們給查詢定義的返回參數為String,GraphQL支持的標量類型還有StringIntFloatBooleanID

如果類型是一個列表,我們可以這個[Int],表示一個整數的列表,如果是自定義類型也可以這樣使用。

同時作為參數的時候,我們可以通過String!來表示該參數為必傳非空的。

指定查詢字段

const Koa = require('koa');
const low = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync');
const { ApolloServer, gql } = require('apollo-server-koa');
const adapter = new FileSync('./db/data.json');
const db = low(adapter);
// 定義兩個查詢方法
const typeDefs = gql`
type Query {
hero: Hero
heroList: [Hero]
}
type Hero {
name: String,
age: Int
}
`;
// 為各自的方法提供數據
const resolvers = {
Query: {
hero: async (parent, args, context, info) => {
return db.get('hero').value()[0];
},
heroList: async (parent, args, context, info) => {
return db.get('hero').value();
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const app = new Koa();
server.applyMiddleware({ app });
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`),
);
  • 簡單查詢

    如果查詢返回的一個對象的時候,你需要為查詢執行需要返回的字段,只會返回指定的字段

    GraphQL入門到實踐

  • 只需要 hero 的 name 屬性

    GraphQL入門到實踐

  • 一次請求,可以同時進行多次查詢,比如同時查詢 hero 和 heroList 列表

    GraphQL入門到實踐

參數(Arguments)

類似 REST API ,Graphql也支持給每一個查詢添加查詢參數,我們只需要做如下修改:

...
const typeDefs = gql`
type Query {
hero(id :Int): Hero
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hero: async (parent, args, context, info) => {
const { id } = args;
return db.get('hero').find({ id }).value();
}
},
};
...

下面演示了查詢結果:我們通過不同的查詢ID,查詢出來不同的 hreo:

GraphQL入門到實踐

細心的你會發現一個細節,返回的字段名為first,second。這裡我使用了別名的方式,因為兩個hero字段相同,所以存在衝突,我們使用別名來進行區分。

別名(Aliases)

通過上圖,又接觸了一個新的概念 別名。主要作用是使得返回字段使用另外一個名字,避免對相同方法不同參數下返回數據衝突的問題(如上)。只要在字段前面使用 別名:就可以把返回數據的key自動替換為該名稱。

片段(Fragments)

如果你追求嚴謹的代碼規範和工程能力,你肯會對 firstsecond中兩處冗餘同樣的 nameage感到難受。一旦以後需需要增減或者修改字段的時候,需要多出地方,容易造成bug。Graphql 為已經考慮到這一點,我們可以使用片段來優化:

GraphQL入門到實踐

通過片段我們很好的提取了可服用單元,然後再需要他們的地方引入。

變量(Variables)

我我們現在查詢,變量是寫在查詢字符串中,而實際情況,我們的變量是多態的,每次都去拼接查詢字符串不是一明智的方式。Graphql提供了很好的方式來幫我我們來解決這個問題。我們可以定義一個 變量

  • 使用 $variableName 聲明一個變量
  • 使用 $variableName:value 通過變量字典來傳輸
GraphQL入門到實踐

如上圖我們通過變量 的方式,解決了變量多態的問題。

指令(Directives)

雖然變量幫我們解決了多態的問題,但還是不夠完美,比如有一個用戶信息列表,我們想針對不同權限的人顯示不同的信息。用戶收入只允許人力部門查看,用戶的學歷只允許直屬領導查看等,因此我們需要針對不同的權限來過濾。

Graphql核心規範包含兩個指令

  • @include(if: Boolean) 僅在參數為 true 時,包含此字段。
  • @skip(if: Boolean) 如果參數為 true,跳過此字段(不查詢此字段)。
GraphQL入門到實踐

GraphQL入門到實踐

通過以上查詢,我們對isAdmin 變量賦值truefalse進行了兩次查詢,從結果可以看到我們控制到了查詢字段的展示。

變更(Mutations)

如同REST一樣,我們不僅需要查詢,還需要修改數據:

操作流程如下

  1. typeDefs 中使用在 Mutation塊中定義一個 修改的方法
  2. resolvers 中使用在Mutation塊中去實現
...
const typeDefs = gql`
type Mutation {
createHero(name: String, age: Int): [Hero]
}
type Hero {
id: Int
name: String,
age: Int
}
`;
const resolvers = {
Mutation: {
createHero: async (parent, args, context, info) => {
const model = db.get('hero');
const len = model.value().length;
model
.push({ id: len + 1, ...args })
.write();
return db.get('hero').value();
}
}
};
...

最後我們在 mutation中進行操作,如下圖:

GraphQL入門到實踐

從結果中,我們看到成功的創建了英雄長孫長雪 。注意我們創建英雄後,我們返回了heroList。和查詢一樣,我們也可以同時進行多個變更操作,不一樣的是:

查詢是並行執行,而變更操作是線性執行的一個接著一個

如下圖:

GraphQL入門到實踐

輸入類型(Input Types

在前面的例子中,我們傳入的類型都為標量類型,如果我們想傳入一個複雜的結構數據可以使用 input關鍵字。其用法和type一樣,服務端定義完後,客戶端查詢的時候也可以使用該類型了。

const typeDefs = gql`
type Query {
hero(id :Int): Hero
}
## 這裡對 AttrInput 類型進行使用
type Mutation {
createHero(name: String, age: Int, attr: AttrInput): [Hero]
}
## 這邊定義一個 input 參數 ##
input AttrInput {
shoes: String
clothes: String
hat: String
}
type Hero {
id: Int
name: String
age: Int
attr: Attr
}
type Attr {
shoes: String
clothes: String
hat: String
}
`;

然後我們進行一個 變更操作:

GraphQL入門到實踐

根據定義我們的 attr 參數為:

"attr": {
"shoes":"Boot",
"hat":"Peaked",
"clothes":"shirt"
}

對象類型(Object Types)

有時候我們不僅僅希望API返回的只是一個數字或者字符串,我們希望可以返回一個對象,而且是帶有行為的對象,GraphQL可以很完美的契合這個需求。我們只需要設置返回值為對象

const Koa = require('koa');
const { ApolloServer, gql } = require('apollo-server-koa');
// 定義一個查詢,並且返回值為一個對象
const typeDefs = gql`
type Query {
getDie(numSides: Int): RandomDie
}
type RandomDie {
numSides: Int!
rollOnce: Int!
roll(numRolls: Int!): [Int]
}
`;
...
// 實現一個 對象
class RandomDie {
constructor(numSides) {
this.numSides = numSides;
}
rollOnce() {
return 1 + Math.floor(Math.random() * this.numSides);
}
roll({ numRolls }) {
var output = [];
for (var i = 0; i < numRolls; i++) {
output.push(this.rollOnce());
}
return output;
}
}
// 查詢實現中,返回該對象的實例
const resolvers = {
Query: {
getDie: async (parent, args, context, info) => {
return new RandomDie(args.numSides || 6);
}
}
}
const server = new ApolloServer({ typeDefs, resolvers });
const app = new Koa();
server.applyMiddleware({ app });
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`),
);

然後我們就可以調用了,而且可以調用對象的行為(對象的方法):

GraphQL入門到實踐

從上圖中我們可以看出,我們調用 查詢getDie,獲得對象後,可以直接繼續執行對象的行為,這樣為後端服務的抽象和複用提供了非常大的幫助。

接口(Interfaces)

GraphQL 也支持接口,它包含某些字段:

  • 對象類型必須包含這些字段,才能算實現了這個接口;

  • 因為 GraphQL 是強類型的,所以當返回類型為接口的時候,需要 實現__resolveType來告訴GraphQL返回的具體類型。

const Koa = require('koa');
const { ApolloServer, gql } = require('apollo-server-koa');
// 定義一個查詢,並且返回值為一個對象
const typeDefs = gql`
interface Book {
title: String
author: String
}
type TextBook implements Book {
title: String
author: String
classes: [String]
}
type Ebook implements Book {
title: String
author: String
format: String
}
type Query {
schoolBooks(type: String): [Book]
}
`;
// 查詢實現中,返回該對象的實例
const resolvers = {
Book: {
// 當你定義一個查詢的返回類型是 union 或者是 interface 的時候,
// 必須定義這個解析器告訴 graphql 返回的具體類型
__resolveType(book, context, info) {
if (book.classes) {
return 'TextBook';
}
if (book.format) {
return 'Ebook';
}
return null;
},
},
Query: {
schoolBooks: (parent, args) => {
const { type } = args;
if (type === 'TextBook') {
return [{
title: '紅樓夢', author: '曹雪芹', classes: ['名著', '文學']
}]
}
if (type === 'Ebook') {
return [{
title: '紅樓夢', author: '曹雪芹', format: 'pdf'
}]
}
}
}
}
const server = new ApolloServer({ typeDefs, resolvers });
const app = new Koa();
server.applyMiddleware({ app });
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`),
);

GraphQL入門到實踐

內聯片段(Inline Fragments

在上圖的查詢中,schoolBooks字段返回Book類型,取決於參數返回具體的TextBookEbook。此時如果你需要請求具體類型上的字段就需要使用內聯片段。標註為:... on TextBook,僅在schoolBooks 返回TextBook類型的時候才會被執行,同理適用於Ebook類型。

元字段(Meta fields)

繼續上面的案例,某些情況下,你並不知道你將從 GraphQL 服務獲得什麼類型。但是你又需要根據類型來決定如何處理數據,這時候就可以使用__typename

  • 允許放在查詢的任何位置請求__typename
GraphQL入門到實踐

GraphQL 服務提供了不少元字段,剩下的部分用於描述 內省 系統。

聯合類型(Union Types)

聯合類型和接口十分相似,但是它並不指定類型之間的任何共同字段。他只的是一個類型可以支持多種不同的類型返回:union Result = Book | Author。任何一個返回Result的地方都可能得到Book或`Author“。

  • 聯合類型的成員需要是具體對象類型;你不能使用接口或者其他聯合類型來創造一個聯合類型。
  • 查詢同樣需要使用條件片段
const Koa = require('koa');
const { ApolloServer, gql } = require('apollo-server-koa');
// 定義一個查詢,並且返回值為一個對象
const typeDefs = gql`
union Result = Book | Author
type Book {
title: String
}
type Author {
name: String
}
type Query {
search(type:String): [Result]
}
`;
// 查詢實現中,返回該對象的實例
const resolvers = {
Result: {
// 當你定義一個查詢的返回類型是 union 或者是 interface 的時候,
// 必須定義這個解析器告訴 graphql 返回的具體類型
__resolveType(obj, context, info) {
if (obj.title) {
return 'Book';
}
if (obj.name) {
return 'Author';
}
return null;
},
},
Query: {
search: (parent, args) => {
console.log(args)
const { type } = args;
if (type === 'Book') {
return [{
title: '紅樓夢'
}]
}
if (type === 'Author') {
return [{
name: '曹雪芹'
}]
}
}
}
}
const server = new ApolloServer({ typeDefs, resolvers });
const app = new Koa();
server.applyMiddleware({ app });
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`),
);

GraphQL入門到實踐

Mock數據

  • 以上案例使用如下的數據進行演示
    • db 中的數據
  • lowdb 提供一個本地的數據查詢引擎服務
{
"hreo": [
{
"id": 1,
"name": "007",
"age": 18,
"family": {
"mother": "Madhh",
"father": "Father"
}
},
{
"id": 2,
"name": "R2-D2",
"age": 20,
"family": {
"mother": "Madhh",
"father": "Father"
}
}
]
}

更多內容,歡迎關注我的Github

參考

GraphQL 官網

Unions and interfaces

相關文章

代碼生成方案——parser&&generaotr帶來的無限想象

說說Flutter中的Semantics

這一次,徹底理解https原理

那天晚上和@FeignClient註解的深度交流