使用 Node.js 和 Express 的实用 GraphQL 入门指南

简介

GraphQL是Facebook创建的一种查询语言,目的是基于直观和灵活的语法构建客户端应用程序,以描述其数据需求和交互。GraphQL服务是通过定义类型和这些类型上的字段,然后为每个类型上的每个字段提供函数来创建的。

一旦GraphQL服务运行(通常位于Web服务上的URL),它就可以接收要验证和执行的GraphQL查询。首先检查接收到的查询,以确保它只引用定义的类型和字段,然后运行所提供的函数以产生结果。

在本教程中,我们将使用Express)实现一个图形QL服务器,并使用它来学习重要的图形QL功能。

GraphQL

GraphQL的一些功能包括:

  • 分层-查询看起来与它们返回的数据一模一样。
  • 客户端指定的查询-客户端可以自由指定要从服务器获取的内容。
  • 强类型-您可以在执行之前在语法上和GraphQL类型系统内验证查询。这还有助于利用可改善开发体验的强大工具,如GraphiQL。
  • 内省-您可以使用GraphQL语法本身来查询类型系统。这对于将传入数据解析为强类型接口非常有用,而不必处理解析和手动将JSON转换为对象。

目标

传统REST调用的主要挑战之一是客户端无法请求定制(有限或扩展)的数据集。在大多数情况下,一旦客户端从服务器请求信息,它要么获取所有字段,要么不获取任何字段。

另一个困难是工作和维护多个端点。随着平台的发展,这个数字也会随之增加。因此,客户端经常需要从不同的端点请求数据。GraphQL API是根据类型和字段组织的,而不是根据端点组织的。您可以从单个终端访问数据的全部功能。

在构建GraphQL服务器时,只需要使用一个URL来获取和更改所有数据。因此,客户端可以通过向服务器发送描述其所需内容的查询字符串来请求一组数据。

前提条件

第一步-使用Node设置GraphQL

您将从创建基本的文件结构和示例代码片段开始。

首先,创建一个GraphQL目录:

1mkdir GraphQL

切换到新目录:

1cd GraphQL

初始化npm项目:

1npm init -y

然后创建将作为主文件的server.js文件:

1touch server.js

您的项目应该类似于以下内容:

目录contents.列表

本教程将在实现必要的包时对其进行讨论。接下来,使用Expressexpress-graphql,)设置服务器。

1npm install graphql express express-graphql

在文本编辑器中打开server.js,并添加以下代码行:

 1[label server.js]
 2var express = require('express');
 3var graphqlHTTP = require('express-graphql');
 4var { buildSchema } = require('graphql');
 5
 6// Initialize a GraphQL schema
 7var schema = buildSchema(`
 8  type Query {
 9    hello: String
10  }
11`);
12
13// Root resolver
14var root = { 
15  hello: () => 'Hello world!'
16};
17
18// Create an express server and a GraphQL endpoint
19var app = express();
20app.use('/graphql', graphqlHTTP({
21  schema: schema,  // Must be provided
22  rootValue: root,
23  graphiql: true,  // Enable GraphiQL when server endpoint is accessed in browser
24}));
25app.listen(4000, () => console.log('Now browse to localhost:4000/graphql'));

<$>[备注] 注意: 此代码是用较早版本的express-graph ql编写的。在v0.10.0之前,您可以使用var graph qlHTTP=Required(‘Express-GraphQL’);。V0.10.0之后,需要使用var{graph qlHTTP}=Required(‘Express-GraphQL’);。 <$>

这段代码完成了几件事。它使用``来包含已安装的包。它还初始化泛型的schemaroot值。此外,它还在/graph ql处创建了一个端点,可以通过Web浏览器访问该端点。

进行这些更改后,保存并关闭该文件。

如果节点服务器未运行,请启动该服务器:

1node server.js

<$>[备注] 注意: 在本教程中,您将对server.js进行更新,这将需要重新启动节点服务器以反映最新的更改。 <$>

在Web浏览器中访问本地主机:4000/graph ql。您将看到一个欢迎使用GraphiQL Web界面。

左侧将有一个窗格,您可以在其中输入查询。还有一个用于输入查询变量的附加窗格,您可能需要拖动这些变量并调整其大小才能查看。右侧的窗格将显示执行查询的结果。此外,还可以通过按下带有播放 图标的按钮来执行查询。

GraphiQL Web interface的屏幕截图

到目前为止,我们已经探索了GraphQL的一些特性和优势。在下一节中,我们将深入研究GraphQL中一些技术特性的不同术语和实现。我们将使用Express服务器来实践这些功能。

