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,我的服务器运维以及微服务感兴趣的话,能够关注我
相关文章
相关标签/搜索