描述 GraphQL API

了解如何使用 Mock Service Worker 描述 GraphQL API。

先决条件

¥Prerequisites

GraphQL 客户端

¥GraphQL client

尽管这不是强制性的,但我们强烈建议使用 GraphQL 客户端向(模拟的)GraphQL API 发出请求。拥有 GraphQL 客户端对本教程没有影响,但会产生可用于生产的代码,从而带来更好的开发者体验和规范合规性。

¥Although it’s not mandatory, we highly recommend using a GraphQL client to make requests to the (mocked) GraphQL API. Having a GraphQL client has no effect on this tutorial but instead results in a production-ready code which, in return, yields a much better developer experience and specification-compliance.

以下是一些流行的 GraphQL 客户端可供选择:

¥Here are some of the popular GraphQL clients to choose from:

当然,你可以使用纯 window.fetch 或任何其他 HTTP 客户端发出 GraphQL 请求。如果你决定这样做,请确保遵循 GraphQL over HTTP 规范。MSW 在区分 GraphQL 请求和不相关的 HTTP 请求时会考虑该规范。

¥You can, of course, make GraphQL requests using plain window.fetch or any other HTTP client. If you decide to do so, make sure you follow the GraphQL over HTTP specification. MSW takes that specification into account when distinguishing between GraphQL requests and unrelated HTTP requests.

导入

¥Import

MSW 提供指定的 graphql 命名空间来描述 GraphQL 操作。我们将使用该命名空间来描述要拦截的操作以及如何响应它们。

¥MSW provides a designated graphql namespace for describing GraphQL operations. We will use that namespace to describe what operations to intercept and how to respond to them.

Import the graphql namespace from the msw package:

// src/mocks/handlers.js
import { graphql } from 'msw'
 
export const handlers = []

请求处理程序

¥Request handler

接下来,我们将创建一个 请求处理程序graphql 命名空间上的方法允许我们创建请求处理程序来拦截相应类型的 GraphQL 操作,例如用于查询的 graphql.query() 或用于突变的 graphql.mutation()

¥Next, we will create a Request handler. Methods on the graphql namespace allow us to create request handlers to intercept GraphQL operations of corresponding types, like graphql.query() for queries or graphql.mutation() for mutations.

graphql[operationType](operationName, resolver)

请求处理程序是允许你 拦截请求模拟响应 的函数。

¥Request handlers are functions that allow you to Intercept requests and Mock responses.

在本教程中,我们将描述一个涵盖以下操作的基本 GraphQL API:

¥In this tutorial, we will describe a basic GraphQL API that covers the following operations:

  • query ListPosts,返回所有现有帖子;

    ¥query ListPosts, to return all existing posts;

  • mutation CreatePost,创建新帖子;

    ¥mutation CreatePost, to create a new post;

  • mutation DeletePost,按 ID 删除帖子。

    ¥mutation DeletePost, to delete a post by ID.

让我们从为 ListPosts 查询创建一个请求处理程序开始。

¥Let’s start by creating a request handler for the ListPosts query.

Call graphql.query() to declare your first request handler

// src/mocks/handlers.js
import { graphql } from 'msw'
 
export const handlers = [
  graphql.query('ListPosts', ({ query }) => {
    console.log('Intercepted a "ListPosts" GraphQL query:', query)
  }),
]

请注意,我们在描述此操作时没有指定确切的服务器端点。默认情况下,MSW 将拦截所有匹配的 GraphQL 操作,无论其目的地如何。你可以使用 graphql.link() API 选择加入基于端点的 GraphQL 拦截。如果你的应用同时与多个 GraphQL 服务器通信,这会很方便。在本教程中,我们将描述一个 GraphQL 服务器,因此我们不需要该 API。

¥Notice that we don’t specify the exact server endpoint when describing this operation. By default, MSW will intercept all matching GraphQL operations regardless of their destination. You can opt-in into the endpoint-based GraphQL interception by using the graphql.link() API. That can be handy if your application communicates with multiple GraphQL servers at the same time. In this tutorial, we will describe a single GraphQL server so we don’t need that API.

按照相同的示例,为其余操作添加请求处理程序:

¥Following the same example, add request handlers for the remaining operations:

// src/mocks/handlers.js
import { graphql } from 'msw'
 
export const handlers = [
  graphql.query('ListPosts', ({ query }) => {
    console.log('Intercepted a "ListPosts" GraphQL query:', query)
  }),
  graphql.mutation('CreatePost', ({ query, variables }) => {
    console.log(
      'Intercepted a "CreatePost" GraphQL mutation:',
      query,
      variables
    )
  }),
  graphql.mutation('DeletePost', ({ query, variables }) => {
    console.log('Intercepted a "DeletePost" GraphQL mutation', query, variables)
  }),
]