第二步-定义模式

在GraphQL中,Schema管理查询和变化,定义允许在GraphQL服务器中执行的内容。模式定义了GraphQL API的类型系统。它描述了可能的数据(对象、字段、关系等)的完整集合。客户端可以访问。来自客户端的调用将根据模式进行验证和执行。客户端可以通过introspection找到有关模式的信息。schema驻留在GraphQL API服务器上。

GraphQL接口定义语言(IDL)或架构定义语言(SDL)是指定GraphQL架构的最简明的方法。GraphQL模式最基本的组件是对象类型,它表示我们可以从我们的服务中获取的一种对象,以及它具有哪些字段。

在GraphQL模式语言中,您可以使用idnameage表示一个user,如下例所示:

1type User {
2  id: ID!
3  name: String!
4  age: Int
5}

在JavaScript中,您可以使用BuildSchema函数,该函数从GraphQL模式语言构建一个模式对象。如果要表示上面相同的用户,则如下所示:

1var schema = buildSchema(`
2  type User {
3    id: Int
4    name: String!
5    age: Int
6  }
7`);

构造类型

您可以在buildSchema中定义不同的类型,在大多数情况下,您可能会注意到type Query {...}类型Mutation {...} 类型查询{...}是一个对象,包含将映射到GraphQL查询的函数,用于获取数据(相当于REST中的GET)。type Mutation {...}保存将映射到变化的函数,用于创建、更新或删除数据(相当于REST中的POST、UPDATE和RESTs)。

通过添加一些合理的类型,您将使您的模式变得有点复杂。例如,您希望返回一个user和一个Person类型的users数组,这些users具有idnameage和他们最喜欢的Shark属性。

用这个新的架构对象替换server.js中的schema的已有代码行:

 1[label server.js]
 2// Initialize a GraphQL schema
 3var schema = buildSchema(`
 4  type Query {
 5    user(id: Int!): Person
 6    users(shark: String): [Person]
 7  },
 8  type Person {
 9    id: Int
10    name: String
11    age: Int
12    shark: String
13  }
14`);

您可能注意到上面的一些有趣的语法,[Person]表示返回Person类型的数组,而user(id:int!)中的感叹号表示必须提供idusers查询接受一个可选的Shark变量。

第三步-定义解析器

解析器负责将操作映射到实际函数。在type Query中,您有一个名为users的操作。您可以将此操作映射到root中具有相同名称的函数。

您还将为该功能创建一些样例用户。

将这些新代码行添加到server.js中,紧靠在代码行BuildSchema之后,但在代码行root之前:

 1[label server.js]
 2...
 3// Sample users
 4var users = [
 5  {
 6    id: 1,
 7    name: 'Brian',
 8    age: '21',
 9    shark: 'Great White Shark'
10  },
11  {
12    id: 2,
13    name: 'Kim',
14    age: '22',
15    shark: 'Whale Shark'
16  },
17  {
18    id: 3,
19    name: 'Faith',
20    age: '23',
21    shark: 'Hammerhead Shark'
22  },
23  {
24    id: 4,
25    name: 'Joseph',
26    age: '23',
27    shark: 'Tiger Shark'
28  },
29  {
30    id: 5,
31    name: 'Joy',
32    age: '25',
33    shark: 'Hammerhead Shark'
34  }
35];
36
37// Return a single user
38var getUser = function(args) {
39  // ...
40}
41
42// Return a list of users
43var retrieveUsers = function(args) { 
44  // ...
45}
46...

server.jsroot的原有代码行替换为以下新对象:

1[label server.js]
2// Root resolver
3var root = { 
4  user: getUser,  // Resolver function to return user with specific id
5  users: retrieveUsers
6};

要使代码更具可读性,请创建单独的函数,而不是将所有内容堆积在根解析器中。这两个函数都有一个可选的args参数,该参数携带来自客户端查询的变量。让我们为解析器提供一个实现并测试它们的功能。

将您之前添加到server.js中的getUserreceseUsers代码行替换为:

 1[label server.js]
 2// Return a single user (based on id)
 3var getUser = function(args) {
 4  var userID = args.id;
 5  return users.filter(user => user.id == userID)[0];
 6}
 7
 8// Return a list of users (takes an optional shark parameter)
 9var retrieveUsers = function(args) {
10  if (args.shark) {
11    var shark = args.shark;
12    return users.filter(user => user.shark === shark);
13  } else {
14    return users;
15  }
16}

在Web界面的输入窗格中输入以下查询:

1query getSingleUser {
2  user {
3    name
4    age
5    shark
6  }
7}

