上下文 | Context

在 Node.js 世界中,上下文(Context)允许我们在同一个请求中共享数据和状态。在 GraphQL 中,上下文允许在同一个请求的多个解析函数中间件之间共享数据。

一个常见的用例是将当前访问者的身份信息存储在上下文中,以便在解析函数和中间件中访问。

访问上下文

GQLoom 中,我们通过 useContext() 函数访问上下文。

GQLoom 的 useContext 函数的设计参考了 ReactuseContext 函数。 你可以在解析器内的任何地方调用 useContext 函数以访问当前请求的上下文,而不需要显式地传递 context 函数。 在幕后,useContext 使用 node.js 的 AsyncLocalStorage 来隐式传递上下文。

接下来,让我们尝试在各个地方访问上下文。 我们将使用 graphql-yoga 作为适配器。

在解析函数中访问上下文

valibot
zod
import { query, resolver, useContext, weave } from "@gqloom/valibot" import * as v from "valibot" import { type YogaInitialContext, createYoga } from "graphql-yoga" import { createServer } from "http" const HelloResolver = resolver({ hello: query(v.string(), () => { const user = useContext<YogaInitialContext>().request.headers.get("Authorization") return `Hello, ${user ?? "World"}` }), }) const yoga = createYoga({ schema: weave(HelloResolver) }) createServer(yoga).listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") })

在上面的代码中,我们使用 useContext 函数从上下文中获取当前请求的 Authorization 头部,并将其与 Hello 字符串连接起来。 useContext 函数接受一个泛型参数,该参数指定上下文的类型,在这里,我们传入了 YogaInitialContext 类型。

让我们尝试调用这个查询:

curl -X POST http://localhost:4000/graphql -H "content-type: application/json" -H "authorization: Tom" --data-raw '{"query": "query { hello }"}'

你应该会得到以下响应:

{"data":{"hello":"Hello, Tom"}}

在中间件中访问上下文

import { useContext, Middleware } from "@gqloom/core" import { type YogaInitialContext } from "graphql-yoga" function useUser() { const user = useContext<YogaInitialContext>().request.headers.get("Authorization") return user } const authGuard: Middleware = (next) => { const user = useUser() if (!user) throw new Error("Please login first") return next() }

在上面的代码中,我们创建了一个名为 useUser 的自定义钩子,它使用 useContext 函数从上下文中获取当前请求的 Authorization 头部。 然后,我们创建了一个名为 authGuard 的中间件,它使用 useUser 钩子来获取用户,并在用户未登录时抛出错误。

要了解更多关于中间件的信息,请参阅 中间件文档

在验证输入时访问上下文

valibot
zod

我们可以在 valibot 中自定义验证或转换,并在其中直接访问上下文。

import { query, resolver, useContext, weave } from "@gqloom/valibot" import * as v from "valibot" import { type YogaInitialContext, createYoga } from "graphql-yoga" import { createServer } from "http" async function useUser() { const authorization = useContext<YogaInitialContext>().request.headers.get("Authorization") const user = await UserService.getUserByAuthorization(authorization) return user } const HelloResolver = resolver({ hello: query(v.string(), { input: { name: v.pipeAsync( v.nullish(v.string()), v.transformAsync(async (value) => { if (value != null) return value const user = await useUser() return user }) ), }, resolve: () => `Hello, ${name}`, }), }) const yoga = createYoga({ schema: weave(HelloResolver) }) createServer(yoga).listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") })

在上面的代码中,我们在 v.transformAsync 中使用 useUser() 来获取上下文中的用户信息,并将其作为 name 的值返回。

记忆化

考虑我们通过以下自定义函数来访问用户:

async function useUser() { const authorization = useContext<YogaInitialContext>().request.headers.get("Authorization") const user = await UserService.getUserByAuthorization(authorization) return user }

我们可能在 useUser() 中执行一些昂贵的操作,例如从数据库中获取用户信息,并且我们还有可能在同一请求中多次调用它。 为了避免多次调用造成的额外开销,我们可以使用记忆化(Memoization)来缓存结果,并在后续调用中重用它们。

在 GQLoom 中,我们使用 createMemoization 函数来创建一个记忆化函数。 记忆化函数会在第一次被调用后,将其结果缓存在上下文中,并在后续调用中直接返回缓存的结果。 也就是说,在同一个请求中,记忆化函数只会被执行一次,无论它被调用多少次。

让我们将 useUser() 函数记忆化:

import { createMemoization } from "@gqloom/core" const useUser = createMemoization(async () => { const authorization = useContext<YogaInitialContext>().request.headers.get("Authorization") const user = await UserService.getUserByAuthorization(authorization) return user })

如你所见,我们只需要将函数包装在 createMemoization 函数中即可。 随后,我们可以在解析器内的任何地方调用 useUser(),而无需担心多次调用带来的开销。

访问解析器参数

除了 useContext 函数,GQLoom 还提供了 useResolverPayload 函数,用于访问解析器中的所有参数:

  • root: 上一个对象,对于根查询类型上的字段来说,通常不会使用;

  • args: 在 GraphQL 查询中为字段提供的参数;

  • context: 在各个解析函数和中间件中共享的上下文对象;

  • info: 包含有关当前解析器调用的信息,例如 GraphQL 查询的路径、字段名称等;

  • field: 当前解析器正在解析的字段定义;

在各个适配器中使用上下文

在 GraphQL 生态中,每个适配器都提供了不同的上下文对象,你可以在适配器章节中了解如何使用: