使用 GraphQL Helix 构建 GraphQL 服务器
本周早些时候,我发布了GraphQL Helix,这是一个新的 JavaScript 库,可让您掌控 GraphQL 服务器实现。
有几个因素促使我推出自己的 GraphQL 服务器库:
- 我想使用前沿的 GraphQL 功能
@defer
,例如@stream
和@live
指令。 - 我想确保我不会被束缚于特定的框架或运行时环境。
- 我希望控制如何实现持久查询等服务器功能。
- 我想使用 WebSocket 以外的其他东西(即 SSE)进行订阅。
不幸的是,像Apollo Server、express-graphql和Mercurius这样的流行解决方案在一个或多个方面存在不足,所以我们就来到这里。
现有的库(例如 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);
请注意,我们假设我们已经创建了一个 GraphQL 模式。无论您如何构建模式(GraphQL Tools、TypeGraphQL、
graphql-compose、GraphQL Nexus等)都无关紧要——只要您有一个 GraphQLSchema 对象,就可以开始了。
接下来,让我们将请求中的相关位提取到标准 GraphQL Helix 对象中:
app.use("/graphql", async (res, req) => {
const request = {
body: req.body,
headers: req.headers,
method: req.method,
query: req.query,
};
});
更敏锐的读者可能会注意到,我们可以直接使用这个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,
})
});
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
}
});
我们的结果包括我们应该发回的标头、HTTP 状态代码和响应有效负载(即包含我们通过实际验证和执行请求获得的对象data
)errors
。
就这样!我们现在有一个/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`);
});
}
这里,我们的结果包含两个方法——subscribe
和unsubscribe
。subscribe
每次推送新的订阅事件时,我们都会通过一个回调函数来调用它,并在回调函数中传递结果。在这个回调函数中,我们只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");
}
请注意,返回的 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
}
});
返回的 GraphiQL 包含一个 fetcher 实现,可以处理多部分请求和 SSE,如上例所示。如果您需要为服务器添加其他功能,可以renderGraphiQL
仅使用模板自行实现。
改进服务器实现
GraphQL Helix 从设计上讲是轻量级且不拘泥于任何规范的。而像 Apollo Server 这样的库则充斥着许多你可能永远不需要的功能。
但是,这并不意味着你不能在需要的时候重新添加这些功能。例如,我们可以通过添加 Upload 标量并使用graphql-upload中的相应中间件来将上传功能添加到我们的服务器。
import { graphqlUploadExpress } from "graphql-upload";
app.use(
"/graphql",
graphqlUploadExpress({
maxFileSize: 10000000,
maxFiles: 10,
}),
(req, res) => {
// Our implementation from before
}
)
@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,
});
可以轻松添加跟踪、日志记录、持久查询、请求批处理、响应重复数据删除和任意数量的其他功能,而不会造成膨胀,也不必与某些插件 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