您将收到以下输出:

 1[secondary_label Output]
 2{
 3  "errors": [
 4    {
 5      "message": "Cannot query field \"user\" on type \"Query\".",
 6      "locations": [
 7        {
 8          "line": 2,
 9          "column": 3
10        }
11      ]
12    }
13  ]
14}

在上面的示例中,我们使用名为getSingleUser的操作来获取单个用户的nameage和收藏的Shark。我们可以选择性地指定只有在不需要ageShark的情况下才需要它们的名称

根据官方documentation,]的说法,通过名称识别代码库中的查询最容易,而不是通过解密内容。

该查询没有提供所需的id,而GraphQL给出了一条描述性错误消息。我们现在将进行正确的查询。注意变量和参数的使用。

在Web界面中,将输入窗格的内容替换为以下更正后的查询:

1query getSingleUser($userID: Int!) {
2  user(id: $userID) {
3    name
4    age
5    shark
6  }
7}

仍在Web界面中时,将变量窗格的内容替换为以下内容:

1[secondary_label Query Variables]
2{
3  "userID": 1
4}

您将收到以下输出:

 1[secondary_label Output]
 2{
 3  "data": {
 4    "user": {
 5      "name": "Brian",
 6      "age": 21,
 7      "shark": "Great White Shark"
 8    }
 9  }
10}

这将返回与1id匹配的单个用户Brian。它还返回请求的nameageshark字段。

Step 4 -定义别名

在需要检索两个不同用户的情况下,您可能想知道如何识别每个用户。在GraphQL中,你不能用不同的参数直接查询同一个字段。我们来演示一下。

在Web界面中,将输入窗格的内容替换为以下内容:

 1query getUsersWithAliasesError($userAID: Int!, $userBID: Int!) {
 2  user(id: $userAID) {
 3    name
 4    age
 5    shark
 6  },
 7  user(id: $userBID) {
 8    name
 9    age
10    shark
11  }
12}

仍在Web界面中时,将变量窗格的内容替换为以下内容:

1[secondary_label Query Variables]
2{
3  "userAID": 1,
4  "userBID": 2
5}

您将收到以下输出:

 1[secondary_label Output]
 2{
 3  "errors": [
 4    {
 5      "message": "Fields \"user\" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.",
 6      "locations": [
 7        {
 8          "line": 2,
 9          "column": 3
10        },
11        {
12          "line": 7,
13          "column": 3
14        }
15      ]
16    }
17  ]
18}

错误是描述性的,甚至建议使用别名。让我们纠正一下实现。

在Web界面中,将输入窗格的内容替换为以下更正后的查询:

 1query getUsersWithAliases($userAID: Int!, $userBID: Int!) {
 2  userA: user(id: $userAID) {
 3    name
 4    age
 5    shark
 6  },
 7  userB: user(id: $userBID) {
 8    name
 9    age
10    shark
11  }
12}

当仍在Web界面中时,请确保变量窗格包含以下内容:

1[secondary_label Query Variables]
2{
3  "userAID": 1,
4  "userBID": 2
5}

您将收到以下输出:

 1[secondary_label Output]
 2{
 3  "data": {
 4    "userA": {
 5      "name": "Brian",
 6      "age": 21,
 7      "shark": "Great White Shark"
 8    },
 9    "userB": {
10      "name": "Kim",
11      "age": 22,
12      "shark": "Whale Shark"
13    }
14  }
15}

现在,我们可以通过每个用户的字段正确地识别他们。

第五步-创建分片

上面的查询并没有那么糟糕,但它有一个问题;我们正在为userAuserB重复相同的字段。我们可以找到一些可以使我们的查询成为DRY.的东西GraphQL包括称为_Fragments的可重用单元,它允许您构造字段集,然后在需要的地方将它们包括在查询中。

在Web界面中,将变量窗格的内容替换为以下内容:

 1query getUsersWithFragments($userAID: Int!, $userBID: Int!) {
 2  userA: user(id: $userAID) {
 3    ...userFields
 4  },
 5  userB: user(id: $userBID) {
 6    ...userFields
 7  }
 8}
 9
10fragment userFields on Person {
11  name
12  age
13  shark
14}

当仍在Web界面中时,请确保变量窗格包含以下内容:

1[secondary_label Query Variables]
2{ 
3  "userAID": 1,
4  "userBID": 2
5}

您将收到以下输出:

 1[secondary_label Output]
 2{
 3  "data": {
 4    "userA": {
 5      "name": "Brian",
 6      "age": 21,
 7      "shark": "Great White Shark"
 8    },
 9    "userB": {
10      "name": "Kim",
11      "age": 22,
12      "shark": "Whale Shark"
13    }
14  }
15}

