GraphQL 简介

GraphQL 是一种开源查询语言、服务器端运行时(执行引擎)和规范(技术标准)。但这意味着什么?它是什么?GraphQL 是一种查询语言,这是 GraphQL 中“QL”部分的含义。具体来说,它是一种客户端查询语言。但再说一次,这意味着什么?以下示例将消除你对 GraphQL 查询的任何疑问:

{
  planet(name: "earth") {
    id
    age
    population
  }
}

像前面这样的 GraphQL 查询在 HTTP 客户端(如 Nuxt 或 Vue)中用于将查询发送到服务器,以换取 JSON 响应,如下所示:

{
  "data": {
    "planet": {
      "id": 3,
      "age": "4543000000",
      "population": "7594000000"
    }
  }
}

正如你所见,你获得了所请求字段(age 和 population)的特定数据,仅此而已。这就是使 GraphQL 与众不同的地方,并赋予客户端精确请求他们想要的数据的能力。这很酷也很令人兴奋,不是吗?但是,服务器中返回 GraphQL 响应的是什么呢?是一个 GraphQL API 服务器(服务器端运行时)。

客户端通过 HTTP 端点使用 POST 方法将 GraphQL 查询作为字符串发送到 GraphQL API 服务器。服务器提取并处理查询字符串。然后,就像任何典型的 API 服务器一样,GraphQL API 将从数据库或其他服务/API 获取数据,并以 JSON 响应的形式将其返回给客户端。

那么,我们可以使用像 Express 这样的服务器作为 GraphQL API 服务器吗?答案是“是”和“否”。所有合格的 GraphQL 服务器都必须实现 GraphQL 规范中指定的两个核心组件,这两个组件用于验证、处理和返回数据:模式和解析器。

GraphQL 模式是类型定义的集合,这些类型定义由客户端可以请求的对象以及对象拥有的字段组成。另一方面,GraphQL 解析器是附加到字段的函数,当客户端进行查询或变更时,这些函数会返回值。例如,以下是查找行星的类型定义:

type Planet {
  id: Int
  name: String
  age: String
  population: String
}

type Query {
  planet(name: String): Planet
}

在这里,你可以看到 GraphQL 使用强类型模式——每个字段都必须定义一个类型,该类型可以是标量类型(可以是整数、布尔值或字符串的单个值)或对象类型。Planet 和 Query 类型是对象类型,而 String 和 Int 是标量类型。对象类型中的每个字段都必须使用一个函数来解析,如下所示:

Planet: {
  id: (root, args, context, info) => root.id,
  name: (root, args, context, info) => root.name,
  age: (root, args, context, info) => root.age,
  population: (root, args, context, info) => root.population,
}

Query: {
  planet: (root, args, context, info) => {
    return planets.find(planet => planet.name === args.name)
  },
}

前面的示例是用 JavaScript 编写的,但是只要你遵循并实现 https://spec.graphql.org/ 上 GraphQL 规范中概述的内容,就可以使用任何编程语言编写 GraphQL 服务器。以下是一些不同语言的 GraphQL 实现示例:

  • GraphQL.js (JavaScript):https://github.com/graphql/graphql-js

  • graphql-php (PHP):https://github.com/webonyx/graphql-php

  • Graphene (Python):https://github.com/graphql-python/graphene

  • GraphQL Ruby (Ruby):https://github.com/rmosolgo/graphql-ruby

只要你符合 GraphQL 规范,就可以自由创建新的实现,但本书中我们只会使用 GraphQL.js。现在,你可能有一些更深层次的问题——查询类型到底是什么?我们知道它是一个对象类型,但是为什么我们需要它?我们的模式中需要包含它吗?简短的答案是肯定的。

我们将在下一节中更详细地讨论这个问题,并找出无论如何都需要它的原因。我们还将了解如何使用 Express 作为 GraphQL API 服务器。所以,请继续阅读。

理解 GraphQL Schema 和 Resolvers

我们在上一节中讨论的用于查找行星的示例模式和解析器假定我们使用 GraphQL 模式语言,这有助于我们创建 GraphQL 服务器所需的 GraphQL 模式。我们可以使用来自名为 GraphQL Tools 的 Node.js 包的 makeExecutableSchema 函数,轻松地从 GraphQL 模式语言创建 GraphQL.js 的 GraphQLSchema 实例。

你可以在 https://www.graphql-tools.com/https://github.com/ardatan/graphql-tools 找到有关此包的更多信息。

