使

使用 GraphQL Helix 构建 GraphQL 服务器

2025-06-08

使用 GraphQL Helix 构建 GraphQL 服务器

本周早些时候,我发布了GraphQL Helix,这是一个新的 JavaScript 库,可让您掌控 GraphQL 服务器实现。

有几个因素促使我推出自己的 GraphQL 服务器库:

  • 我想使用前沿的 GraphQL 功能@defer,例如@stream@live指令。
  • 我想确保我不会被束缚于特定的框架或运行时环境。
  • 我希望控制如何实现持久查询等服务器功能。
  • 我想使用 WebSocket 以外的其他东西(即 SSE)进行订阅。

不幸的是,像Apollo Serverexpress-graphqlMercurius这样的流行解决方案在一个或多个方面存在不足,所以我们就来到这里。

现有的库(例如 Apollo Server)要么提供完整的 HTTP 服务器,要么提供可插入所选框架的中间件功能。GraphQL Helix 则采用了不同的方法——它只提供了一些函数,可用于将 HTTP 请求转换为 GraphQL 执行结果。换句话说,GraphQL Helix 让自行决定如何返回响应。

让我们看看这在实践中是如何运作的。

一个基本的例子

我们将首先构建一个快速应用程序并添加一个/graphql端点。

import express from "express";
import { schema } from "./my-awesome-schema";

const app = express();

app.use(express.json());

app.use("/graphql", async (res, req) => {
  // TODO
});

app.listen(8000);
Enter fullscreen mode Exit fullscreen mode

请注意,我们假设我们已经创建了一个 GraphQL 模式。无论您如何构建模式(GraphQL ToolsTypeGraphQL
graphql-composeGraphQL Nexus等)都无关紧要——只要您有一个 GraphQLSchema 对象,就可以开始了。

接下来,让我们将请求中的相关位提取到标准 GraphQL Helix 对象中:

app.use("/graphql", async (res, req) => {
  const request = {
    body: req.body,
    headers: req.headers,
    method: req.method,
    query: req.query,
  };
});
Enter fullscreen mode Exit fullscreen mode

更敏锐的读者可能会注意到,我们可以直接使用这个req对象——没错!但是,根据我们使用的框架或运行时,这一步会略有不同,所以我会更明确地说明我们如何定义这个对象。

现在我们从请求中提取相关参数并进行处理。

import {
  getGraphQLParameters,
  processRequest
} from "graphql-helix";

...

app.use("/graphql", async (res, req) => {
  const request = {
    body: req.body,
    headers: req.headers,
    method: req.method,
    query: req.query,
  };

  const {
    query,
    variables,
    operationName
  } = getGraphQLParameters(request);

  const result = await processRequest({
    schema,
    query,
    variables,
    operationName,
    request,
  })
});
Enter fullscreen mode Exit fullscreen mode

processRequest仍然将我们的Request对象作为参数,那么为什么它不直接调用getGraphQLParameters我们呢?正如我们稍后会看到的,这是一个有意为之的设计选择,它使我们能够灵活地决定如何从请求中实际获取参数。

好了,我们已经处理了请求,现在有结果了。太棒了!让我们用这个结果来做点什么吧。

app.use("/graphql", async (res, req) => {
  const request = {
    body: req.body,
    headers: req.headers,
    method: req.method,
    query: req.query,
  };

  const {
    query,
    variables,
    operationName
  } = getGraphQLParameters(request);

  const result = await processRequest({
    schema,
    query,
    variables,
    operationName,
    request,
  })

  if (result.type === "RESPONSE") {
    result.headers.forEach(({ name, value }) => {
      res.setHeader(name, value)
    });
    res.status(result.status);
    res.json(result.payload);
  } else {
    // TODO
  }
});
Enter fullscreen mode Exit fullscreen mode

我们的结果包括我们应该发回的标头、HTTP 状态代码和响应有效负载(即包含我们通过实际验证和执行请求获得的对象dataerrors

就这样!我们现在有一个/graphql可以处理请求的端点了。太棒了!

那么,既然我可以在 Apollo Server 中用几行代码实现同样的功能,为什么还要编写这么多额外的样板代码呢?答案是:灵活。如果我们将 Express 换成 Fastify 之类的其他框架,只需更改请求对象的构造方式和结果的处理方式即可。事实上,我们几乎可以在任何其他运行时中使用我们实现的核心部分——无服务器、Deno,甚至是浏览器。

此外,我们可以根据业务需求来处理结果。我们有一个基于 HTTP 的 GraphQL 规范,但如果出于某种原因您需要更改它,您也可以。这是您的应用程序——您可以返回适合您用例的状态、标头或响应。

那么...这个块是怎么回事else?事实证明,processRequest它将返回以下三种结果之一:

  • RESPONSE对于标准查询和突变,
  • MULTIPART_RESPONSE对于包含新指令的请求@defer@stream以及
  • PUSH订阅

再次强调,如何发回这些响应取决于我们自己,所以现在就开始吧!

订阅

我们将使用服务器发送事件 (SSE) 实现订阅。与 WebSocket 之类的订阅方式相比,使用 SSE 有很多优势,例如可以对所有请求使用相同的中间件。至于这两种方法的更深入比较,我们将在以后的文章中探讨。

有一些库可以使 SSE 与 Express 的集成更加容易,但在本例中我们将从头开始:

if (result.type === "RESPONSE") {
  ...
} else if (result.type === "PUSH") {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    Connection: "keep-alive",
    "Cache-Control": "no-cache",
  });

  req.on("close", () => {
    result.unsubscribe();
  });

  await result.subscribe((result) => {
    res.write(`data: ${JSON.stringify(result)}\n\n`);
  });
}
Enter fullscreen mode Exit fullscreen mode

