GraphQL 查询批处理
拦截和模拟批量 GraphQL 查询。
查询批处理是某些 GraphQL 客户端提供的一种性能机制,通过将它们分组到单个查询中来优化操作数量。虽然此功能具有实际好处,但查询批处理不是 GraphQL 规范(也不是 GraphQL-over-HTTP 规范)的一部分,缺乏对批处理查询的语法和行为的任何标准共识。因此,MSW 不提供处理此类查询的内置方法。
¥Query batching is a performance mechanism provided by some GraphQL clients to optimize the number of operations made by grouping them togeher in a single query. While this feature has its practical benefits, query batching is not a part of the GraphQL specification (neither the GraphQL-over-HTTP specification), lacking any standard consensus on the syntax and behavior of batched queries. Because of this, MSW does not provide a built-in way of handling such queries.
我们强烈建议将对批处理 GraphQL 查询的支持作为 MSW 设置的一部分来实现。下面,你可以找到几个如何实现这一点的示例。
¥We highly recommend implementing the support for batched GraphQL queries as a part of your MSW setup. Below, you can find a couple of examples of how to achieve that.
常识
¥General knowledge
从本质上讲,模拟批量 GraphQL 查询归结为以下步骤:
¥At its core, mocking a batched GraphQL query comes down to the following steps:
-
拦截批量 GraphQL 查询;
¥Intercept the batched GraphQL query;
-
将批处理查询解包为单独的 GraphQL 查询;
¥Unwrap the batched query into individual GraphQL queries;
-
针对现有请求处理程序解决单个查询;
¥Resolve the individual queries against the existing request handlers;
-
编写批处理响应。
¥Compose the batched response.
Apollo
Apollo 通过将多个查询分组到单个根级数组中来提供 查询批处理。
¥Apollo provides Query batching by grouping multiple queries in a single root-level array.
[
query GetUser {
user {
id
}
},
query GetProduct {
product {
name
}
}
]
此分组稍后会反映在响应批处理查询时收到的有效负载结构中:
¥This grouping is later reflected in the payload structure received in response to a batched query:
[
{ "data": { "user": { "id": "abc-123" } } },
{ "data": { "product": { "name": "Hoover 2000" } } }
]
你可以通过引入自定义 batchedGraphQLQuery
高阶请求处理程序来模拟 Apollo 中的批处理 GraphQL 查询,该处理程序拦截此类批处理查询,解开它们,并使用 msw
中的 getResponse
函数根据任何给定的请求处理程序列表解析它们。
¥You can mock batched GraphQL queries in Apollo by introducing a custom batchedGraphQLQuery
higher-order request handler that intercepts such batched queries, unwraps them, and resolves them against any given list of request handlers using the getResponse
function from msw
.
import { http, HttpResponse, getResponse, bypass } from 'msw'
function batchedGraphQLQuery(url, handlers) {
return http.post(url, async ({ request }) => {
const payload = await request.clone().json()
// Ignore non-batched GraphQL queries.
if (!Array.isArray(payload)) {
return
}
const responses = await Promise.all(
payload.map((query) => {
// Construct an individual query request
// to the same URL but with an unwrapped query body.
const queryRequest = new Request(request, {
body: JSON.stringify(query),
})
// Resolve the individual query request
// against the list of request handlers you provide.
const response = await getResponse(handlers, queryRequest)
// Return the mocked response, if found.
// Otherwise, perform the individual query as-is,
// so it can be resolved against an original server.
return response || fetch(bypass(queryRequest))
})
)
// Read the mocked response JSON bodies to use
// in the response to the entire batched query.
const queryData = await Promise.all(
responses.map((response) => response?.json()),
)
return HttpResponse.json(queryData)
})
}
然后,在你的请求处理程序中使用 batchedGraphQLQuery
函数:
¥Then, use the batchedGraphQLQuery
function in your request handlers:
import { graphql, HttpResponse } from 'msw'
const graphqlHandlers = [
graphql.query('GetUser', () => {
return HttpResponse.json({
data: {
user: { id: 'abc-123' },
},
})
}),
]
export const handlers = [
batchedGraphQLQuery('/graphql', graphqlHandlers),
...graphqlHandlers,
]
batched-execute
batched-execute
包通过在单个查询上提升多个操作并使用字段别名实现分组来提供 查询批处理。
¥The batched-execute
package provides Query batching by hoisting multiple operations on a single query and achieving grouping by using field aliases.
query {
user_0: user {
id
}
product_0: product {
name
}
}
然后,客户端将字段别名重新映射到原始操作,从而生成扁平响应对象。
¥The client then remap the field aliases to the original operations, producing a flat response object.
你可以通过引入自定义 batchedGraphQLQuery
高阶请求处理程序来模拟 batched-execute
中的批量 GraphQL 查询,该处理程序会拦截此类批量查询并根据模拟模式解析它们。在这种情况下,我们建议使用模式优先的 API 模拟来支持匿名查询。
¥You can mock batched GraphQL queries in batched-execute
by introducing a custom batchedGraphQLQuery
higher-order request handler that intercepts such batched queries and resolves them against a mocked schema. We recommend a schema-first API mocking in this case to support anonymous queries.
import {
buildSchema,
print,
graphql as executeGraphQL,
defaultFieldResolver,
} from 'graphql'
import { http, HttpResponse, bypass } from 'msw'
// Describe the GraphQL schema.
// You can also use an existing schema!
const schema = buildSchema(`
type User {
id: ID!
}
type Query {
user: User
}
`)
function batchedGraphQLQuery(url, handlers) {
return http.post(url, async ({ request }) => {
const payload = await request.json()
// Resolve the intercepted GraphQL batched query
// against the mocked GraphQL schema.
const result = await executeGraphQL({
source: payload.query,
variableValues: data.variables,
schema,
rootValue: {
// Mock individual queries, fields, and types.
user: () => ({ id: 'abc-123' }),
},
async fieldResolver(source, args, context, info) {
// Resolve the known fields from the "rootValue".
if (source[info.fieldName]) {
return defaultFieldResolver(source, args, context, info)
}
// Proxy the unknown fields to the actual GraphQL server.
const compiledQuery = info.fieldNodes
.map((node) => print(node))
.join('\n')
const query = `${info.operation.operation} { ${compiledQuery} }`
const queryRequest = new Request(request, {
body: JSON.stringify({ query }),
})
const response = await fetch(bypass(queryRequest))
const { error, data } = await response.json()
if (error) {
throw error
}
return data[info.fieldName]
},
})
return HttpResponse.json(result)
})
}