GraphQL 模式语言是一种 “快捷方式” ——一种用于构建你的 GraphQL 模式及其类型系统的简写符号。在使用这种简写符号之前,我们应该先了解如何从 GraphQL.js 的底层对象和函数(如 GraphQLObjectType、GraphQLString、GraphQLList 等)构建 GraphQL 模式,GraphQL.js 实现了 GraphQL 规范。让我们安装这些包并使用 Express 创建一个简单的 GraphQL API 服务器:

  1. 通过 npm 安装 Express、GraphQL.js 和 GraphQL HTTP Server Middleware:

    $ npm i express
    $ npm i express-graphql
    $ npm i graphql

    GraphQL HTTP Server Middleware 是一种中间件,它允许我们使用任何实现了 Connect 支持中间件方式的 HTTP Web 框架(如 Express、Restify 和 Connect 本身)创建 GraphQL HTTP 服务器。

    有关这些包的更多信息,请访问以下链接:

  2. 在项目的根目录中创建一个 index.js 文件,并使用 require 方法导入 express、express-graphql 和 graphql:

    // index.js
    const express = require('express')
    const graphqlHTTP = require('express-graphql')
    const graphql = require('graphql')
    const app = express()
    const port = process.env.PORT || 4000
  3. 创建一个包含行星列表的虚拟数据:

    // index.js
    const planets = [
      { id: 3, name: "earth", age: 4543000000, population:
        7594000000 },
      { id: 4, name: "mars", age: 4603000000, population: 0 },
    ]
  4. 定义 Planet 对象类型以及客户端可以查询的字段:

    // index.js
    const planetType = new graphql.GraphQLObjectType({
      name: 'Planet',
      fields: {
        id: { ... },
        name: { ... },
        age: { ... },
        population: { ... },
      }
    })

    请注意,按照约定,在 GraphQL 模式创建的 name 字段中,对象类型的首字母要大写。

  5. 定义各种类型以及你希望如何解析每个字段的值:

    // index.js
    id: {
      type: graphql.GraphQLInt,
      resolve: (root, orgs, context, info) => root.id,
    },
    name: {
      type: graphql.GraphQLString,
      resolve: (root, orgs, context, info) => root.name,
    },
    age: {
      type: graphql.GraphQLString,
      resolve: (root, orgs, context, info) => root.age,
    },
    population: {
      type: graphql.GraphQLString,
      resolve: (root, orgs, context, info) => root.population,
    },

    请注意,每个解析器函数都接受以下四个参数:

    • root: 从父对象类型(在步骤 6 中是 Query)解析出的对象或值。

    • args: 字段可以接收的参数(如果已设置)。请参阅步骤 8。

    • context: 一个可变的 JavaScript 对象,用于保存跨所有解析器共享的顶层数据。在使用 Express 时,默认情况下它是 Node.js HTTP 请求对象 (IncomingMessage)。我们可以修改此 context 对象并添加我们想要共享的通用数据,例如身份验证和数据库连接。请参阅步骤 10。

    • info: 一个 JavaScript 对象,其中包含有关当前字段的信息,例如其字段名称、返回类型、父类型(在本例中是 Planet)和一般模式详细信息。

    如果当前字段的值解析不需要这些参数,我们可以省略它们。

  6. 定义 Query 对象类型以及客户端可以查询的字段:

    // index.js
    const queryType = new graphql.GraphQLObjectType({
      name: 'Query',
      fields: {
        hello: { /* ... */ },
        planet: { /* ... */ },
      },
    })
  7. 定义 hello 字段的类型并解析如何返回值:

    // index.js
    hello: {
      type: graphql.GraphQLString,
      resolve: (root, args, context, info) => 'world',
    }
  8. 定义 planet 字段的类型并解析如何返回值:

    // index.js
    planet: {
      type: planetType,
      args: {
        name: { type: graphql.GraphQLString }
      },
      resolve: (root, args, context, info) => {
        return planets.find(planet => planet.name === args.name)
      },
    }

    请注意,我们将创建并存储在 planetType 变量中的 Planet 对象类型传递给了 Query 对象类型中的 planet 字段,以便它们之间可以建立关系。

  9. 使用必需的 query 字段以及您刚刚定义的包含字段、类型、参数和解析器的 Query 对象类型,构造一个 GraphQL schema 实例,如下所示:

    // index.js
    const schema = new graphql.GraphQLSchema({ query: queryType })

    请注意,必须提供 query 键作为 GraphQL 查询的根类型,这样我们的查询才能向下链接到 Planet 对象类型中的字段。我们可以说 Planet 对象类型是 Query 对象类型(根类型)的子类型或子级,它们的关系必须在父对象 (Query) 中使用 planet 字段的 type 字段来建立。

  10. 使用 GraphQL HTTP Server Middleware 作为中间件,并将 GraphQL schema 实例传递给它,以在 Express 允许的 /graphiql 端点上建立 GraphQL 服务器,如下所示:

    // index.js
    app.use('/graphiql', graphqlHTTP({ schema, graphiql: true }),)

    建议将 graphiql 选项设置为 true,这样当在浏览器中加载 GraphQL 端点时,我们可以使用 GraphQL IDE。在这个顶层,您还可以使用 graphqlHTTP 中间件内的 context 选项来修改 GraphQL API 的上下文,如下所示:

    context: {
      something: 'something to be shared',
    }

    通过这样做,您可以从任何解析器访问这个顶层数据。这非常有用,不是吗?

  11. 最后,在所有数据加载完毕后,在终端中使用 node index.js 命令启动服务器,在 index.js 文件中添加以下行:

    // index.js
    app.listen(port)
  12. 在浏览器中访问 localhost:4000/graphiql。您应该会看到 GraphQL IDE,这是一个可以测试您的 GraphQL API 的用户界面。因此,在左侧的输入区域中键入以下查询:

    # localhost:4000/graphiql
    {
      hello
      planet (name: "earth") {
        id
        age
        population
      }
    }

    当您点击播放按钮时,您应该会在右侧看到前面的 GraphQL 查询已转换为一个 JSON 对象:

    // localhost:4000/graphiql
    {
      "data": {
        "hello": "world",
        "planet": {
          "id": 3,
          "age": "4543000000",
          "population": "7594000000"
        }
      }
    }

