阅读 1400

GraphQL 入门简介

此篇文章介绍 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,个人服务器运维以及微服务感兴趣的话,可以关注我

关注下面的标签,发现更多相似文章
评论