响应解析器

¥Response resolver

响应解析器是请求处理程序的第二个参数,它决定如何处理被拦截的请求。你可以使用这样的请求执行多种操作:使用模拟响应进行响应、按原样执行、执行代理请求并增强原始响应等。你始终可以在此页面上了解有关响应解析器的更多信息:

¥Response resolver is the second argument to the request handler that decides how to handle the intercepted request. There are multiple things you can do with such a request: respond with a mock response, perform it as-is, perform a proxy request and augment the original response, etc. You can always learn more about response resolver on this pages:

在本教程中,我们将使用模拟响应来响应被拦截的请求。

¥In this tutorial, we will be responding to the intercepted requests with mock responses.

模拟响应

¥Mocking responses

响应 GraphQL 请求遵循与响应任何其他 HTTP 请求相同的原则:构造一个有效的 Fetch API Response 实例并从响应解析器返回它。

¥Responding to GraphQL requests follows the same principle as responding to any other HTTP request: construct a valid Fetch API Response instance and return it from the response resolver.

但是,GraphQL 响应有许多不同之处:

¥GraphQL responses, however, have a number of differences:

  • GraphQL 响应始终为 200 OK,即使在错误响应的情况下也是如此;

    ¥GraphQL responses are always 200 OK, even in case of error responses;

  • GraphQL 响应主体必须具有固定结构({ data, errors, extensions });

    ¥GraphQL response bodies must have a fixed structure ({ data, errors, extensions });

为了让你的 GraphQL 客户端理解和处理模拟响应,它们必须是 有效的 GraphQL 响应。它们还必须符合特定 GraphQL 客户端所期望的响应主体形状(例如,Apollo 期望根级类型包含 __typename 字符串属性)。查阅你正在使用的 GraphQL 客户端的文档并观察其响应,以确保你的请求处理程序符合客户端的期望。

¥In order for your GraphQL client to understand and process the mocked responses, they must be valid GraphQL responses. They must also comply with the response body shape expected by the particular GraphQL client (e.g. Apollo expects root-level types to contain the __typename string property). Consult the documentation of the GraphQL client you are using and observe its responses to make sure your request handlers comply with what the client expects.

让我们为 ListPosts 查询声明一个模拟响应,其中包含所有帖子的列表。

¥Let’s declare a mock response for the ListPosts query containing a list of all posts.

Import the HttpResponse class from msw:

import { graphql, HttpResponse } from 'msw'

了解什么是 HttpResponse,以及为什么应该在 此处 中使用它而不是标准 Response

¥Learn about what HttpResponse is and why you should use it over the standard Response in here.

接下来,我们将描述模拟响应主体。使用 GraphQL API 时,响应主体的形状必须与查询的形状匹配。例如,假设客户端执行如下所示的 ListPosts 查询:

¥Next, we will describe the mock response body. When working with a GraphQL API, the shape of the response body must match the shape of the query. For example, let’s say the client performs the ListPosts query that looks like this:

query ListPosts {
  posts {
    id
    title
  }
}

这意味着客户端期望使用 posts 根级属性的响应,该属性代表帖子数组。每个数组项(即帖子)然后包含属性 idtitle。基于此查询定义,我们可以使用 HttpResponse.json() 静态方法构造模拟 JSON 响应。

¥This means that the client expects a response with the posts root-level property that represents an array of posts. Each array item (i.e. post) then contains properties id and title. Based on this query definition, we can construct a mock JSON response using HttpResponse.json() static method.

Return a mock response from the query ListPosts resolver:

import { graphql, HttpResponse } from 'msw'
 
// Represent the list of all posts in a Map.
const allPosts = new Map([
  [
    'e82f332c-a4e7-4463-b440-59bc91792634',
    {
      id: 'e82f332c-a4e7-4463-b440-59bc91792634',
      title: 'Introducing a new JavaScript runtime',
    },
  ],
  [
    '64734573-ce54-435b-8528-106ac03a0e11',
    {
      id: '64734573-ce54-435b-8528-106ac03a0e11',
      title: 'Common software engineering patterns',
    },
  ],
])
 
export const handlers = [
  graphql.query('ListPosts', () => {
    return HttpResponse.json({
      data: {
        // Convert all posts to an array
        // and return as the "posts" root-level property.
        posts: Array.from(allPosts.values()),
      },
    })
  }),
]

读取变量

¥Reading variables

mutation CreatePost 处理程序中,让我们访问我们尝试创建的新帖子并将其添加到现有帖子列表中。为此,我们需要访问代表下一篇文章的突变变量。与模拟 GraphQL 响应类似,访问变量将取决于操作的定义方式。

