快速开始

在本教程中,我们将引导你完成创建一个简单的 GraphQL 后端应用程序。

我们将使用 GQLoom 搭配你喜欢的 schema builder 来定义 GraphQL Resolver 和 Schema,构建一个简单的猫舍应用,能够查询猫舍中的猫并能给猫舍中添加新的猫。

你将使用以下技术:

  • Node.js:服务器端 JavaScript 运行时;
  • TypeScript:JavaScript 的超集,增加了静态类型和面向对象编程的功能;
  • GraphQL.js:JavaScript 的 GraphQL 参考实现;
  • graphql-yoga:构建 HTTP GraphQL 服务器的最简单方法;
  • GQLoom:更简单、更高效地定义 GraphQL Schema 和解析器。

你可以选择你熟悉的 Schema Builder,比如 ValibotZod,甚至直接使用 GraphQL.js

前提条件

在开始之前,请确保你已经安装了以下软件:

本教程假设你已经掌握 TypeScriptNode.jsGraphQL 的基础知识,并对 Valibot 或者 Zod 有一定认识。如果你是初学者,我们建议你先学习一下这些基础知识。

初始化项目

首先,我们需要创建一个新的 Node.js 项目。

打开你的命令行,运行以下命令:

mkdir cattery cd cattery npm init -y

在上面的命令中:我们创建了一个名为 cattery 的新目录,并进入该目录。然后,我们使用 npm init -y 命令初始化一个新的 Node.js 项目,并自动生成一个默认的 package.json 文件。

安装依赖

接下来,我们需要安装一些必要的依赖项。

npm
yarn
pnpm
bun
npm install -D typescript @types/node tsx

在这一步,我们安装了 TypeScript、Node.js 的类型定义以及 tsxtsx 是一个用于在 Node.js 中运行 TypeScript 的工具。

npm
yarn
pnpm
bun
npm install graphql graphql-yoga

我们还安装了 graphqlgraphql-yoga,来帮助我们运行 GraphQL 服务。

选择一个 Schema Builder

valibot
zod
graphql.js
npm
yarn
pnpm
bun
npm install @gqloom/core valibot @gqloom/valibot

现在,使用下面命令创建一个新的 TypeScript 配置文件

npx tsc --init

启动项目

首先,我们在 package.json 中添加一个 dev 脚本,用于启动我们的应用程序:

