发布于 2026-01-06 6 阅读
0

GraphQL 简介

GraphQL 简介

GraphQL 标志

什么是GraphQL?

GraphQL 是一种查询语言,最初由 Facebook 开发,于 2015 年开源。它的创建是为了解决与 RESTful 架构相关的一些问题,并提供对请求和返回的数据进行更精细的控制。

此外,GraphQL 对数据源没有要求,因此可以方便地从各种 API 检索数据,甚至可以直接公开您的 API。

我们将探讨 GraphQL 的优缺点,并创建一个简单的项目来熟悉其语法。让我们开始吧!

与 REST 的比较

理解 GraphQL 强大功能的最简单方法之一是将其与 REST 进行比较。如果您也是 REST 新手,可以查看此资源了解更多信息,但简而言之,您只需知道 REST 是一种架构范式,它提供了资源访问和提供给客户端的指导原则。它在构建 Web 应用程序方面非常流行。

REST 的流行并非毫无道理,它确实已经证明自己完全有能力驱动互联网上一些最大的网站。然而,随着网络不断发展,尤其是在移动用户呈爆炸式增长的推动下,REST 的局限性开始显现,开发者们正在寻找优化方法。

问题一:路线过多

请考虑以下情况……假设我们想要获取用户的帖子及其相关评论:

评论示例截图

在 RESTful Node.js 应用程序中,我们可以设置如下路由:

const express = require('express');
const router = express.Router();
// Middleware that will query our database and pass data along to our route handler
const dbController = require('../controllers/db');