做得好!您已经成功地使用低级方法通过 Express 创建了一个基本的 GraphQL API 服务器!我们希望这能让您全面了解如何使用 GraphQL schema 和解析器创建 GraphQL API 服务器。我们还希望您能理解 GraphQL 中这两个核心组件之间的关系,并且我们已经回答了您的问题;也就是说,Query 类型到底是什么?为什么我们需要它?我们的 schema 中是否需要它?答案是肯定的,query(对象)类型是一个根对象类型(通常称为根 Query 类型),在创建 GraphQL schema 时必须提供它。

但是您可能仍然有一些疑问和抱怨,特别是关于解析器——您肯定会觉得在步骤 5 中为 Planet 对象类型中的字段定义解析器是乏味且愚蠢的,因为它们除了返回从查询对象解析的值之外什么也不做。有没有办法避免这种痛苦的重复呢?答案是肯定的:您不必为 schema 中的每个字段都指定它们,这在于默认解析器。但是我们如何做到这一点呢?我们将在下一节中找到答案。

您可以在本书 GitHub 存储库的 /chapter-18/graphqlapi/graphql-express/ 中找到此示例和其他示例。

理解 GraphQL 默认 Resolvers

当没有为字段指定解析器时,默认情况下,该字段将采用父级解析的对象中的属性值——也就是说,如果该对象具有与字段名称匹配的属性名称。因此,Planet 对象类型中的字段可以重构如下:

fields: {
  id: { type: graphql.GraphQLInt },
  name: { type: graphql.GraphQLString },
  age: { type: graphql.GraphQLString },
  population: { type: graphql.GraphQLString },
}

这些字段的值将在底层回退到父级(查询类型)解析的对象中的属性,如下所示:

root.id
root.name
root.age
root.population

反过来说,当为一个字段显式指定解析器时,即使父级的解析器为该字段返回任何值,该解析器也始终会被使用。例如,让我们为 Planet 对象类型中的 id 字段显式指定一个值,如下所示:

fields: {
  id: {
    type: graphql.GraphQLInt,
    resolve: (root, orgs, context, info) => 2,
  },
}

我们已经知道地球和火星的默认 ID 值分别为 3 和 4,并且它们由父级(查询类型)解析,如前一节的步骤 8 所示。但是这些解析后的值永远不会被使用,因为它们被 ID 解析器中的值覆盖了。因此,让我们查询地球或火星,如下所示:

{
  planet (name: "mars") {
    id
  }
}

在这种情况下,你将始终在 JSON 响应中得到 2:

{
  "data": {
    "planet": {
      "id": 2
    }
  }
}

这非常聪明,不是吗?如果你的对象类型中有大量的字段,它可以避免我们进行痛苦的重复。然而,到目前为止,我们一直在遵循最痛苦的方式,通过使用 GraphQL.js 来构建我们的模式。这是因为我们想看到并理解如何从底层类型创建 GraphQL 模式。在现实生活中,尤其是在大型项目中,我们可能不想走这条漫长而曲折的道路。相反,我们应该更喜欢使用 GraphQL 模式语言来为我们构建模式和解析器。在下一节中,我们将向你展示如何使用 GraphQL 模式语言和 Apollo Server(作为 GraphQL HTTP Server Middleware 的替代方案)轻松创建 GraphQL API 服务器。所以,请继续阅读!