¥In the mutation CreatePost handler, let’s access the new post we are trying to create and add it to the list of the existing posts. To do that, we need to access a mutation variable representing the next post. Similar to mocking GraphQL responses, accessing variables will depend on how the operation is defined.

考虑客户端上 CreatePost 突变的以下定义:

¥Consider the following definition for the CreatePost mutation on the client:

mutation CreatePost($post: PostInput!) {
  createPost(post: $post) {
    id
  }
}

客户端在类型 PostInput 的突变上创建一个 $post 变量,表示要添加的帖子。你可以通过响应解析器的 variable 参数中的名称访问操作变量。

¥The client creates a $post variable on the mutation of type PostInput that represents a post to be added. You can access operation variables by their name in the variable argument to the response resolver.

Read the mutation variables using the variables object:

export const handlers = [
  graphql.mutation('CreatePost', ({ variables }) => {
    // Read the "post" variable on the mutation.
    const { post } = variables
 
    // Push the new post to the list of all posts.
    allPosts.set(post.id, post)
 
    // Respond with the body matching the mutation.
    return HttpResponse.json({
      createPost: {
        id: post.id,
      },
    })
  }),
]

以同样的方式,以下是 DeletePost 突变声明及其请求处理程序的示例:

¥In the same manner, here’s an example of the DeletePost mutation declaration and its request handler:

mutation DeletePost($postId: ID!) {
  deletePost(id: $postId) {
    id
  }
}
export const handlers = [
  graphql.mutation('DeletePost', ({ variables }) => {
    const { postId } = variables
    const deletedPost = allPosts.get(postId)
 
    // Respond with a GraphQL error when trying
    // to delete a post that doesn't exist.
    if (!deletedPost) {
      return HttpResponse.json({
        errors: [
          {
            message: `Cannot find post with ID "${postId}"`,
          },
        ],
      })
    }
 
    allPosts.delete(postId)
 
    return HttpResponse.json({
      deletePost: deletedPost,
    })
  }),
]

读取查询

¥Reading queries

你可以将客户端设置的原始查询定义作为响应解析器参数上的 query 键访问。

¥You can access the original query definition set from the client as the query key on the response resolver argument.

graphql.query('ListPosts', ({ query, variables }) => {
  // resolve the request
})

如果你想要根据模拟 GraphQL 模式解析拦截的 GraphQL 操作,query 对象非常有用。

¥The query object is useful if you want to resolve the intercepted GraphQL operations against a mock GraphQL schema.


采用 GraphQL 的原因之一是仅获取客户端需要的数据。但是,在使用 MSW 模拟 GraphQL 响应时,整个模拟数据将按原样发送到客户端,无论它查询的字段是什么。

¥One of the reasons to adopt GraphQL is to fetch only the data the client needs. When mocking the GraphQL responses with MSW, however, the entire mock data will be sent to the client as-is, regardless of the fields it queries.

例如,如果客户端决定在 ListPosts 查询中删除 title 字段,则如果在模拟响应中发送该字段,它仍将接收该字段。这是因为 MSW 在解析传入查询时不会进行常规 GraphQL 服务器会执行的字段匹配。

¥For example, if the client decides to drop the title field on the ListPosts query, it will still receive that field if it’s sent in the mock response. That is because MSW does no field matching that a regular GraphQL server would do when resolving the incoming queries.

你可以通过使用 graphql 包解析拦截的查询来更紧密地匹配实际 GraphQL 服务器的行为。这种方法要求你定义 GraphQL 模式并转发被拦截的 GraphQL 操作的操作名称、查询和变量,以根据模拟根值进行解析。

¥You can match the behavior of an actual GraphQL server more closely by resolving the intercepted queries using the graphql package. This approach requires you to define the GraphQL schema and forward the operation name, query, and variables of the intercepted GraphQL operations to be resolved against a mock root value.

Read the operation name and query from the resolver:

import { graphql as executeGraphQL, buildSchema } from 'graphql'
import { graphql, HttpResponse } from 'msw'
 
const schema = buildSchema(`
  type Post {
    id: ID!
    title: String!
  }
 
  type Query {
    posts: [Post!]
  }
`)
 
const allPosts = new Map([
  /* posts here */
])
 
export const handlers = [
  graphql.query('ListPosts', async ({ query, variables }) => {
    const { errors, data } = await executeGraphQL({
      schema,
      source: query,
      variableValues: variables,
      rootValue: {
        posts: Array.from(allPosts.values()),
      },
    })
 
    return HttpResponse.json({ errors, data })
  }),
]

后续步骤

¥Next steps

描述所需的网络后,将其集成到应用中的任何环境中。

¥Once you have described the network you want, integrate it into any environment in your application.