使用Node.js和MongoDB创建GraphQL服务器

本文由Ryan Chenkie进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

从客户端的服务器请求数据并不是一个新概念。 它允许应用程序加载数据而无需刷新页面。 这在单页应用程序中最常用,单页应用程序不从服务器获取呈现的页面,而仅请求在客户端呈现该页面所需的数据。

过去几年中,Web上最常见的方法是REST体系结构样式 。 但是,这种方法为高数据需求应用带来了一些限制。 在RESTful系统中,我们需要发出多个HTTP请求以获取所需的所有数据,这会对性能产生重大影响 。 如果可以在单个HTTP请求中请求多个资源怎么办?

引入GraphQL,这是一种统一客户端和服务器端之间通信的查询语言。 它允许客户端在单个请求中准确描述其所需的数据。

在本文中,我们将使用GraphQL路由创建一个Node.js / Express服务器,该服务器将处理我们的所有查询和变异。 然后,我们将通过发送一些POST请求来测试此路由,并使用Postman分析结果。

您可以在此处找到此应用程序完整源代码 。 我还制作了一个Postman收藏夹,您可以在此处下载

在Express Server上设置GraphQL端点

首先要做的是使用Express框架创建我们的Node.js服务器。 我们还将MongoDBMongoose一起用于数据持久性,而babel则使用ES6 。 由于代码是在运行时编译到ES5的,因此不需要构建过程。 这是在index.js中完成的:

// index.js
require('babel/register');
require('./app');

app.js中,我们将启动服务器,连接Mongo数据库并创建GraphQL路由。

// app.js
import express from 'express';
import graphqlHTTP from 'express-graphql';
import mongoose from 'mongoose';

import schema from './graphql';

var app = express();

// GraphqQL server route
app.use('/graphql', graphqlHTTP(req => ({
  schema,
  pretty: true
})));

// Connect mongo database
mongoose.connect('mongodb://localhost/graphql');

// start server
var server = app.listen(8080, () => {
  console.log('Listening at port', server.address().port);
});

在本文的上下文中,上面代码中最相关的部分是定义GraphQL路由的地方。 我们使用express-graphql ,这是由Facebook的GraphQL团队开发的Express中间件。 这将通过GraphQL处理HTTP请求并返回JSON响应。 为此,我们需要传递下一节中讨论的GraphQL Schema选项。 我们还设置选项pretty真。 这使得JSON响应打印精美,使其更易于阅读。

GraphQL模式

为了使GraphQL理解我们的要求,我们需要定义一个架构。 GraphQL模式不过是一组查询和变异。 您可以将查询视为从数据库中检索的资源,将突变视为对数据库的任何类型的更新。 作为示例,我们将创建BlogPostComment Mongoose模型,然后将为其创建一些查询和变异。

猫鼬模型

让我们从创建猫鼬模型开始。 由于猫鼬不是本文的重点,因此在此将不做详细介绍。 您可以在models / blog-post.jsmodels / comment.js中找到两个模型。

GraphQL类型

像Mongoose一样,在GraphQL中,我们需要定义数据结构。 不同之处在于,我们为每个查询和变异定义可以输入什么类型的数据以及在响应中发送什么类型的数据。 如果这些类型不匹配,则会引发错误。 尽管它看起来似乎很多余,但是由于我们已经在猫鼬中定义了一个模式模型,因此它具有很大的优势,例如:

  • 您可以控制允许的内容,从而提高系统安全性
  • 您控制允许的内容。 这意味着您可以定义特定字段,以使其永远不会被检索。 例如:密码或其他敏感数据
  • 它过滤无效请求,以便不进行进一步处理,从而可以提高服务器的性能

您可以在graphql / types /中找到GraphQL类型的源代码。 这是一个例子:

// graphql/types/blog-post.js
import {
  GraphQLObjectType,
  GraphQLNonNull,
  GraphQLString,
  GraphQLID
} from 'graphql';

export default new GraphQLObjectType({
  name: 'BlogPost',
  fields: {
    _id: {
      type: new GraphQLNonNull(GraphQLID)
    },
    title: {
      type: GraphQLString
    },
    description: {
      type: GraphQLString
    }
  }
});

在这里,我们定义了博客文章输出的GraphQL类型,在创建查询和变异时将进一步使用它。 注意结构与猫鼬模型BlogPost有多相似。 这似乎是重复的工作,但这些是分离的关注点。 猫鼬模型定义了数据库的数据结构,GraphQL类型定义了查询或服务器突变中接受的规则。

GraphQL模式创建

通过创建Mongoose模型和GraphQL类型,我们现在可以创建GraphQL模式。

// graphql/index.js
import {
  GraphQLObjectType,
  GraphQLSchema
} from 'graphql';

import mutations from './mutations';
import queries from './queries';

export default new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: queries
  }),
  mutation: new GraphQLObjectType({
    name: 'Mutation',
    fields: mutations
  })
});

在这里,我们导出一个GraphQLSchema,其中定义了两个属性:查询和突变。 GraphQLObjectType是许多GraphQL类型之一 。 特别是这一步,您可以指定:

  • 名称 –应该唯一并标识对象;
  • 字段 -接受对象的属性,在这种情况下将是我们的查询和变异。

我们正在从另一个位置导入queriesmutations ,这仅出于结构目的。 如果我们要添加更多模型,查询,变异等,则源代码的结构使我们的项目能够很好地扩展。

我们传递给字段的queriesmutations变量是普通的JavaScript对象。 键是突变或查询名称。 这些值是纯JavaScript对象,其配置告诉GraphQL如何处理它们。 让我们以以下GraphQL查询为例:

query {
    blogPosts {
        _id,
        title
    }
    comments {
        text   
    }
}

为了使GrahpQL了解如何使用此查询,我们需要定义blogPostscomments查询。 因此,我们的queries变量将如下所示:

{
    blogPosts: {...},
    comments: {...}
}

mutations 。 这说明在查询或变异中具有的键与在查询中放入的名称之间存在直接关系。 现在让我们看看如何定义这些查询和变异中的每一个。

查询

从查询开始,让我们从到目前为止使用我们创建的模型的示例中进行学习。 一个很好的例子是获取博客文章及其所有评论。

在REST解决方案中,您必须为此发出两个HTTP请求。 一个获得博客文章,另一个获得评论,看起来像这样:

GET /api/blog-post/[some-blog-post-id]
GET /api/comments?postId='[some-blog-post-id]'

在GraphQL中,我们只能通过以下查询在一个HTTP请求中进行此操作:

query ($postId: ID!) {
    blogPost (id: $postId) {
        title,
        description
    }
    comments (postId: $postId) {
        text
    }
}

我们可以在一个请求中获取所需的所有数据,仅此一项就可以提高性能。 我们也可以要求我们要使用的确切属性。 在上面的示例中,响应仅带有博客文章的titledescription ,而评论仅带有text

仅从每种资源中检索所需的字段,可能会对网页或应用程序的加载时间产生重大影响。 例如,让我们看一下注释,它们也具有_idpostId属性。 这些中的每一个都很小, 每个都是12字节,这是准确的 (不计入对象密钥)。 当它是一个或几个评论时,这几乎没有影响。 当我们谈论200条评论时,超过了4800个字节,我们甚至不会使用。 这可能会大大缩短应用程序的加载时间。 这对于资源有限的设备(例如移动设备,通常具有较慢的网络连接)特别重要。

为此,我们需要告诉GraphQL如何获取每个特定查询的数据。 让我们看一个查询定义的例子:

// graphql/queries/blog-post/single.js
import {
  GraphQLList,
  GraphQLID,
  GraphQLNonNull
} from 'graphql';
import {Types} from 'mongoose';

import blogPostType from '../../types/blog-post';
import getProjection from '../../get-projection';
import BlogPostModel from '../../../models/blog-post';

export default {
  type: blogPostType,
  args: {
    id: {
      name: 'id',
      type: new GraphQLNonNull(GraphQLID)
    }
  },
  resolve (root, params, options) {
    const projection = getProjection(options.fieldASTs[0]);

    return BlogPostModel
      .findById(params.id)
      .select(projection)
      .exec();
  }
};

在这里,我们正在创建一个查询,该查询根据id参数检索单个博客文章。 请注意,我们指定的是先前创建的type ,用于验证查询的输出。 我们还为该查询设置了带有所需参数的args对象。 最后,是一个resolve函数,我们在其中查询数据库并返回数据。

为了进一步优化数据获取过程并利用mongoDB上的投影功能,我们正在处理GraphQL提供给我们的AST,以生成与猫鼬兼容的投影。 因此,如果我们进行以下查询:

query ($postId: ID!) {
    blogPost (id: $postId) {
        title,
        description
    }
}

由于我们只需要从数据库中获取titledescription ,因此getProjection函数将生成猫鼬有效的投影:

{
    title: 1,
    description: 1
}

您可以在源代码中的graphql/queries/*查看其他查询。 因为它们都与上面的示例相似,所以我们不会逐一介绍。

变异

突变是将处理数据库中某种更改的操作。 像查询一样,我们可以在一个HTTP请求中将不同的突变分组。 通常,一个动作是孤立的,例如“添加评论”或“创建博客帖子”。 尽管随着应用程序和数据收集的复杂性不断提高(用于分析,用户体验测试或复杂的操作),网站或应用程序上的用户操作可能会触发数据库中不同资源的大量变更。 按照我们的示例,对博客文章的新评论可能意味着新评论和博客文章评论计数的更新。 在REST解决方案中,您将具有以下内容:

POST /api/blog-post/increment-comment
POST /api/comment/new

使用GraphQL,您只能在一个HTTP请求中执行以下操作:

mutation ($postId: ID!, $comment: String!) {
    blogPostCommentInc (id: $postId)
    addComment (postId: $postId, comment: $comment) {
        _id
    }
}

请注意,查询和突变的语法是完全一样的,唯一的改变querymutation 。 我们可以从变异中查询数据,就像查询中一样。 通过不指定片段,就像上面查询blogPostCommentInc ,我们只是要求返回true或false返回值,这通常足以确认操作。 或者,我们可以要求一些数据,就像addComment突变一样,这对于仅检索服务器上生成的数据很有用。

然后让我们在服务器中定义我们的变异。 完全按照查询创建突变:

// graphql/mutations/blog-post/add.js
import {
  GraphQLNonNull,
  GraphQLBoolean
} from 'graphql';

import blogPostInputType from '../../types/blog-post-input';
import BlogPostModel from '../../../models/blog-post';

export default {
  type: GraphQLBoolean,
  args: {
    data: {
      name: 'data',
      type: new GraphQLNonNull(blogPostInputType)
    }
  },
  async resolve (root, params, options) {
    const blogPostModel = new BlogPostModel(params.data);
    const newBlogPost = await blogPostModel.save();

    if (!newBlogPost) {
      throw new Error('Error adding new blog post');
    }
    return true;
  }
};

此突变将添加新的博客文章,如果成功,则返回true 。 注意在type ,我们如何指定要返回的内容。 在args ,从突变得到的论点。 resolve()功能与查询定义中的功能完全相同。

测试GraphQL端点

现在,我们已经使用GraphQL路由以及一些查询和变异创建了Express服务器,让我们通过向其发送一些请求进行测试。

有很多方法可以将GET或POST请求发送到某个位置,例如:

  • 浏览器 –通过在浏览器中键入URL,您正在发送GET请求。 这具有无法发送POST请求的局限性
  • cURL –适用于命令行爱好者。 它可以将任何类型的请求发送到服务器。 尽管它不是最佳界面,但是您无法保存请求,并且需要在命令行中编写所有内容,从我的角度来看这并不理想
  • GraphiQLGraphQL的绝佳解决方案。 它是一个浏览器中的IDE,可用于创建对服务器的查询。 它具有一些很棒的功能,例如:语法突出显示和提前键入

除了上述解决方案以外,还有更多解决方案。 前两个是最著名和最常用的。 GraphiQL是GraphQL团队的解决方案,可简化GraphQL的流程,因为查询的编写可能更加复杂。

从这三个方面,我将推荐GraphiQL,尽管我更喜欢并推荐所有Postman 。 该工具绝对是API测试的一项进步。 它提供了一个直观的界面,您可以在其中创建和保存任何类型的请求的集合。 您甚至可以为您的API创建测试,然后单击一下按钮即可运行它们。 它还具有协作功能,可以共享请求集合。 因此,我创建了一个您可以在此处下载的文件 ,然后将其导入Postman。 如果您没有安装Postman,我绝对建议您这样做。

让我们从运行服务器开始。 您应该安装节点4或更高版本; 如果尚未安装,建议使用nvm进行安装。 然后,我们可以在命令行中运行以下命令:

$ git clone https://github.com/sitepoint-editors/graphql-nodejs.git
$ cd graphql-nodejs
$ npm install
$ npm start

现在,服务器已准备就绪,可以接收请求,因此让我们在Postman上创建一些请求。 我们的GraphQL路由是在/graphql上设置的,因此首先要做的是将位置设置为我们要将请求定向到的位置,即http://localhost:8080/graphql 。 然后,我们需要指定它是GET还是POST请求。 尽管您可以使用其中任何一种,但我更喜欢POST,因为它不会影响URL,从而使其更整洁。 我们还需要配置请求所附带的标头,在本例中,我们只需要添加与application/json相等的Content-Type即可。 这是Postman中所有设置的外观:

邮递员头设置

现在,我们可以创建主体,该主体将具有我们的GraphQL查询和所需的JSON格式的变量,如下所示:

邮递员GraphQL请求正文示例

假设您已经导入了我提供的集合,则应该已经有一些可以测试的查询和变异请求。 由于我使用了硬编码的Mongo ID,因此请按顺序运行请求,它们都应该成功。 分析我放在每个人体内的内容,您会发现它只是本文所讨论内容的一种应用。 另外,如果您多次运行第一个请求,由于它是重复的ID,您可以看到如何返回错误:

邮递员GraphQL错误响应示例

结论

在本文中,我们介绍了GraphQL的潜力以及它与REST体系结构风格的区别。 这种新的查询语言将对网络产生重大影响。 特别是对于更复杂的数据应用程序,现在可以准确描述所需的数据并通过一个HTTP请求进行请求。

我希望收到您的来信:您如何看待GraphQL,您对此有何经验?

From: https://www.sitepoint.com/creating-graphql-server-nodejs-mongodb/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值