使用 Apollo Server 创建 GraphQL API

Apollo Server 是 Apollo 平台开发的开源且符合 GraphQL 规范的服务器,用于构建 GraphQL API。我们可以独立使用它,也可以与其他 Node.js Web 框架(如 Express、Koa、Hapi 等)一起使用。本书中我们将直接使用 Apollo Server,但如果你想将其与其他框架一起使用,请访问 https://github.com/apollographql/apollo-serverinstallation-integrations。

在这个 GraphQL API 中,我们将创建一个服务器,该服务器按标题和作者查询书籍集合。让我们开始吧:

  1. 通过 npm 将 Apollo Server 和 GraphQL.js 安装为项目依赖项:

    $ npm i apollo-server
    $ npm i graphql
  2. 在项目根目录中创建一个 index.js 文件,并从 apollo-server 包中导入 ApolloServer 和 gql 函数:

    // index.js
    const { ApolloServer, gql } = require('apollo-server')

    gql 函数用于通过使用模板字面量标签(或带标签的模板字面量)包装 GraphQL 操作和模式语言来解析它们。有关模板字面量和带标签的模板的更多信息,请访问 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals。

  3. 创建以下静态数据,其中包含作者和帖子列表:

    // index.js
    const authors = [
      { id: 1, name: 'author A' },
      { id: 2, name: 'author B' },
    ]
    const posts = [
      { id: 1, title: 'Post 1', authorId: 1 },
      { id: 2, title: 'Post 2', authorId: 1 },
      { id: 3, title: 'Post 3', authorId: 2 },
    ]
  4. 定义 Author、Post 和 Query 对象类型,以及客户端可以查询的字段:

    // index.js
    const typeDefs = gql`
      type Author {
        id: Int
        name: String
      }
      type Post {
        id: Int
        title: String
        author: Author
      }
      type Query {
        posts: [Post]
      }
    `

    请注意,我们可以将 AuthorPostQuery 对象类型简称为 Author 类型、Post 类型和 Query 类型。这样比使用“对象类型”来描述它们更清晰,因为它们本质上就是对象类型。请记住,除了本质上是对象类型之外,Query 类型也是 GraphQL 模式创建中的根类型。

    请注意我们如何建立 AuthorPost 以及 PostQuery 的关系——author 字段的类型是 Author 类型。Author 类型对其字段(idname)使用简单的标量类型,而 Post 类型对其字段使用简单的标量类型(idtitle)和 Author 类型 (author)。Query 类型对其唯一的字段 posts 使用 Post 类型,但 posts 是一个帖子列表,因此我们必须使用类型修饰符将 Post 类型包装在方括号中,以表明 posts 字段将解析为一个 Post 对象数组。

    有关类型修饰符的更多信息,请访问 https://graphql.org/learn/schema/lists-and-non-null

  5. 定义解析器以指定你希望如何解析 Query 类型中的 posts 字段和 Post 类型中的 author 字段的值:

    // index.js
    const resolvers = {
      Query: {
        posts: (root, args, context, info) => posts
      },
      Post: {
        author: root => authors.find(author => author.id ===
          root.authorId)
      },
    }

    请注意 GraphQL 模式语言如何帮助我们将解析器与对象类型分离,并且它们只是在一个简单的 JavaScript 对象中定义的。只要我们的解析器的属性名称与类型定义中的字段名称相匹配,JavaScript 对象中的解析器就会“神奇地”与对象类型连接起来。因此,这个 JavaScript 对象被称为解析器映射。

    在定义解析器之前,我们还必须在解析器映射中定义顶层属性名称(Query、Post),以便它们与类型定义中的对象类型(Author、Post、Query)相匹配。但是,我们不需要在此解析器映射中为 Author 类型定义任何特定的解析器,因为 Author 类型中字段(id、name)的值由默认解析器自动解析。

    另一个需要注意的点是,Post 类型中字段(id、title)的值也由默认解析器解析。如果你不喜欢使用属性名称来定义解析器,你可以使用解析器函数,只要函数名称与类型定义中的字段名称相对应。例如,author 字段的解析器可以重写如下:

    Post: {
      author (root) {
        return authors.find(author => author.id === root.authorId)
      },
    }
  6. 使用类型定义和解析器从 ApolloServer 构造一个 GraphQL 模式实例。然后,启动服务器,如下所示:

    // index.js
    const server = new ApolloServer({ typeDefs, resolvers })
    server.listen().then(({ url }) => {console.log(`Server ready at ${url}`)
    })
  7. 在终端上使用 node 命令启动你的 GraphQL API:

    $ node index.js
  8. 将你的浏览器指向 localhost:4000。你应该看到 GraphQL Playground 加载在你的屏幕上。从那里,你可以测试你的 GraphQL API。因此,在左侧的输入区域中键入以下查询:

    {
      posts {
        title
        author {
          name
        }
      }
    }

    当你点击播放按钮时,你应该看到上面的 GraphQL 查询已在右侧替换为一个 JSON 对象:

    {
      "data": {
        "posts": [
          {
            "title": "Post 1",
            "author": {
              "name": "author A"
            }
          },
          ...
        ]
      }
    }

