GraphQL 入門簡介

shanyue發表於2019-11-26

此篇文章介紹 graphql 基礎知識,會過於無聊。如果你想快速上手,可以使用我的腳手架 shfshanyue/apollo-server-starter。如果你想用它寫一個前端,可以參考我的 shfshanyue/shici

本文地址: graphql schema and query

我們先寫一個關於 graphql.jshello, world 示例,並且圍繞它展開對 graphql 的學習

const {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString,
} = require('graphql')

// schema,由 web 框架實現時,這部分放在後端裡
const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
      hello: {
        type: GraphQLString,
        resolve() {
          return 'hello, world'
        }
      }
    }
  })
})

// query,由 web 框架實現時,這部分放在前端裡
const query = '{ hello }'

// 查詢,這部分放在服務端裡
graphql(schema, query).then(result => {
  // {
  //   data: { hello: "hello, world" }
  // }
  console.log(result)
})
複製程式碼

由上,也可以看出 graphql 很關鍵的兩個要素:schemaquery。而當我們開發 web 應用時,schema 將會是服務端的主體,而 query 存在於前端中,類似 REST 中的 API。

schema and query

我們先抽出 schema 的程式碼部分,這裡有 GraphQLSchemaGraphQLObjectTypeGraphQLString 等諸多 API。

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
      hello: {
        type: GraphQLString,
        resolve() {
          return 'hello, world'
        }
      }
    }
  })
})
複製程式碼

正如在 React 中使用jsx 簡化了 React.createElement 的寫法。graphql 對於 schema 也有一套它自己的 DSL (Domain Specified Language),也更為簡單,易懂。在程式碼中以 graphqlgql 作為檔名字尾,用法如下

# schama,在後端進行維護
type Query {
  hello: String
}

# query,在前端進行管理
{
  hello
}
複製程式碼

以及查詢結果

{
  hello: 'hello, world'
}
複製程式碼

在前端中所有查詢的 gql 往往會通過 graphql/gql 字尾的檔案來統一維護,這裡有一份程式碼示例: shfshanyue/shici:query.gql

你看到這裡,想必有兩個疑問:

  1. 以上的 graphql 代表什麼,以及我們如何書寫 graphql
  2. 相比 js 程式碼,DSL 少了一個 resolve 函式,而它又是什麼

Object Type and Field

這裡引入 graphql 中的兩個基本術語,object typefield。它們是組成 graphql 最基本的元件,如同細胞是生物體的基本單位。

這裡來一個更復雜的 schema,如下所示

# schema
schema {
  query: Query
}

type Query {
  hello: String
  todos: [Todo!]!
}

type Todo {
  id: ID!
  title: String!
}

# query
{
  # 只能查詢 Query 下的欄位
  todos {
    id 
    title
  }
  hello
}
複製程式碼

如果說 graphql 是資料庫的進一步抽象,則 object type 類似於 sql 中的 tablefield 類似於 sql 中的 column

那我們仔細審視以上示例,能從其中得到一些資訊:

  • type 標註為 graphql.js 中的型別: GraphQLObjectType
  • {} 代表一個 query (查詢),其中由若干欄位組成,用以查你所需要的資料
  • Query 是一個特殊的 object type,表示為 RootQueryType,它會放到 schema 中。如同C語言中的 main 函式,可以理解為 graphql 的入口查詢。正因如此,它所包含的 field 沒有緊密的內關聯關係。
  • hellotodos 是 Query 下的兩個 field,一切前端的查詢均要從 Queryfield 查起。如在以上的 query 示例中,只能查詢 todoshello
  • Todo 是一個自定義名稱的 object type,可以理解為對應資料庫中的一個 todo 的表。
  • idtitle 是 Todo 下的 field,可以理解他們為 Todo 的屬性,它們往往由一些基本屬性以及聚合屬性 (count, sum) 組成。
  • [Todo!]! 代表返回結果將是一個 Todo 的陣列。[] 代表返回為陣列,! 代表返回不能為空,[!] 代表陣列中的每一項都不能為空。
  • id: ID! 代表 Todo 的 id 全域性唯一
  • title: String! 代表 Todo 的 title 是不為空的字串

到了這裡,你會發現,對於 graphql schema 的認識還有一些資訊尚未涉及:IDString,它們被稱作 scalar type,你可以理解為資料型別。 正是因為 scalar,graphql 才成為強型別查詢語言。

