GraphQL 入門簡介

此篇文章介紹 graphql 基礎知識,會過於無聊。若是你想快速上手,可使用個人腳手架 shfshanyue/apollo-server-starter。若是你想用它寫一個前端,能夠參考個人 shfshanyue/shicijavascript

本文地址: graphql schema and query前端

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

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

schema and query

咱們先抽出 schema 的代碼部分,這裏有 GraphQLSchemaGraphQLObjectTypeGraphQLString 等諸多 API。git

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 做爲文件名後綴,用法以下github

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

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

以及查詢結果web

{
  hello: 'hello, world'
}
複製代碼

在前端中全部查詢的 gql 每每會經過 graphql/gql 後綴的文件來統一維護,這裏有一份代碼示例: shfshanyue/shici:query.gql面試

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

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

Object Type and Field

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

這裏來一個更復雜的 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,我的服務器運維以及微服務感興趣的話,能夠關注我
相關文章
相關標籤/搜索