如何使用 Deno 构建 GraphQL 服务器
GraphQL 是一种流行的构建 API 的方法,因为它使开发人员可以轻松地仅访问他们所需的数据,从而节省带宽并减少瀑布请求,从而提高客户端性能。
在本教程中,我们将向您展示如何使用 Deno 构建 GraphQL API 服务器。
您也可以在此教程中使用 Apollo 以及 npm 说明符 进行操作。但是请注意,npm
说明符目前在 Deno Deploy 上尚不可用。
你好,世界!
让我们使用 Deno 设置一个 GraphQL 的“你好,世界!”示例。
我们将创建一个名为 hello_world_server.ts
的空文件,并添加我们的依赖项。
import { Server } from "https://deno.land/[email protected]/http/server.ts";
import { GraphQLHTTP } from "https://deno.land/x/[email protected]/mod.ts";
import { makeExecutableSchema } from "https://deno.land/x/[email protected]/mod.ts";
import { gql } from "https://deno.land/x/[email protected]/mod.ts";
我们将在下面看到这些内容的具体用法,但简而言之
Server
构建我们的 HTTP 服务器GraphQLHTTP
使用我们指定的模式创建 GraphQL HTTP 中间件makeExecutableSchema
根据我们的类型定义和解析器创建模式gql
创建我们的类型定义
首先,让我们使用 gql
设置类型和解析器。
const typeDefs = gql`
type Query {
hello: String
}
`;
这里,我们的 Query
类型有一个字段“hello”,它是 String
类型。获得类型后,我们需要为该 Query
提供一个解析器。解析器处理查询和变异的功能。
在本例中,我们希望我们的查询只打印“你好,世界!”。
const resolvers = {
Query: {
hello: () => `Hello, World!`,
},
};
为了与我们的服务器协同工作,我们需要将我们的类型和解析器组合成一个模式。我们可以使用 makeExecutableSchema
来实现。
const schema = makeExecutableSchema({ resolvers, typeDefs });
然后,模式对象将被传递给 GraphQLHTTP
,它位于我们的主 HTTP 服务器中。
const server = new Server({
handler: async (req) => {
const { pathname } = new URL(req.url);
return pathname === "/graphql"
? await GraphQLHTTP<Request>({
schema,
graphiql: true,
})(req)
: new Response("Not Found", { status: 404 });
},
port: 3000,
});
因此,主包装器是一个新的 Server
对象。在该对象中,我们有一个 handler
处理来自浏览器的请求。我们获取 url,如果它包含 /graphql
,则运行 GraphQLHTTP
中间件,将我们的模式传入并设置 graphiql
为 true
(您也可以不使用 graphiql
,而是使用 Postman/Insomnia 或直接使用 curl
)。
如果它不是 /graphql
页面,则返回 404 状态并打印“未找到”。
最后一行将启动我们在指定端口(3000)上运行的服务器并进行监听。
server.listenAndServe();
使用 --allow-net
标记运行此命令,以确保我们有网络访问权限。
$ deno run --allow-net hello_world_server.ts
如果我们然后转到 localhost:3000/graphql,我们将看到 graphiql 接口。
然后您可以运行查询。
query {
hello
}
并接收响应。
{
"data": {
"hello": "Hello World!"
}
}
就这样,你就在 GraphQL 了!
你好,(史前)世界!
一切都很好,但让 GraphQL API 如此强大的原因是它可以与数据进行交互。让我们扩展我们的示例,以便我们可以查询我们的数据、选择要接收的响应以及将数据插入我们的数据库。
因此,让我们设置一个后端数据库并使用一些我们可以查询和添加到其中的数据来加载它。
对于我们的数据存储,我们将使用 Postgres(您可以按照我们的教程 here → 连接到 Postgres 在 Supabase 上设置 Postgres 实例)。
接下来,让我们向我们的数据库中添加一些非常重要的数据:dinosaurs.json。
除了添加变异(这是您在 GraphQL 中添加数据的方式)之外,从我们的“你好,世界!”到使其正常工作,几乎没有什么变化。但我们将重构我们的代码,以使我们的项目更加整洁。
首先,我们将创建一个 typedefs.ts
文件并将我们的类型以及 gql
导入移动到那里。
import { gql } from "https://deno.land/x/[email protected]/mod.ts";
export const typeDefs = gql`
type Query {
allDinosaurs: [Dinosaur]
oneDinosaur(name: String): Dinosaur
}
type Dinosaur {
name: String
description: String
}
type Mutation {
addDinosaur(name: String, description: String): Dinosaur
}
`;
现在,我们有几个要定义的类型。
- 我们的两个查询是
allDinosaurs
和OneDinosaur
。allDinosaurs
返回一个列表(用方括号表示)。OneDinosaur
返回一个Dinosaur
。 Dinosaur
是我们从查询返回的对象,其中包含一个字符串名称和描述。addDinosaur
是我们的变异,用于向我们的数据库添加一个Dinosaur
,其中包含名称和描述。
我们还将我们的查询和变异解析器移到 resolvers.ts
中,因为这些解析器现在包含更多功能。resolvers.ts
也是我们将连接到 Postgres 的地方。
因此,我们需要先导入我们的 Postgres 库。
import * as postgres from "https://deno.land/x/[email protected]/mod.ts";
然后,我们可以构建我们的 Postgres 连接函数。
const connect = async () => {
// Get the connection string from the environment variable "DATABASE_URL"
const databaseUrl = Deno.env.get("DATABASE_URL")!;
// Create a database pool with three connections that are lazily established
const pool = new postgres.Pool(databaseUrl, 3, true);
// Connect to the database
const connection = await pool.connect();
return connection;
};
我们可以使用 Deno.env.get()
访问环境变量(我们将使用 --allow-env
标记来启用对 Postgres 环境变量的访问)。这里,我们的 DATABASE_URL
是存储的 Postgres 连接字符串。然后,我们创建连接池并连接到数据库。
然后,我们可以定义我们的查询和变异函数。我们将创建两个查询函数,一个用于获取数据库中所有恐龙的列表(allDinosaurs
),另一个用于仅通过名称获取一个恐龙(oneDinosaur
)。
const allDinosaurs = async () => {
const connection = await connect();
const result = await connection.queryObject`
SELECT name, description FROM dinosaurs
`;
return result.rows;
};
const oneDinosaur = async (args: any) => {
const connection = await connect();
const result = await connection.queryObject`
SELECT name, description FROM dinosaurs WHERE name = ${args.name}
`;
return result.rows;
};
allDinosaurs
连接到数据库并返回其中所有 Dinosaurs
的列表。oneDinosaur
类似,但
- 它接受一个参数,该参数将是我们想要的
Dinosaur
的名称。 - 它使用该参数名称来查询数据库以获取仅该
Dinosaur
。
我们还有一个变异函数用于向数据库添加 Dinosaur
。
const addDinosaur = async (args: any) => {
const connection = await connect();
const result = await connection.queryObject`
INSERT INTO dinosaurs(name, description) VALUES(${args.name}, ${args.description}) RETURNING name, description
`;
return result.rows[0];
};
一旦我们拥有所有函数,我们就可以在我们的解析器中引用它们。
export const resolvers = {
Query: {
allDinosaurs: () => allDinosaurs(),
oneDinosaur: (_: any, args: any) => oneDinosaur(args),
},
Mutation: {
addDinosaur: (_: any, args: any) => addDinosaur(args),
},
};
将解析器和类型定义移出主文件(我们将重命名为 server.ts
)后,我们需要将它们导入到该文件中,但 server.ts
的其余部分可以保持不变。
import { Server } from "https://deno.land/[email protected]/http/server.ts";
import { GraphQLHTTP } from "https://deno.land/x/[email protected]/mod.ts";
import { makeExecutableSchema } from "https://deno.land/x/[email protected]/mod.ts";
import { resolvers } from "./resolvers.ts";
import { typeDefs } from "./typedefs.ts";
const schema = makeExecutableSchema({ resolvers, typeDefs });
const server = new Server({
handler: async (req) => {
const { pathname } = new URL(req.url);
return pathname === "/graphql"
? await GraphQLHTTP<Request>({
schema,
graphiql: true,
})(req)
: new Response("Not Found", { status: 404 });
},
port: 3000,
});
server.listenAndServe();
这次运行 server.ts
时,我们还需要 --allow-env
标记来使 Postgres 正确工作。
deno run --allow-net --allow-env server.ts
如果我们转到 localhost:3000/graphql,我们将看到与以前一样的 graphiql 接口,但这次,自动完成将包含我们新的查询和变异的选项,例如查询 allDinosaurs
。
query {
allDinosaurs {
name
description
}
}
这将生成数据库中所有恐龙的列表。
请记住,GraphQL 的一大优点是最终用户可以选择他们想要请求的数据。在本例中,我们可以选择仅检索 names
。
我们也可以运行 oneDinosaur
查询以获取仅一个恐龙。
query {
oneDinosaur(name:"Aardonyx") {
name
description
}
}
最后,我们可以使用 addDinosaur
添加恐龙。
mutation {
addDinosaur(name:"Deno",description:"the fastest Deno in the world") {
name
description
}
}
我们提供名称和描述,但我们也可以从 API 中获取名称和描述,以检查恐龙是否已添加。
为了加倍确认,我们也可以再次使用 oneDinosaur
。
下一步是什么?
此服务器展示了使用 Deno 实现 GraphQL 的基本方法,但您可以做更多的事情,例如定义更多类型、查询以及数据之间的关系。例如,这里每个恐龙都是独立的,但我们可以将 分支 类型添加到我们的定义条目中,并展示 Aardonyx 如何与 Seitaad 和 Sefapanosaurus 相关。
我们还可以构建一个前端来使用这些数据。要了解它的外观,请查看我们的 如何使用完美的 Lighthouse 得分构建电子商务网站,了解我们如何在 我们的商品商店 中使用 Shopify GraphQL API。