对于我们大多数人来说,在设计API和Web应用程序时,我们会想到REST架构。仅仅想到范式转换到其他东西上,就会被看作是一种毫无意义的头痛,不会产生任何净效益和大量工作。
通过不得不自己研究这种转变,我想我会分享我在这个过程中学到的一些东西。我将尝试解释如何使用GraphQL并突出显示两种方法之间的差异。
什么是GraphQL?
GraphQL以多种方式描述,“ 用于访问数据的统一接口。”,“用于在单个端点上公开数据模式的查询语言。“,” 服务器端运行时,用于使用数据定义的类型系统执行查询。”。
坦白地说,所有这些不同的描述都可能会增加最初关于使用它的内容、位置和原因的混淆。从表面上看,所有这些描述都指向同一个东西,而不是对GraphQL的基本存在给出另一个乏味的描述。我认为用图片的形式更容易理解。
下面是GraphQL查询的示例。
在这里,我们使用用户的“id”查询用户数据。关于查询和响应的第一件事是它们是相同的,这是因为GraphQL是键入的,这意味着你要求的是你得到的。我们只是简单地说“用这个ID给我这个用户,然后将他们的名字,帖子和粉丝告诉我”。
现在,为了帮助您了解GraphQL查询,我们可以将其与使用下面的REST端点获得相同结果的方式进行比较。
在这里,我们必须定义3个独立的端点才能访问相同的数据。现在你们中的一些人会说你可以通过在第一个请求中返回整个用户对象来将其减少到一个端点,这将被视为过度获取数据。如果我们想象我们有一个需要只显示用户名列表的UI,那么使用这个单一的整体REST请求来获取所有用户意味着我们必须对响应数据执行一些客户端处理以获得我们的内容原本想要的。
这里要注意的一个主要关键区别是,使用GraphQL,您可以了解每个查询可用的对象和数据,以便指定要从服务器返回的数据的形状。使用REST,您无法真正定义返回给您的内容,因为服务器会决定这一点。
因此,我们可以将GraphQL视为一个单一端点,在该端点上我们有一组可执行查询,这些查询映射到我们希望服务器公开的数据模式。
好吧,对GraphQL进行描述的糟糕尝试可能仍然没有任何意义,所以让我们运行一些代码示例,并启动我们自己的小API来比较两者。我创建了一个小的节点应用程序,您可以从GitHub得到它。在这个应用程序中,我们将重新创建上述图像中显示的示例。所有这些都遵循howtographql.com团队的精彩介绍指南。
我们要做的第一件事是创建并启动本地mongoDB服务器。之后,使用mongoose为我们的用户数据定义模型。
'use strict';
const mongoose = require('mongoose');
exports = module.exports = create;
function create() {
let postSchema = new mongoose.Schema({
title: { type: String },
content: { type : String },
comments: [{
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
comment: { type: String}
}]
}, {strict: false});
let userSchema = new mongoose.Schema({
name: { type: String },
age: { type: Number },
posts: [postSchema],
followers: [{type: mongoose.Schema.Types.ObjectId, ref: 'User'}]
}, {strict: false});
return mongoose.model('User', userSchema, 'users');
}
我还创建了一个简单的数据库管理器类,它可以启动和停止本地MongoDB服务器,并插入一堆虚拟数据供我们运行查询。
接下来,我们将使用express创建一个非常简单的REST API,它将具有与上图中所示的大致相同的端点。每个人都会调用数据库来获取一堆模拟数据。
'use strict'
module.exports = create;
function create(app) {
let db = app.services.db;
return {
setupRoutes
};
function setupRoutes(router) {
router.route('/user').get(getUser);
router.route('/user/all').get(getAllUsers);
router.route('/user/:name/posts').get(getUserPosts);
router.route('/user/:name/followers').get(getUserFollowers);
router.route('/user/create').post(createUser);
return router;
}
function getUser(req, res) {
let { name } = req.query;
db.models.User.findOne({name: name}, function (err, doc) {
if (err) {
return res.status(400).send('Error was given when trying to fetch user with name' + name);
}
return res.json(doc);
}).lean();
}
function getAllUsers(req, res) {
db.models.User.find({}, function (err, docs) {
if (err) {
return res.status(400).send('Error was given when trying to fetch all users');
}
return res.json(docs);
}).lean();
}
function getUserPosts(req, res) {
let { name } = req.params;
db.models.User.findOne({name: name}, function (err, doc) {
if (err) {
return res.status(400).send('Error was given when trying to fetch users post with name' + name);
}
return res.json(doc);
}).lean().select('posts');
}
function getUserFollowers(req, res) {
let { name } = req.params;
db.models.User.findOne({name: name}, function (err, doc) {
if (err) {
return res.status(400).send('Error was given when trying to fetch users post with name' + name);
}
return res.json(doc);
}).lean().select('followers').populate('followers');
}
function createUser(req, res) {
let { user } = req.body;
db.models.User.create(user, function (err, doc) {
if (err) {
return res.status(400).send('Error was given when trying to create new user.');
}
return res.json(doc);
});
}
}
最后,让我们创建GraphQL端点和模式。为此,我们将使用express-graphql完成,它将通过HTTP提供我们的GraphQL API,并构建和定义我们将使用graphql-tools的模式。还有许多其他方法可以表示您的GraphQL架构,但是对于此示例,我希望尝试尽可能接近GraphQL SDL模型。我打算从解析器函数中分离出类型定义,希望能让它更容易理解。其他方式请看这里。
首先,我们创建我们的类型定义,它们布局数据的结构。我们将它映射到我们之前定义的猫鼬模型。这里的前两个类型定义是特殊类型。每个GraphQL服务都必须具有查询类型(默认情况下不需要突变)。这些类型与常规对象类型相同,但它们是唯一的,因为它们定义了每个GraphQL查询的入口点。
let typeDefs =`
type Query {
# Query user by their name
user(name: String!): User!
# Query all users in database
allUsers(last: Int): [User!]!
}
type Mutation {
# Create a new user in database
createUser(name: String!, age: Int!): User!
}
## User Schema
type User {
# Full name of the user
name: String!
# Exact age of user
age: Int!
# A list of posts the user has generated
posts: [Post!]!
# A list of followers the user has
followers: [User!]
}
## Post Schema
type Post {
# Title of post
title: String!
# Content body of the post
content: String!
# A list of comments on the post
comments: [Comments]
}
## Comments Schema
type Comments {
# The ID of user who made the comment
user: ID!
# Comment text
comment: String!
}
`;
因此,使用'Query'类型,我们将定义一个用户函数,它将接收一个字符串并返回一个用户对象,'!' 表示我们返回的内容不能是null对象,它必须是我们稍后定义的“User”类型的对象。
user(name: String!): User!
其他类型是普通对象类型(它们对GraphQL不是特殊的),它们使用一系列支持的标量类型,这些类型表示数据的实际类型(String,Int,Date)。所以这里我们要创建基于我们存储在mongo中的数据结构的类型,并返回到客户端。'User','Post'和'Comments'类型定义将类似于我们的mongoose模式定义。
type User: {
name: String!
age: Int!
posts: [Post!]!
followers: [User!]
}
了解我们如何使“User”对象的上述GraphQL类型定义类似于下面的“User”mongoose模式。
let userSchema = new mongoose.Schema({
name: { type: String },
age: { type: Number },
posts: [postSchema],
followers: [{type: mongoose.Schema.Types.ObjectId, ref: 'User'}] });
GraphQL附带以下类型:
-
Int
:带符号的32位整数。 -
Float
:带符号的双精度浮点值。 -
String
:UTF-8字符序列。 -
Boolean
:true
或false
。 -
ID
:ID标量类型表示唯一标识符,通常用于重新获取对象或作为缓存的键。ID类型以与String相同的方式序列化; 但是,将其定义为ID
表示它不是人类可读的。
GraphQL还允许您构建和创建自己的标量类型,因此虽然它们只支持开箱即用的基本标量类型,但您可以更轻松地构建自己的类型,有关GraphQL类型的更多信息,请参见此处。
因此,使用我们编写的Query和Mutation方法的正式类型定义,下一步是使用这些定义来构建相应的函数/解析器,它将被调用以获取或更新数据。
let resolvers = {
Query: {
user: async (parent, { name }, context, info) => {
return await context.db.models.User.findOne({name: name}).lean().populate('followers').exec();
},
allUsers: async (parent, args, context, info) => {
return await context.db.models.User.find({}).lean().exec();
}
},
Mutation: {
createUser: async (parent, { name, age }, context, info) => {
return await context.db.models.User.create({name, age});
}
}
}
每个解析器可以包含四个参数:
-
parent
:上一个对象,通常不使用根查询类型的字段。 -
args
:您在类型定义中注册的输入以及在GraphQL查询中提供给字段的参数。 -
context
:一个值,提供给每个解析程序并保存重要的上下文信息,如当前登录的用户或访问数据库。 -
info
:包含与当前查询相关的特定于字段的信息以及架构详细信息的值
在这种情况下,我们将在上下文中传递我们的数据库连接。我们解构的参数与我们之前在这些方法的类型定义中定义的参数相同。我们还定义了哪些参数是强制性的,哪些是可选的,使用' !'符号,带符号的符号是解析器的必需参数。
因此,请从“ Query”类型返回“ user”类型定义。
user(name: String!): User!
我们已经定义了这个'user'查询接受一个名为name的参数,该参数必须是非空字符串。现在我们知道我们的旋转变压器的'args'参数将是以下形状的对象。
args: {
name: 'John'
}
我们对每个解析器所做的就是接受给定的参数并使用它们对我们的mongoose模型执行查询,以获取或更新数据记录。
就是这样!我想我还没有详细介绍GraphQL模式的所有组件,但那是因为有很多其他很棒的描述和教程,我想鼓励人们为自己运行代码。用它作为学习和改进的基础。这里要注意的一个要点是,即使是嵌套模式,创建类型定义也不仅简单,而且需要多少代码来创建一个比REST对应方更具通用性的查询方法。
有关如何安装和运行应用程序的详细信息,请参阅GitHub项目上的README文件。
总而言之,让我们回顾一下使用GraphQL而不是REST的优缺点。
好处
1.不再多余的提取
REST最常见的问题之一是过度提取和提取不足。我们的意思是客户端调用固定端点,它返回由服务器定义的数据结构,因此客户端可能会收到它不想要的数据,或者可能需要多次请求才能获得它实际需要的所有数据。设计一个没有这个固有问题的REST API非常困难。
另一方面,GraphQL没有这个问题。在GraphQL中,服务器声明可用的资源,并且客户端询问当时需要什么。
2.架构定义和文档
GraphQL使用强类型系统,该系统概述了服务器可以返回的数据结构。使用SDL定义模式意味着客户端可以轻松确定他们可以接收的数据的形状以及他们可以进行的突变或查询,这样做可以让客户端开发人员独立工作,因为他们可以轻松查看他们可用的数据。
以这种方式定义模式允许您非常轻松地记录API,而在开发REST端点之后必须编写API文档之前,您的模式就像一种文档形式。
3.减少所需的端点数量
这可能是我最喜欢的一点。必须设计和构建多个具有多个端点的REST API,将其降低到一个似乎是开发人员时间的光荣胜利。
4.订阅
GraphQL订阅是一种将数据从服务器推送到选择从服务器侦听实时消息的客户端的方法。订阅类似于查询,因为它们指定要传递给客户端的一组字段,但不是立即返回单个答案,而是每次在服务器上发生特定事件时都会发送结果。
这允许您通过Web套接字通知客户端有关已执行或已完成的特定事件。
缺点
1.采用和支持
GraphQL是新的,因此它不仅仅具有REST所提供的时间天赋,因此它允许它被普遍采用,以及围绕其框架构建的大量支持和工具。但是对于GraphQL来说,情况正在快速变化,随着越来越大的名字采用这种技术,我们只会看到支持和工具的增长相匹配。
你是如何嵌入的?
这不是GraphQL的直接缺点,而是更多关于切换到它的过程。对于某些应用程序和API来说,转换到这种新的工作方式可能为时已晚。如果您已经拥有一个宏大的复杂路由结构或使用您的API的众多客户端,那么进行切换可能永远不会具有成本效益甚至实用性。但请记住,Facebook有这个确切的问题,并且首先是GraphQL的出现,所以在这方面总是可以进行切换,它的情况就是值得的。
这些只是我在将API从REST移动到GraphQL时发现的一些内容。如果您对两者之间的比较有任何更多的想法?请在评论中发布!
译自:https://medium.com/@cameron.m.newby/a-comparison-of-graphql-and-rest-e125d77fb329