这里,我们的结果包含两个方法——subscribeunsubscribesubscribe每次推送新的订阅事件时,我们都会通过一个回调函数来调用它,并在回调函数中传递结果。在这个回调函数中,我们只write使用兼容 SSE 的有效负载来响应。此外,unsubscribe为了防止内存泄漏,我们会在请求关闭时(即客户端关闭连接时)调用它。

非常简单。现在我们来看一下MULTIPART_RESPONSE

多部分响应

如果我们的请求包含@stream@defer指令,则需要以分块形式发送给客户端。例如,使用 时@defer,我们会发送除延迟片段之外的所有内容,并在延迟片段最终解析后再发送。因此,我们的MULTIPART_RESPONSE结果与 的结果非常相似,PUSH但有一个关键区别——我们希望在所有部分都发送完毕后最终结束响应。

if (result.type === "RESPONSE") {
  ...
} else if (result.type === "PUSH") {
  ...
} else {
  res.writeHead(200, {
    Connection: "keep-alive",
    "Content-Type": 'multipart/mixed; boundary="-"',
    "Transfer-Encoding": "chunked",
  });

  req.on("close", () => {
    result.unsubscribe();
  });

  await result.subscribe((result) => {
    const chunk = Buffer.from(
      JSON.stringify(result),
      "utf8"
    );
    const data = [
      "",
      "---",
      "Content-Type: application/json; charset=utf-8",
      "Content-Length: " + String(chunk.length),
      "",
      chunk,
      "",
    ].join("\r\n");
    res.write(data);
  });

  res.end("\r\n-----\r\n");  
}
Enter fullscreen mode Exit fullscreen mode

请注意,返回的 Promisesubscribe直到请求完全解决并且使用所有块调用回调后才会解决,此时我们就可以安全地结束响应。

恭喜!我们的 API 现已支持@defer@stream(前提是您使用的 版本正确graphql-js)。

添加 GraphiQL

GraphQL Helix 附带两个附加功能,可用于在您的服务器上公开 GraphiQL 接口。

shouldRenderGraphiQL接受一个 Request 对象并返回一个布尔值,正如你可能已经猜到的那样,该布尔值指示是否应该渲染界面。当你的 API 和界面只有一个端点,并且只想在浏览器内部处理 GET 请求时返回 GraphiQL 界面时,这很有用。

renderGraphiQL仅返回渲染界面所需的 HTML 字符串。如果您想为文档创建单独的端点,则无需使用shouldRenderGraphiQL任何代码即可使用此函数。

app.use("/graphql", async (req, res) => {
  const request = {
    body: req.body,
    headers: req.headers,
    method: req.method,
    query: req.query,
  };

  if (shouldRenderGraphiQL(request)) {
    res.send(renderGraphiQL());
  } else {
    // Process the request
  }
});
Enter fullscreen mode Exit fullscreen mode

返回的 GraphiQL 包含一个 fetcher 实现,可以处理多部分请求和 SSE,如上例所示。如果您需要为服务器添加其他功能,可以renderGraphiQL仅使用模板自行实现。

改进服务器实现

GraphQL Helix 从设计上讲是轻量级且不拘泥于任何规范的。而像 Apollo Server 这样的库则充斥着许多你可能永远不需要的功能。

屏幕截图 2020-11-05 上午 11.22.49

但是,这并不意味着你不能在需要的时候重新添加这些功能。例如,我们可以通过添加 Upload 标量并使用graphql-upload中的相应中间件来将上传功能添加到我们的服务器。

import { graphqlUploadExpress } from "graphql-upload";

app.use(
  "/graphql",
  graphqlUploadExpress({
    maxFileSize: 10000000,
    maxFiles: 10,
  }),
  (req, res) => {
    // Our implementation from before
  }
)
Enter fullscreen mode Exit fullscreen mode

@live类似地,我们可以通过添加@n1ru4l/graphql-live-query@n1ru4l/in-memory-live-query-store来使用指令添加对实时查询的支持。我们只需要将指令添加到我们的架构中并提供适当的execute实现:

import {
  InMemoryLiveQueryStore
} from "@n1ru4l/in-memory-live-query-store";

const liveQueryStore = new InMemoryLiveQueryStore();

...

const result = const result = await processRequest({
  schema,
  query,
  variables,
  operationName,
  request,
  execute: liveQueryStore.execute,
});
Enter fullscreen mode Exit fullscreen mode

可以轻松添加跟踪、日志记录、持久查询、请求批处理、响应重复数据删除和任意数量的其他功能,而不会造成膨胀,也不必与某些插件 API 或不友好的抽象作斗争。

您可以检查存储库以获取更多示例和食谱(我会在时间允许的情况下添加更多内容并接受 PR!)。

结论

那么什么时候应该使用 Apollo Server 而不是 GraphQL Helix 呢?如果你需要快速完成一个 POC 或教程,Apollo Server 非常适合。如果你想使用联邦,你可能还是会选择 Apollo(即使如此,在微服务中使用 GraphQL 也有更好的选择)。

GraphQL Helix 提供了一种灵活、可扩展且不会臃肿的 GraphQL 服务器构建方法。如果你正在构建其他待办事项教程以外的内容,我强烈建议你尝试一下 :)

鏂囩珷鏉ユ簮锛�https://dev.to/danielrearden/building-a-graphql-server-with-graphql-helix-2k44
PREV
水管工云指南
NEXT
React:类组件 VS 带 Hooks 的函数组件