你肯定听说过GraphQL,这是Facebook的基于图形的查询语言。自2015年发布以来,越来越多的数据提供商已经提供了GraphQL端点。这个端点通常与传统的基于REST的API一起提供。
我更喜欢前端的GraphQL端点。我喜欢能够查询到我想要的特定数据,避免过度或不足的问题。我喜欢GraphQL的自我记录性质,因为它的基于类型的模式准确地描述了预期和返回的内容。我曾多次与REST APIs打交道,却发现文档已经过时或错误。
不过在后端,我继续提供REST端点。传统的HTTP动词和路由都很熟悉,而且我可以很快就把一些功能搞出来。
在这篇文章中,我想回答的问题是,让GraphQL API启动和运行需要什么?
背景介绍
为了帮助这篇文章提供一些背景,我创建了一个虚构的冲浪店。今年夏天,我经常出去划皮划艇,而这正是这家特殊商店所销售的东西。伴随着这篇文章的代码可以在这里找到。
我的冲浪店使用MongoDB数据库,并有一个Fastify服务器可以使用。你可以在这里找到这个商店的启动代码,同时还有一个播种脚本,如果你想跟着做的话。你需要安装Node和MongoDB,这已经超出了本文的范围,但是点击名字可以进入安装页面。
为了使这成为一个现实的场景,我想让我目前正在使用REST API的客户不受影响,因为我添加了一个GraphQL端点。
让我们开始吧
GraphQL模式
有两个库我们需要添加到我们的项目中,以启动和运行GraphQL。第一个是graphql ,第二个是mercurius ,这并不令人惊讶。Mercurius是GraphQL的Fastify适配器。让我们来安装它们:
yarn add graphql mercurius
GraphQL是基于模式的,这意味着我们的API将始终被记录下来,并且类型安全。这对我们的消费者来说是一个很大的好处,在我们思考数据之间的关系时,也有助于我们。
我们的商店有两种类型,Craft 和Owner 。导航到Mongoose模型,你可以看到每个模型上有哪些字段可用。让我们看一下Owner 模型。
Mongoose模型看起来像这样:
const ownerSchema = new mongoose.Schema({
firstName: String,
lastName: String,
email: String,
});
我们要创建一个模式目录,这是一个index.js文件,然后创建我们的GraphQL模式。这个模式中的OwnerType ,看起来将与Mongoose的模式非常相似:
const OwnerType = `type OwnerType {
id: ID!
firstName: String
lastName: String
email: String
}`;
模板字符串被用来定义我们的类型,以关键字type 和我们的类型名称开始。与JavaScript对象不同,我们类型定义的每一行后面都没有逗号。相反,每一行都有字段名和它的类型,用一个冒号分开。我在定义中使用了ID 和String 类型。你会注意到,ID后面有一个感叹号,! ,这标志着它是一个强制性的、不可置换的字段。所有其他字段都是可选的。
我现在要把这个类型添加到我的模式的Query 类型中:
const schema = `
type Query {
Owners: [OwnerType]
Owner(id: ID!): OwnerType
}
${OwnerType}
`;
你会看到,Owners 的类型是返回一个数组的OwnerType ,由方括号表示。
Owner 要求查询的消费者传递一个id字段。这是由括号中的值表示的, (id: ID!)显示了字段的名称和它必须确认的类型。
最后,我们将从这个文件中导出这个模式,并将其导入我们的主index.js 文件:
module.exports = { schema };
并且
const { schema } = require("./schema");
在导入模式的同时,我们可以导入mercurius插件,并在Fastify注册。
const mercurius = require("mercurius");
fastify.register(mercurius, {
schema,
graphiql: true,
});
在选项插件中,我们将传递模式和另外一个属性--我们将设置graphiql ,等于true。
GraphiQL
GraphiQL是一个基于浏览器的界面,旨在探索和使用你的GraphQL端点。现在,它被设置为true,我们可以运行我们的服务器并导航到http://localhost:3000/graphiql ,找到这个页面。