Query: query everyting

由上所述,Querygraphql 的入口查詢處,我們可以並且只可以查詢 Query 下的任意欄位 (field)。因此,他組成了 graphql 最核心的功能: 查詢你所需要的任何資料

# schema
type Query {
  hello: String
  todos: [Todo!]!
}

// 以下三個 query 一般會在前端進行統一管理
# query 1
{
  hello
}

# query 2
{
  todos {
    id 
  }
}

# query 3
{
  hello
  todos {
    id
    title 
  }
}
複製程式碼

查詢結果如下

{ hello: 'hello, world' }
{ todos: [{ id: 1 }] }
{ hello: 'hello, world', todos: [{ id: 1, title: 'learn react' }] }
複製程式碼

在前端我們根據 Query 組合成各種查詢,而我們為了在查詢過程中方便辨認,可以為查詢新增 operationName

query HELLO {
  hello
}

query TODOS {
  todos {
    id 
  }
}

query HELLO_AND_TODOS {
  hello
  todos {
    id
    title 
  }
}
複製程式碼

Scalar Type

graphql 中有一些內建的 scalar 型別,用以表示 graphql 中 field 的資料型別,這也是 graphql 為強型別語言的基礎。內建型別如下所示

  • Int,代表32位有符號型整數
  • Float
  • String
  • Boolean
  • ID,唯一識別符號,一般可視為資料庫中的主鍵。在 object type 中,一般會把 id 設定為 ID 型別,依賴它做一些快取的操作。

正因為 scalar!,來保證了 graphql 的 query 是強型別的。所以當我們看到如下的 query 時,可以在前端大膽放心的使用: data.todos.map(todo => todo.title.replace(' ', ''))。既不用擔心 data.todos 突然報錯 Cannot read property 'map' of null,也不用擔心 Cannot read property 'title' of null

# schema
type Query {
  hello: String
  todos: [Todo!]!
}

type Todo {
  id: ID!
  title: String!
}

# query
{
  todos {
    id 
    title
  }
}
複製程式碼

如果不使用 graphql,你無法保證響應中資料的型別。你可能需要使用 lodash 來做一些邊界的處理

const data = {
  todos: [{
    id: 1, 
    title: 'learn react'
  }]
}

// 使用 graphql 後
data.todos.map(todo => todo.title.replace(' ', ''))

// 如果不使用 graphql,你無法保證返回資料型別。你可能需要使用 lodash 來做一些邊界的處理
_.map(data.todos, todo => _.replace(todo.title, ' ', ''))
複製程式碼

當在資料庫中,一個欄位除了整型,浮點型等基本型別外,還會有更多而且比較重要和常用的資料型別:jsondatetime。既然 scalar 用以表示 field 的資料型別,那麼它如何表示 jsondatetime 或者更多的資料型別呢?

這時,可以使用 graphql.js 的API graphql.GraphqlQLScalarType 來自定義 scalar

resolve function

再回到剛開始的 hello, world 的示例,用 graphql 表示如下

# schema
type Query {
  hello: String
}

# query
query HELLO {
  hello
}
複製程式碼

對以上章節的內容再梳理一遍:

  1. 可以對 hello 進行查詢,因為該欄位在 Query
  2. HELLO 查詢所得到的 data.hello 是一個字串

恩?我們好像把最重要的內容給漏了,hello 中的內容到底是什麼?!而 resolve 函式就是做這個事的

// 使用 graphql.js 的寫法,把 schema 與 resolve 寫在一起
new GraphQLObjectType({
  name: 'RootQueryType',
  fields: {
    hello: {
      type: GraphQLString,
      resolve() {
        return 'hello, world'
      }
    }
  }
})


// 單獨把 resolve 函式給寫出來
function Query_hello_resolve () {
  return 'hello, world'
}
複製程式碼

於是我們再補齊以上內容

# schema
type Query {
  hello: String
}

# query
{
  hello
}
複製程式碼

由此得到的資料示例

{
  hello: 'hello, world'
}
複製程式碼

context and args

檢視一個很典型的 REST 服務端的一段邏輯:抽取使用者ID以及讀取引數(querystring/body)

app.use('/', (ctx, next) {
  ctx.user.id = getUserIdByToken(ctx.headers.authorization)
  next()
})