// GET postById route
router.get('/post/:id', dbController.getPostById, (req, res) => {
  res.json({
    confirmation: 'success',
    postId: res.locals.postId,
    postBody: res.locals.body,
    userId: res.locals.userId,
    profilePicURL: res.locals.profilePicURL,
    timestamp: res.locals.timestamp 
});

为了获取评论,我们需要访问另一个接口:

// GET commentById route
router.get('/comment/:postId', dbController.getCommentsByPostId, (req, res) => {
  res.json({
    confirmation: 'success',
    comments: res.locals.comments
  });
})

假设我们要添加一条评论,就需要添加一个 POST 路由。要编辑帖子,则需要单独的路由来处理 PUT 请求。更新现有评论也是如此……由此可见,我们与应用的每一次交互都需要设置相应的路由。

这种方法虽然可行,但随着路由列表的不断增长,会变得非常繁琐。此外,如果前端团队想要更改显示的数据,他们必须请求后端团队修改 API。

而 GraphQL 则只有一个 URL 端点(通常类似于 '/graphql')。具体实现逻辑则内置于解析器函数中(稍后会详细介绍)。

一个终点统领一切

REST 与 GraphQL 端点的比较

在 REST 中处理 CRUD 操作,我们需要一个单独的端点和函数来处理访问该端点时发生的情况。而在 GraphQL 中,所有查询和变更都发送到同一个 URL,并且处理这些请求的函数是单独处理的。

问题 2:API 版本控制

公司可能面临的另一个问题是,当他们想要更改面向公众的 API 时,可能会破坏那些期望以特定方式返回数据的应用程序。解决这个问题的方法是创建 API 的多个版本,这就是为什么你会看到一些 API 被标记为 v1、v2、v3 等。但这会增加 API 维护团队的复杂性和工作量。

使用 GraphQL,可以添加更多功能而无需担心破坏性更改,因为所有现有查询仍将按预期解析,并且希望实现任何新更改的客户端仍然可以访问这些更改。

问题三:过度获取/获取不足

此外,如果您只需要现有路由提供的某一项数据,REST 中没有办法只获取所需数据而不创建自定义路由。您获取的是整个响应对象,而只使用了其中的一部分。这个问题被称为过度获取,意味着您为发送不需要的数据付出了代价,从而降低了速度。

这种情况的另一面是所谓的“数据获取不足”,即路由未能提供客户端渲染所需的所有数据,导致需要再次访问服务器。这有点像我们上面提到的示例。多次往返服务器会带来问题,因为它会引入不必要的延迟,从而导致用户体验下降。

GraphQL 通过允许客户端精确指定所需数据来解决这个问题,服务器随后可以从任何来源获取这些数据,并将其全部包含在一个响应中返回。很棒吧?

缺点

GraphQL 的缓存
虽然有很多优点,但也存在一些不足。例如,GraphQL 的缓存不像 REST 那样直接,因为它的查询缺少类似 URL 的内置全局唯一标识符来区分哪些资源被频繁访问。此外,GraphQL 与 HTTP 缓存的兼容性也不佳,因为许多实际的 GraphQL 实现仅依赖于一种类型的请求(通常是 POST 请求)。

速率限制
另一个需要考虑的问题是速率限制。对于面向公众的 API,公司通常会限制客户端在特定时间段内对特定资源的请求次数。在 REST 中,由于每个端点都可以单独限制,因此实现起来更容易。而对于 GraphQL API,公司可能需要自行制定速率限制计算方法。由于 GraphQL 请求是否会导致高成本操作取决于客户端指定所需的数据,因此很难预测,这些计算方法可能会迅速变得复杂。

性能
方面,GraphQL 的灵活性是其主要卖点之一,但也可能导致性能问题。深度嵌套的查询可能需要较长时间才能解析,这可能会给最终用户带来意想不到的延迟。因此,需要权衡利弊,例如采用多次往返服务器的方式,虽然这种方式会增加网络延迟,但可能会降低服务器的整体负载。

一个简单的例子

现在我们了解了GraphQL的一些优缺点,让我们撸起袖子来实践一下吧。我们将构建一个非常简单的图书库,并编写一些查询语句来查找一些书籍。

首先,我们创建一个项目目录并进入该目录。我们将使用npm来搭建一个 Node.js 项目(-y 参数表示接受所有默认设置)。我们还会安装三个包:expressgraphqlexpress-graphql,以设置我们的 GraphQL 服务。

mkdir graphql-example
cd graphql-example
npm init -y
npm i -S express graphql express-graphql 

让我们创建一个index.js文件来编写服务器端逻辑。首先,我们需要在 Express 中引入依赖项并启动我们的应用程序。请注意,在定义路由处理器并导入 schema 之前,我们的服务无法正常工作,我们稍后会完成这两项工作。

graphql-example/index.js
const express = require('express');
const app = express();

const { buildSchema } = require('graphql');
const graphqlExpress = require('express-graphql');

// Initialize an array where we'll store our books
const books = [];

// We'll insert our /graphql route handler here in just a second. For now, our server won't do anything interesting.

// Our server will listen on port 4000;
const PORT = 4000;
app.listen(PORT, () => {
  console.log(`Listening on port ${PORT}`);
});

我之前提到过我们的应用目前功能还不完善。我们来解决这个问题。创建一个名为 bookSchema.js 的新文件。在这个文件中,我们将导出一个字符串,列出我们的类型。但首先,我们需要讨论一下如何编写 GraphQL schema。

类型

在 GraphQL 中,我们将类型定义为可以从服务中获取的任何对象。对于我们的图书馆应用,我们可以像这样定义一个图书类型:

例如,书籍类型定义
type Book {
  title: String!
  author: String!
}

GraphQL schema 有三种不同的表示方式,但为了方便起见,我们将采用最易于读写的一种:模式定义语言(Schema Definition Language,简称 SDL)。上面的代码片段就是一个 SDL 的示例。如果您对其他表示方式感兴趣,请参阅这篇文章。

GraphQL 中有两种特殊类型:查询 (query)变更 (mutation )。每个 GraphQL 服务都会有一个查询类型,因为 GraphQL 需要一个入口点来处理接收到的每个请求。变更,顾名思义,用于处理我们如何更改(或修改)数据。

所以,在我们新建的 bookSchema 文件中,让我们添加以下代码:

graphql-example/bookSchema.js
module.exports.types = `
type Query {
  greeting: String
  books: [Book]
}

type Book {
  id: Int!
  title: String!
  author: String!
}
`

这里我们定义了根对象 Query 和一个 Book 对象。Query 对象有两个字段:greeting(返回一个字符串)和books(返回一个 Book 对象列表)。每个 Book 对象本身包含三个必填字段(即不能为 null),用感叹号 (*) 表示。

解析器

GraphQL 类型告诉我们数据的结构以及客户端可以发送哪些类型的查询。而如何实际返回这些数据则由与每种类型对应的特定函数(称为解析器)来处理。解析器的作用是使用它们返回的值来解析查询和变更操作。

让我们回到index.js文件,引入类型定义并编写一些解析器。

  • 使用解构赋值从 bookSchema.js 中引入类型字符串。
  • 接下来,就在我们声明空 books 数组的正下方,声明一个名为resolvers 的常量,它将是一个包含两个键的对象,每个键都有自己的函数。
  • 然后,按照之前注释中指定的路径,为我们的“/graphql”端点创建一个路由处理程序。这里我们将使用 graphqlExpress 包。
  • 最后,创建一个名为schema的变量,并调用graphql 库提供的buildSchema方法,传入我们刚刚导入的类型字符串。

我们的索引文件现在应该如下所示:

graphql-example/index.js
const express = require('express');
const app = express();

const { buildSchema } = require('graphql');
const graphqlExpress = require('express-graphql');
const { types } = require('./bookSchema');

// Initialize an array where we'll store our books
const books = [];
const resolvers = {
  greeting: () => 'Hello world!',
  books: () => books
}

const schema = buildSchema(types);
app.use('/graphql', 
  graphqlExpress({
    schema,
    rootValue: resolvers,
    graphiql: true
  })
);

// Our server will listen on port 4000;
const PORT = 4000;
app.listen(PORT, () => {
  console.log(`Listening on port ${PORT}`);
});

现在终于到了编写第一个 GraphQL 查询的时候了。在终端中执行`node index.js`命令启动服务器。如果没有错误,它应该会输出以下日志:

Listening on port 4000

现在打开浏览器,访问localhost:4000/graphql。我们应该会立即看到 GraphiQL IDE 加载完毕。

GraphiQL IDE

删除所有注释文本,并编写一个查询来检索我们的问候语(参见下方屏幕截图)。点击播放按钮(或按 Ctrl + Enter),我们应该会收到响应:

问候回复

太棒了!我们刚刚编写了第一个 GraphQL 查询!这个示例目前还缺少一些功能,所以接下来我们添加一个 Mutation 类型,以便与我们的模拟库 API 进行交互。

打开 bookSchema.js 文件,并在type Book代码块之后添加以下字符串:

graphql-example/bookSchema.js
type Mutation {
  addBook ( id: Int!, title: String!, author: String! ): [Book]
}

在这里,我们定义了根 Mutation,并为其添加了一个 addBook 字段,该字段有三个必需参数,并返回一个 Book 对象数组。

为了让我们的 addBook 操作具备一些功能,我们需要创建一个相应的解析器函数。返回index.js 文件,并按如下方式更新解析器对象:

const resolvers = {
  greeting: () => 'Hello world!',
  books: () => books,
  addBook: args => {
    const newBook = {
      id: args.id,
      title: args.title,
      author: args.author
    };

    books.push(newBook);
    return books;
  }
}

好的,这里我们得到了第一个解析器,它接收一个参数,这个参数被巧妙地命名为args。实际上,所有解析器函数都会接收四个参数作为输入。它们通常被命名为:

  • parent/root - 前一个(或父级)解析器执行的结果。由于 GraphQL 允许嵌套查询(就像嵌套对象一样),parent参数使我们能够访问前一个解析器函数的返回值。
  • args - 这些是传递给 GraphQL 查询字段的参数。在本例中,args 将是我们要添加的新书的idtitleauthor 。
  • context - 一个在解析器链中传递的对象,每个解析器都可以对其进行写入和读取(基本上是解析器之间进行通信和共享信息的一种方式)。
  • info值包含与当前查询相关的字段特定信息以及架构详细信息。点击此处了解更多信息。

然而,由于我们之前的两个解析器(greeting 和 books)都相当简单,不需要访问这四个参数提供的任何内容,所以我们直接省略了它们。

我们来测试一下 addBook 功能。再次启动服务器并打开浏览器。然后执行以下 mutation:添加书突变

很棒吧?我们刚刚向之前为空的 books 数组中添加了一本新书。我们可以通过在 GraphiQL 中执行以下查询来验证这一点:

{
  books {
    id
    title
    author
  }
}

如果运行另一个 addBook mutation,这次使用不同的 id、title 和 author,并再次执行 books 查询,我们应该会看到 books 数组增长到两个对象。

在结束之前,我们再添加一个功能。回到 bookSchema.js 文件,在 Mutation 代码块中添加一个名为deleteBook 的字段。现在我们的文件应该看起来像这样:

graphql-example/bookSchema.js
module.exports.types = `
type Query {
  greeting: String
  books: [Book]
}

type Book {
  id: Int!
  title: String!
  author: String!
}

type Mutation {
  addBook ( id: Int!, title: String!, author: String! ): [Book]
  deleteBook ( id: Int, title: String ): [Book]
}
`

并将以下函数定义添加到解析器对象中:

graphql-example/index.js
  deleteBook: args => {
    if (args.id) {
      books.forEach( (book, index) => {
        if (book.id === args.id) {
          books.splice(index, 1);
        }
      });
    } 
    if (args.title) {
      books.forEach( (book, index) => {
        if (book.title === args.title) {
          books.splice(index, 1);
        } 
      });
    } 
    return books;
  }

当我们调用 deleteBook 操作时,我们会传入要删除的书籍的 id 或 title 属性。deleteBook 解析器会遍历整个数组,找到属性值与参数匹配的对象,并将其从数组中移除,最终返回修改后的 books 数组。

以下是两个文件最终应有的样子:

graphql-example/index.js
const express = require('express');
const app = express();

const { buildSchema } = require('graphql');
const graphqlExpress = require('express-graphql');
const { types } = require('./bookSchema');

// Initialize an array where we'll store our books
const books = [];
const resolvers = {
  greeting: () => 'Hello world!',
  books: () => books,
  addBook: args => {
    const newBook = {
      id: args.id,
      title: args.title,
      author: args.author
    };

    books.push(newBook);
    return books;
  },
  deleteBook: args => {
    if (args.id) {
      books.forEach( (book, index) => {
        if (book.id === args.id) {
          books.splice(index,1);
        }
      });
    } 
    if (args.title) {
      books.forEach( (book, index) => {
        if (book.title === args.title) {
          books.splice(index, 1);
        } 
      });
    } 
    return books;
  }

}

const schema = buildSchema(types);
app.use('/graphql',
  graphqlExpress({
    schema,
    rootValue: resolvers,
    graphiql: true
  })
);

// Our server will listen on port 4000;
const PORT = 4000;
app.listen(PORT, () => {
  console.log(`Listening on port ${PORT}`);
});
graphql-example/bookSchema.js
module.exports.types = `
type Query {
  greeting: String
  books: [Book]
}

type Book {
  id: Int!
  title: String!
  author: String!
}

type Mutation {
  addBook ( id: Int!, title: String!, author: String! ): [Book]
  deleteBook ( id: Int, title: String ): [Book]
}
`

最后,我们将在 GraphiQL 中进行测试。重启服务器,并运行两次 addBook mutation,每次都更改值。使用books查询验证数组中是否存在两本不同的书。

图书查询

现在我们可以调用deleteBook函数,并传入其中一本书的标题或 ID。如果一切顺利,匹配的书应该会从数组中移除,只留下另一本书。

删除书

如果成功了,恭喜!我们现在开始看到如何在通常需要构建 RESTful API 的地方实现 GraphQL。

正如我之前提到的,使用 GraphQL 的好处之一就是你可以精确地指定要返回的数据。例如,如果我们只需要返回标题,而不需要ID作者,那么我们只需要在客户端调整查询/变更,🔥砰🔥,就能以我们想要的方式获取数据。

仅返回标题
GraphQL 让我们能够对数据结构进行细粒度的控制,而无需更改后端 API。

相比之下,REST 则不然,每次更改我们都必须调整后端 API(而且在此过程中还有可能破坏一些下游应用程序)。真是太强大了!

概要

添加和删​​除功能已经实现,我们已经完成了基本 CRUD 应用的一半。为了进一步练习,您可以尝试自行添加 `getBookById` 和 `updateBook` 的功能。需要注意的是,我们的书籍目前仅保存在内存中,因此每次重启服务器后都会被清除。为了使更改持久化,我们需要将应用连接到数据库,这超出了本文的讨论范围,但我建议您也尝试实现一下。

以上就是GraphQL的简要介绍。显然,GraphQL的内容远不止我们在这里能够提及的,但希望这个演示足以激发您的兴趣。

如果你想继续学习 GraphQL,那你真是太幸运了。社区已经创建了一些非常棒的资源。以下是我推荐的一些资源:

如果您有任何问题或想法,请在下方留言。祝您编程愉快!

文章来源:https://dev.to/mychal/a-brief-tour-of-graphql-4lcg