通过这个工具,我们可以做以下工作:
- 编写和验证我们的查询。
- 添加查询变量和请求标题以帮助测试。
- 从我们的API获取结果。
- 探索我们的模式所产生的文档。
探索模式现在显示了一个根类型:query: Query 。我们正是在这个类型上添加了我们的Owner 和Owners 。点击它可以看到以下内容:

并点击它们中的任何一个显示出相应的类型:

我将继续设置其余的类型定义。你可以查看源代码,看看我是如何添加Craft 类型的,并为Owner 类型添加一个crafts 字段。
一旦我完成了这些,我的查询类型现在看起来像这样:

字段关系都已经设置好了,但我们还不能从它们那里获得任何数据。要做到这一点,我们需要探索两个概念:查询和解析器。
GraphQL查询
就其核心而言,GraphQL是一种查询语言;它甚至在名字中就有。但是,到目前为止,我们还没有执行任何查询。GraphiQL工具有自动完成功能,所以我们现在可以开始构建我们的查询。下面的查询应该会返回所有Crafts 的名称。
query {
Crafts {
name
}
}
但是,当我们执行时,我们得到了一个null 的响应。
{
"data": {
"Crafts": null
}
}
这是因为我们还没有设置任何解析器。解析器是一个函数,GraphQL运行这个函数来寻找它所需要的数据来解决一个查询。
对于这个项目,我将在schema/index.js 文件中定义解析器,与模式一起。我已经为我的REST API路由所使用的两种数据类型准备了控制器。我将使用这些控制器,经过一些调整,为我的GraphQL端点服务。
首先,我将导入这些控制器:
const craftController = require("../controllers/craftController");
const ownerController = require("../controllers/ownerController");
然后,我将创建一个resolvers对象:
const resolvers = {}
这个对象应该为我们想要提供解析器的每个根类型有一个键。对于我们的使用,我们有一个单一的根类型,即Query 。这个键的值应该是一个为获得所需数据而执行的函数。这就是我们的 Crafts 字段的样子。
const resolvers = {
Query: {
async Crafts() {
return await craftController.getCrafts();
},
},
};
然后我们导出resolvers函数,将其导入我们的主index.js ,并将其与模式一起传递给我们的插件选项对象。
// in /src/schema/index.js
module.exports = { schema, resolvers };
// in /src/index.js
const { schema, resolvers } = require("./schema");
fastify.register(mercurius, {
schema,
resolvers,
graphiql: true,
});
现在,当我们运行前面的查询时,我们应该得到我们数据库中所有工艺品的名称。

棒极了然而,如果我们想查询一个特定的工艺,该怎么办?这需要多做一点工作。首先,让我们在GraphiQL编辑器中构建查询。

这个查询设置看起来非常相似,但有一些区别:
- 我需要传入一个查询变量。在关键字
query之后,我们指出要传递的变量的名称和类型。该变量应以美元符号开头($)。 - 在这里,我使用变量
$id作为我的Craft字段的查询值。 - 查询变量的值是以JSON格式传递的。
- 最后,我得到了我的回应。
此刻,我没有任何数据返回。让我们来解决这个问题!
回到我的解析器中,我将为Craft添加一个函数。第一个位置参数是父级,在这个操作中我不需要它,所以我将在那里使用下划线。第二个参数是传入查询的参数,我想从中分解出id。
const resolvers = {
Query: {
async Crafts() {
return await craftController.getCrafts();
},
async Craft(_, { id }) {
return await craftController.getCraftById({id})
},
},
};
目前,我的getCraftById 函数期望得到请求对象。我需要在src/controllers/craftController.js 中更新这个函数。
这个原始函数
// Get craft by id
exports.getCraftById = async (request, reply) => {
try {
const craft = await Craft.findById(request.params.id);
return craft;
} catch (error) {
throw boom.boomify(error);
}
};
成为
exports.getCraftById = async (request, reply) => {
try {
const id = request.params === undefined ? request.id : request.params.id;
const craft = await Craft.findById(id);
return craft;
} catch (error) {
throw boom.boomify(error);
}
};
真棒!"。现在,当我们执行我们的查询时,将返回一个结果。