app.get('/todos', (ctx) => {
  const userId = ctx.user.id
  const status = args.status
  return db.Todo.findAll({})
})
複製程式碼

而在 graphql 中,使用 resolve 函式為 field 提供資料,而 context,args 都會作為 resolve 函式的引數

# schema
type Query {
  # 如同 REST 一般,可以攜帶引數,並顯式宣告
  todos (status: String): [Todo!]!
}

type Todo {
  id: ID!
  title: String!
}

# query
{
  # 查詢時,在這裡指定引數 (args)
  todos (status: "TODO") {
    id 
    title
  }
  # 同時也可以指定別名,特別是當有 args 時
  done: todos (status: "DONE") {
    id
    title
  }
}

# query with variables
query TODOS ($status: String) {
  done: todos (status: $status) {
    id 
    title
  }
}
複製程式碼

返回資料示例

{
  todos: [{ id: 1, title: '松風吹解帶' }],
  done: [{ id: 2, title: '山月照彈琴' }],
}
複製程式碼

當然,我們也是通過 Query 以及 Todo 的 resolve 函式來確定內容,對於如何獲取以上資料如下所示

// Query 的 resolve 函式
const Query = {
  todos (obj, args, ctx, info) {
    // 從 ctx 中取一些上下文資訊,如最常見的 user
    const userId = ctx.user.id

    const status = args.status
    return db.Todo.findAll({})
  }
}

// Todo 的 resolve 函式
const Todo = {
  title (obj) {
    return obj.title 
  }
}
複製程式碼
  • obj,代表該欄位所屬的 object type,如 Todo.titleobj 表示 todo
  • args,代表所傳過來的引數
  • ctx,上下文
  • info, GraphQLResolveInfo,關於本次查詢的元資訊,比如 AST,你可以對它進行解析

從這裡可以看出來:graphql 的引數都是顯式宣告,並且強型別。這一點比 REST 要好一些

# query with variables
query TODOS ($status: String) {
  done: todos (status: $status) {
    id 
    title
  }
}
複製程式碼

Mutation

graphql 能夠簡化一切的查詢,或者說它是簡化了服務端開發人員 CRUD 中的 Read。那麼,如何對資源進行修改呢?這裡就提到了 Mutation

# 在後端的 schema
schema {
  query: Query
  mutation: Mutation
}

type Mutation {
  addTodo (title: String!): Todo
}

# 在前端的 query
mutation ADD_TODO {
  addTodo (title: "學習 React") {
    id 
    title
  }
}
複製程式碼
// 以上示例返回結果
{
  addTodo: { id: 128, title: '學習React' }
}
複製程式碼

以上是一個新增 Todo 的例子,從這裡可以注意到幾點

  1. MutationQuery 同樣屬於特殊的 object type,同樣,所有關於資料的更改操作都要從 Mutation 中找起,也需要放到 schema
  2. MutationQuery 分別為 graphql 的兩大類操作,在前端進行 Mutation 查詢時,需要新增 mutation 欄位 (Query 查詢時,在前端新增 query 欄位,但這不是必選的)

input type and variables

type Mutation {
  addTodo (title: String!): Todo
}

mutation ADD_TODO {
  addTodo (title: "學習 React") {
    id 
    title
  }
}
複製程式碼

以上是一個關於新增 Todo 的 mutation,在我們新增一個 Todo 時,它僅有一個屬性: title。如果它擁有更多個屬性呢?這時,可以使用 input type,把某一資源的所有屬性聚合起來。並且配合 variables 一起使用傳遞資料

input InputTodo {
  title: String!
}

type Mutation {
  addTodo (todo: InputTodo!): Todo
}

mutation ADD_TODO ($todo: InputTodo!) {
  addTodo (todo: $todo) {
    id 
    title
  }
}
複製程式碼
// $todo 的值,在前端獲取資料時,使用 variables 傳入
{
  title: '學習 React'
}
複製程式碼

更多文章


我是山月,我會定期分享全棧文章在個人公眾號中。如果你對全棧面試,前端工程化,graphql,devops,個人伺服器運維以及微服務感興趣的話,可以關注我。如果想進群交流,可以新增我微信 shanyue94,備註加群。

如果你對全棧面試,前端工程化,graphql,devops,個人伺服器運維以及微服務感興趣的話,可以關注我

相關文章