描述 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:
Response resolver
Learn more about response resolvers.
Mocking responses
Learn how to mock HTTP responses.
在本教程中,我们将使用模拟响应来响应被拦截的请求。
¥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 standardResponse
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
}
}
为了使用 graphql.query()
和 graphql.mutation()
请求处理程序,最好使用命名查询。对于匿名 GraphQL 查询,请改用 graphql.operation()
API。
¥Prefer named queries in order to use the graphql.query()
and
graphql.mutation()
request handlers. For anonymous GraphQL queries, use the
graphql.operation()
API
instead.
这意味着客户端期望使用 posts
根级属性的响应,该属性代表帖子数组。每个数组项(即帖子)然后包含属性 id
和 title
。基于此查询定义,我们可以使用 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.