GQLoom

上下文(Context)

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

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

访问上下文

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

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

启用上下文

对于不支持 AsyncLocalStorage 的环境,如浏览器或 Cloudflare Workers,可以使用 resolverPayload 中的 context 属性。

我们通过在 weave 函数中传入 asyncContextProvider 来启用上下文。asyncContextProvider 实际上是一个全局中间件。

import { weave } from "@gqloom/core"
import { asyncContextProvider } from "@gqloom/core/context"

const schema = weave(ValibotWeaver,asyncContextProvider, ...resolvers)

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

在解析函数中访问上下文

import { , ,  } from "@gqloom/core"
import {  } from "@gqloom/core/context"
import {  } from "@gqloom/valibot"
import * as  from "valibot"
import { type YogaInitialContext,  } from "graphql-yoga"
import {  } from "http"

const  = ({
  : (.(), () => {
    const  =
      <YogaInitialContext>()...("Authorization") 
    return `Hello, ${ ?? "World"}`
  }),
})

const  = ({ : (, ) })
().(4000, () => {
  .("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 {  } from "@gqloom/core"
import {  } from "@gqloom/core/context"
import { type YogaInitialContext } from "graphql-yoga"

function () {
  const  =
    <YogaInitialContext>()...("Authorization")
  return 
}

const :  = () => {
  const  = ()
  if (!) throw new ("Please login first")
  return ()
}

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

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

在验证输入时访问上下文

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

import { , ,  } from "@gqloom/core"
import {  } from "@gqloom/core/context"
import {  } from "@gqloom/valibot"
import * as  from "valibot"
import { type YogaInitialContext,  } from "graphql-yoga"
import {  } from "http"

async function () {
  const  =
    <YogaInitialContext>()...("Authorization")
  const  = await .()
  return 
}

const  = ({
  : (.(), {
    : {
      : .( 
        .(.()),
        .(async () => { 
          if ( != null) return  
          const  = await () 
          return . 
        }) 
      ),
    },
    : ({  }) => `Hello, ${}`,
  }),
})

const  = ({ : (, ) })
().(4000, () => {
  .("Server is running on http://localhost:4000/graphql")
})

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

记忆化

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

import {  } from "@gqloom/core/context"
import { type YogaInitialContext } from "graphql-yoga"

async function () {
  const  =
    <YogaInitialContext>()...("Authorization")
  const  = await .()
  return 
}

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

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

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

import { ,  } from "@gqloom/core/context"

const  = (async () => {
  const  =
    <YogaInitialContext>()...("Authorization")
  const  = await .()
  return 
})

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

注入上下文

asyncContextProvider 还允许我们注入上下文。这通常与 执行器 一起使用。

const giraffeExecutor = giraffeResolver.toExecutor(
  asyncContextProvider.with(useCurrentUser.provide({ id: 9, roles: ["admin"] }))
)

访问解析器负载

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

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

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

  • context: 在各个解析函数和中间件中共享的上下文对象,即 useContext 的返回值;

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

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

直接访问解析器负载

对于不提供 AsyncLocalStorage 的环境,如浏览器或 Cloudflare Workers,我们可以直接在解析函数和中间件中访问解析器负载。

解析函数

在解析函数中,payload 总是最后一个参数。

const helloResolver = resolver({
  hello: query(v.string()).resolve((_input, payload) => {
    const user =
      (payload!.context as YogaInitialContext).request.headers.get("Authorization") 
    return `Hello, ${user ?? "World"}`
  }),
})

中间件

import { ,  } from "@gqloom/core"
import { type YogaInitialContext } from "graphql-yoga"

function (: ) {
  const  = (. as YogaInitialContext)...(
    "Authorization"
  )
  return 
}

const :  = ({ ,  }) => {
  const  = (!)
  if (!) throw new ("Please login first")
  return ()
}

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

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

目录