您已经创建了一个名为userFields的片段,该片段只能应用于类型Person,然后使用它来检索用户。

第六步-定义指令

指令使我们能够使用变量动态更改查询的结构和形状。在某些情况下,您可能希望在不更改架构的情况下跳过或包含某些字段。两个可用的指令如下所示:

  • @Include(if:boolean)-如果参数为真,则只在结果中包含此字段。
  • @Skip(if:boolean)-如果参数为真,则跳过此字段。

假设您想要检索的用户是锤头鲨‘的粉丝,但包括他们的id并跳过他们的age字段。您可以使用变量传入Shark`,并使用指令实现包含和跳过功能。

在Web界面中,清除输入窗格并添加以下内容:

 1query getUsers($shark: String, $age: Boolean!, $id: Boolean!) {
 2  users(shark: $shark){
 3    ...userFields
 4  }
 5}
 6
 7fragment userFields on Person {
 8  name
 9  age @skip(if: $age)
10  id @include(if: $id)
11}

仍在Web界面中时,清除变量窗格并添加以下内容:

1[secondary_label Query Variables]
2{
3  "shark": "Hammerhead Shark",
4  "age": true,
5  "id": true
6}

您将收到以下输出:

 1[secondary_label Output]
 2{
 3  "data": {
 4    "users": [
 5      {
 6        "name": "Faith",
 7        "id": 3
 8      },
 9      {
10        "name": "Joy",
11        "id": 5
12      }
13    ]
14  }
15}

这将返回两个Shark值匹配Hammerhead Shark的用户--FaithJoy

Step 7-定义突变

到目前为止,我们一直在处理查询,即检索数据的操作。突变是GraphQL中处理创建、删除和更新数据的第二个主要操作。

让我们集中讨论一些如何进行突变的例子。例如,我们想更新一个id == 1的用户,并更改他们的年龄,名字,然后返回新的用户详细信息。

更新您的模式,以包括除了预先存在的代码行之外的突变类型:

 1[label server.js]
 2// Initialize a GraphQL schema
 3var schema = buildSchema(`
 4  type Query {
 5    user(id: Int!): Person
 6    users(shark: String): [Person]
 7  },
 8  type Person {
 9    id: Int
10    name: String
11    age: Int
12    shark: String
13  }
14  # newly added code
15  type Mutation {
16    updateUser(id: Int!, name: String!, age: String): Person
17  }
18`);

getUserreceseUsers之后,添加一个新的updateUser函数来处理用户的更新:

 1[label server.js]
 2// Update a user and return new user details
 3var updateUser = function({id, name, age}) {
 4  users.map(user => {
 5    if (user.id === id) {
 6      user.name = name;
 7      user.age = age;
 8      return user;
 9    }
10  });
11  return users.filter(user => user.id === id)[0];
12}

另外,使用相关的解析器函数更新根解析器:

1[label server.js]
2// Root resolver
3var root = { 
4  user: getUser,
5  users: retrieveUsers,
6  updateUser: updateUser  // Include mutation function in root resolver
7};

假设以下是初始用户详细信息:

 1[secondary_label Output]
 2{
 3  "data": {
 4    "user": {
 5      "name": "Brian",
 6      "age": 21,
 7      "shark": "Great White Shark"
 8    }
 9  }
10}

在Web界面中,将以下查询添加到输入窗格:

 1mutation updateUser($id: Int!, $name: String!, $age: String) {
 2  updateUser(id: $id, name:$name, age: $age){
 3    ...userFields
 4  }
 5}
 6
 7fragment userFields on Person {
 8  name
 9  age
10  shark
11}

仍在Web界面中时,清除变量窗格并添加以下内容:

1[secondary_label Query Variables]
2{
3  "id": 1,
4  "name": "Keavin",
5  "age": "27"
6}

您将收到以下输出:

 1[secondary_label Output]
 2{
 3  "data": {
 4    "updateUser": {
 5      "name": "Keavin",
 6      "age": 27,
 7      "shark": "Great White Shark"
 8    }
 9  }
10}

在突变以更新用户之后,您将获得新的用户详细信息。

id1的用户已从Brian(年龄21)更新为Keavin(年龄27)。

结论

在本指南中,您已经通过一些相当复杂的示例介绍了GraphQL的基本概念。对于与REST交互的用户来说,这些示例中的大多数都揭示了GraphQL和REST之间的区别。

要了解有关图形QL的更多信息,请查看官方文档.

Published At
Categories with 技术
comments powered by Disqus