package.json
{ // ... "scripts": { "dev": "tsx watch src/index.ts" } // ... }

然后,我们创建一个 src/index.ts 文件,并添加以下代码:

valibot
zod
graphql.js
src/index.ts
import { weave, resolver, query } from "@gqloom/core" import { ValibotWeaver } from "@gqloom/valibot" import * as v from "valibot" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" const HelloResolver = resolver({ hello: query(v.string(), () => "Hello, World"), }) export const schema = weave(ValibotWeaver, HelloResolver) const yoga = createYoga({ schema }) createServer(yoga).listen(4000, () => { console.info("Server is running on http://localhost:4000/graphql") })

在上面的代码中:我们使用 resolverquery 来定义我们的 GraphQL Resolver,通过 weave 函数我们将 CatResolver 编织成 GraphQL Schema ,并使用 graphql-yoga 来启动我们的 GraphQL 服务。

现在,你可以运行以下命令来启动你的应用程序:

npm
yarn
pnpm
bun
npm run dev

你应该会看到类似以下的输出:

Server is running on http://localhost:4000/graphql

你可以打开浏览器并访问 http://localhost:4000/graphql,你将看到一个 GraphQL 演练场,你可以在这里测试你的 GraphQL 查询:

例如,当我们输入:

query { hello }

你应该会看到以下输出:

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

编写代码

现在,你已经成功地启动了你的 GraphQL 服务,让我们尝试构造稍微复杂的功能。

定义 Cat 类型

接下来,我们定义一个 Cat 类型,它有一个 name 字段和一个 birthDate 字段。

valibot
zod
graphql.js
src/index.ts
import * as v from "valibot" const Cat = v.object({ __typename: v.nullish(v.literal("Cat")), name: v.string(), birthDate: v.string(), }) interface ICat extends v.InferOutput<typeof Cat> {}

在上面的代码中,我们使用 v.object 来定义 Cat 类型: 它有一个 __typename 字段,它的值是 "Cat",当编织 GraphQL Schema 时,此值将作为此对象的名称,我们还将 __typename 设置为 nullish,如此一来就不必在运行时为每个 Cat 的实例对象携带 __typename 属性; 还有一个 name 字段和一个 birthDate 字段,它们的类型都为字符串。

最后,我们使用 v.InferOutput 轻易地获取了 Cat 的输出类型,并将其命名为 ICat

GQLoom 将把我们刚刚定义的 Cat 类型编织成 GraphQL Schema:

type Cat { name: String! birthDate: String! }

管理数据

为了管理我们的数据,我们简单地使用一个 Map 对象来存储 Cat 实例:

src/index.ts
const catMap = new Map<string, ICat>([ ["Tom", { name: "Tom", birthDate: "2023-03-03" }], ])
TIP

在本篇教程中,为了代码的简洁性,我们直接使用 JavaScript 的 Map 对象来存储数据。这将把数据存储在内存中,当服务器重启时,数据将丢失。

在实际应用中,你可能需要使用更可靠的数据持久化存储解决方案,例如数据库。

定义 query 操作

query 操作是 GraphQL Schema 的入口,它允许客户端查询数据。

现在,让我们回到最开始的 CatResolver,并为其添加一个名为 catsquery 操作,该操作返回所有 Cat 实例:

valibot
zod
graphql.js
src/index.ts
import { weave, resolver, query } from "@gqloom/core" import { ValibotWeaver } from "@gqloom/valibot" import * as v from "valibot" const CatResolver = resolver({ cats: query(v.array(Cat), () => Array.from(catMap.values())), }) const HelloResolver = resolver({ hello: query(v.string(), () => "Hello, World"), }) export const schema = weave(ValibotWeaver, HelloResolver, CatResolver)

在上面的代码中,我们使用 resolver 函数来定义 CatResolver,并为其添加一个名为 catsquery 操作,该操作返回所有 Cat 实例。 query 函数接受两个参数:

  • 第一个参数是 cats 的输出类型,你可以直接将 valibot schema 传入,在这里我们传入的是 v.array(Cat);
  • 第二个参数是一个解析函数,在解析函数中我们定义 cats 的具体解析逻辑,在这里我们使用 Array.from 函数将 catMap 转换为一个数组,并将其作为 cats 的返回值。

另外,我们还将 CatResolverHelloResolver 使用 weave 函数编织在一起,以创建最终的 GraphQL Schema。

让我们在演练场尝试访问 cats 操作:

query cats { cats { name birthDate } }

你应该会看到以下输出:

{ "data": { "cats": [ { "name": "Tom", "birthDate": "2023-03-03" } ] } }

定义输入

接下来,我们定义一个名为 catquery 操作,该操作接受一个 name 参数,并返回与该 name 对应的 Cat 实例:

valibot
zod
graphql.js
src/index.ts
import { resolver, query } from "@gqloom/core" import * as v from "valibot" const CatResolver = resolver({ cats: query(v.array(Cat), () => Array.from(catMap.values())), cat: query(v.nullish(Cat), { input: { name: v.string(), }, resolve: ({ name }) => catMap.get(name), }), hello: query(v.string(), () => "Hello, World"), })

在上面的代码中,我们为 CatResolver 其添加一个名为 catquery 操作。

cats 类似,构建 cat 使用的 query 函数的第一个参数为 Catnullish 类型,表示 cat 操作的返回值可以为 nullCat 类型。

在第二个参数中,我们依旧传入了一个解析函数,但这次我们传入了一个额外的 input 参数,该参数定义了 cat 操作的输入类型。 input 参数是一个对象,其中包含一个名为 name 的属性,该属性的类型为 string。当 cat 操作被访问时,GQLoom 会在内部调用 valibotparse 函数以确保 name 参数的值符合 string 类型。

在解析函数中,我们从 resolve 函数的第一个参数中获取 name 参数的值,TypeScript 会把 name 参数的类型推断为 string,然后我们使用 catMap.get 方法获取与 name 对应的 Cat 实例,并将其作为 cat 操作的返回值。

让我们在演练场尝试访问 cat 操作:

query cat { cat(name: "Tom") { name birthDate } }

你应该会看到以下输出:

{ "data": { "cat": { "name": "Tom", "birthDate": "2023-03-03" } } }

定义 mutation 操作

mutation 操作用于修改数据,例如创建、更新或删除数据。

现在,让我们为 CatResolver 添加一个名为 createCatmutation 操作,该操作接受一个 name 参数,并返回一个 Cat 实例。

valibot
zod
graphql.js
src/index.ts
import { resolver, query, mutation } from "@gqloom/core" import * as v from "valibot" const CatResolver = resolver({ cats: query(v.array(Cat), () => Array.from(catMap.values())), cat: query(v.nullish(Cat), { input: { name: v.string(), }, resolve: ({ name }) => catMap.get(name), }), createCat: mutation(Cat, { input: { name: v.string(), birthDate: v.string(), }, resolve: ({ name, birthDate }) => { const cat = { name, birthDate } catMap.set(name, cat) return cat }, }), hello: query(v.string(), () => "Hello, World"), })

在上面的代码中,我们为 CatResolver 添加了一个名为 createCatmutation 操作。

mutation 函数的输入与 query 函数一致。

在这里,createCat 操作的返回类型为 Cat,同时接受两个参数 namebirthDate 作为输入,它们的类型均为 string

在解析函数中,我们可以轻松从第一个参数中获取 namebirthDate 参数的值,TypeScript 将会为我们推导其类型,然后我们创建一个新的 Cat 实例,并将其添加到 catMap 中,最后返回该 Cat 实例。

让我们在演练场尝试创建新的 Cat 实例:

mutation createCat { createCat(name: "Nala", birthDate: "2020-01-01") { name birthDate } }

你应该会看到类似下面的结果:

{ "data": { "createCat": { "name": "Nala", "birthDate": "2020-01-01" } } }

让我们使用 cats 查询来获取所有 Cat 实例:

query cats { cats { name birthDate } }

你应该会看到类似下面的结果:

{ "data": { "cats": [ { "name": "Tom", "birthDate": "2023-03-03" }, { "name": "Nala", "birthDate": "2020-01-01" } ] } }

定义 field

现在,让我们尝试为 Cat 类型定义一个 age 字段。

age 字段并不保存在 Cat 实例中,而是在每次查询时计算。

valibot
zod
graphql.js
src/index.ts
import { resolver, query, mutation, field } from "@gqloom/core" import * as v from "valibot" const CatResolver = resolver.of(Cat, { age: field(v.pipe(v.number(), v.integer()), (cat) => { const birthDate = new Date(cat.birthDate) return new Date().getFullYear() - birthDate.getFullYear() }), cats: query(v.array(Cat), () => Array.from(catMap.values())), cat: query(v.nullish(Cat), { input: { name: v.string(), }, resolve: ({ name }) => catMap.get(name), }), // ... })

在上面的代码中,我们为 CatResolver 添加了一个名为 agefield

注意,我们使用 resolver.of 函数替代了 resolverresolver.of 函数的第一个参数为一个对象 Schema,在此处为 Cat,它将作为 CatResolversource 类型; 在第二个参数中,我们仍旧传入 querymutationfield 来定义 CatResolver

在名为 agefield 中,我们使用 v.pipe(v.number(), v.integer()) 来定义 age 的类型,GQLoom 将把 age 字段编织为 GraphQL Int 类型。注意,GQLoom 默认不会对解析函数的输出执行 parse 步骤,这是因为在解析函数内部产生的结果通常可控且符合 TypeScript 推导的类型。

在解析函数中,我们轻松地从第一个参数中获取 cat 实例的值,TypeScript 将会为我们推导其类型,然后我们将 cat 实例的 birthDate 字段转换为 Date 实例,并计算当前年份与 birthDate 的年份之差,最后返回该差值即为猫咪的年龄。

让我们在演练场尝试访问 cat 操作:

query cat { cat(name: "Tom") { name birthDate age } }

你应该会看到以下输出:

{ "data": { "cat": { "name": "Tom", "birthDate": "2023-03-03", "age": 1 } } }

为 field 添加输入

我们可以在 field 中添加一个 input 对象,它将作为该 field 的输入参数。

valibot
zod
graphql.js
src/index.ts
import { resolver, query, mutation, field } from "@gqloom/core" import * as v from "valibot" const CatResolver = resolver.of(Cat, { age: field(v.pipe(v.number(), v.integer()), { input: { year: v.nullish(v.pipe(v.number(), v.integer()), () => new Date().getFullYear() ), }, resolve: (cat, { year }) => { const birthDate = new Date(cat.birthDate) return year - birthDate.getFullYear() }, }), cats: query(v.array(Cat), () => Array.from(catMap.values())), cat: query(v.nullish(Cat), { input: { name: v.string(), }, resolve: ({ name }) => catMap.get(name), }), // ... })

在上面的代码中,我们为 age 字段添加了一个 input 对象,它包含一个名为 year 的字段,该字段为 Int 类型,若未提供 year 输入,则使用当前年份作为默认值。 在 field 解析函数中,我们可以从第二个参数中轻易地获取 year 的值。

让我们在演练场尝试访问 cat 操作:

query cat { cat(name: "Tom") { name birthDate age(year: 2026) } }

你应该会看到以下输出:

{ "data": { "cat": { "name": "Tom", "birthDate": "2023-03-03", "age": 3 } } }

总结

非常好,我们编写了一个简单的 GraphQL APP,它包含一个 CatResolver。 在刚刚的例子中,我们学习了:

  • resolver 中定义 querymutation 的方法;
  • 使用 valibotzodgraphql.js 来定义对象和字段;
  • querymutationfield 中定义解析函数和输入参数;
  • 使用 weave 函数将 CatResolverHelloResolver 编织为 GraphQL Schema,并使用 graphql-yoga 启动我们的 GraphQL APP。

下一步

  • 查看 GQLoom 的核心概念:丝线解析器编织
  • 了解常用功能:上下文DataLoader中间件
  • 查看 Valibot 集成 文档,了解如何使用 Valibot 构建更复杂的 GraphQL 对象以及 Union、Interface 和 Enum 等高级类型。
  • 查看 Zod 集成 文档,了解如何使用 Zod 构建更复杂的 GraphQL 对象以及 Union、Interface 和 Enum 等高级类型。