我们需要帮助GraphQL来填充链接到其他类型的字段。如果我们的消费者查询工艺品的当前所有者,它将返回为null 。我们可以添加一些逻辑,根据存储在数据库中的owner_id ,获得所有者。然后,在传回给我们的用户之前,这可以被附加到我们的工艺品对象上。
async Craft(_, { id }) {
const craft = await craftController.getCraftById({ id });
if (craft && craft.owner_id) {
const owner = await ownerController.getOwnerById({
id: craft.owner_id,
});
craft.owner = owner;
}
return craft;
},
我们的ownerController.getOwnerById ,将需要以与相应的工艺函数相同的方式进行更新。但是,一旦处理完毕,我们就可以自由地查询所有者。
你可以检查成品代码目录,找到所有其他字段的解析器和更新的控制器函数。
GraphQL突变
我现在可以自信地提供对GraphQL端点的查询;所有的读取操作都是对我们已经做的一些调整。那么其他操作呢?具体来说,Create,Update, 和Delete 呢?
在GraphQL中,这些操作的每一个都被称为突变。我们正在以某种方式改变数据。设置突变的后端与设置查询几乎完全一样。我们需要在模式中定义突变,然后提供当突变被调用时将被执行的解析器函数。
因此,在/schema/index.js 中,我将扩展Mutation 类型并添加一个addCraft 突变。
type Mutation {
addCraft(
name: String
type: String
brand: String
price: String
age: Int
): CraftType
}
和以前的字段定义一样,括号里的值显示了哪些字段可以被传入函数。这是每一个与它们的类型一起传递的。然后,我们接着说突变将返回什么。在这种情况下,我们的CraftType的形状是一个对象。
在GraphiQL中检查这个,我们可以看到mutation 现在是一个根类型,当我们点击时,我们的addCraft突变存在于模式中。

在GraphiQL中构造一个突变看起来与构造一个查询相同。我们需要像以前那样传入查询变量,它看起来就像这样。

但是,当我们执行时,我们会得到一个null 响应。希望这并不令人惊讶,因为我们还没有为这个突变创建一个解析器。让我们现在就做吧!
我们将为我们的解析器对象添加一个Mutation 键和一个用于addCraft 变异的函数。
Mutation: {
async addCraft(_, fields) {
const { _id: id } = await craftController.addCraft({ ...fields });
const craft = { id, ...fields };
return craft;
},
},
我们当前的addCraft 函数只返回 Mongoose 响应,也就是_id 字段。我们将提取它并返回输入的字段,使我们符合我们先前声明的CraftType。
更新和销毁函数的配置和设置是相同的。在每种情况下,我们都在扩展模式中的突变类型,并添加一个相应的解析器。
你可以查看成品代码目录,找到其他一些突变的解析器。
结论
我想知道建立一个GraphQL服务器是否是一个巨大的不必要的麻烦。我在结束时悄悄地相信,我将在我的下一个后台项目中使用GraphQL。
与通过我们的REST API直接接触Mongo相比,最初有一些设置和模板。这有可能是一个症结所在。然而,我认为有一些令人信服的观点使这是值得的。
你不再需要为你的应用程序的某些特殊用途提供一个端点。消费者只需要调用他们在特定情况下需要的字段。这就省去了杂乱无章的路由文件和多次调用你的API,而一次就可以了。
在更新模式和解析器的过程中,你使这些数据对你的消费者立即可用。虽然你可以将字段标记为废弃的,但你可以将遗留的字段留在原地,对用户来说没有什么成本。此外,这是一个自我记录的API。你的文档网站再也不会与你的API的当前状态脱节了。
你被说服了吗?你会转向GraphQL吗?还是你将永远留在REST API团队中?