这非常漂亮和精彩,不是吗?这就是我们使用 GraphQL 模式语言和 Apollo Server 轻松构建 GraphQL API 的方式。在采用简写方法之前,了解如何创建 GraphQL 模式和解析器的漫长而痛苦的方法是值得的。一旦你掌握了这个基本的具体知识,你应该能够轻松地查询你使用 Keystone 存储的数据。本书中我们只介绍了一些 GraphQL 的类型,包括标量类型、对象类型、查询类型和类型修饰符。还有一些其他类型你应该了解,例如变更类型、枚举类型、联合和输入类型以及接口。请在 https://graphql.org/learn/schema/ 上查看它们。

如果你想了解更多关于 GraphQL 的信息,请访问 https://graphql.org/learn/。有关 Apollo Server 的更多信息,请访问 https://www.apollographql.com/docs/apollo-server/。

你可以在本书 GitHub 存储库的 /chapter-18/graphql-api/graphql-apollo/ 中找到本节中使用的代码以及其他示例 GraphQL 类型定义。

现在,让我们学习如何使用 Keystone GraphQL API。

使用 Keystone GraphQL API

Keystone GraphQL API 的 GraphQL Playground 位于 localhost:4000/admin/graphiql。在这里,我们可以测试通过 localhost:4000/admin 上的 Keystone 管理 UI 创建的列表。Keystone 会为每个创建的列表自动生成四个顶级的 GraphQL 查询。例如,对于我们在上一节中创建的 Page 列表,我们将获得以下查询:

  • allPages

    此查询可用于获取 Page 列表中的所有项目。我们还可以搜索、限制和过滤结果,如下所示:

    {
      allPages (orderBy: "name_DESC", skip: 0, first: 6) {
        title
        content
      }
    }
  • _allPagesMeta

    此查询可用于获取 Page 列表中所有项目的元信息,例如所有匹配项的总计数,这对于分页非常有用。我们还可以搜索、限制和过滤结果,如下所示:

    {
      _allPagesMeta (search: "a") {
        count
      }
    }
  • Page

    此查询可用于从 Page 列表中获取单个项目。我们只能使用带有 id 键的 where 参数来获取页面,如下所示:

    {
      Page (where: { id: $id }) {
        title
        content
      }
    }
  • _PagesMeta

    此查询可用于获取 Page 列表本身的元信息,例如其名称、访问权限、模式和字段,如下所示:

    {
      _PagesMeta {
        name
        access {
          read
        }
        schema {
          queries
          fields {
            name
          }
        }
      }
    }

正如你所见,这四个查询以及过滤、限制和排序参数为我们提供了足够的能力来获取我们需要的特定数据,仅此而已。更重要的是,在 GraphQL 中,我们可以通过单个请求获取多个资源,如下所示:

{
  _allPagesMeta {
    count
  },
  allPages (orderBy: "name_DESC", skip: 0, first: 6) {
    title
    content
  }
}

这非常棒而且有趣,不是吗?在 REST API 中,你可能需要向多个 API 端点发送多个请求才能获取多个资源。GraphQL 为我们提供了一种替代方案,以解决这个困扰前端和后端开发人员的 REST API 的臭名昭著的问题。

请注意,这四个顶级查询也适用于我们创建的其他列表,包括 Project、Image 和 NavLink。

有关这四个顶级查询以及过滤、限制和排序参数,以及本书未涵盖的 GraphQL 变更和执行步骤的更多信息,请访问 https://www.keystonejs.com/guides/intro-to-graphql/。

如果你想了解如何查询 GraphQL 服务器,请访问 https://graphql.org/learn/queries/。

现在你已经掌握了 GraphQL 的基本知识并了解了 Keystone 的顶级 GraphQL 查询,是时候学习如何在 Nuxt 应用程序中使用它们了。