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

介绍

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

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

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

图形语言

一些 GraphQL 功能包括:

  • 分层 – 查询看起来与它们返回的数据完全一样。

  • 客户端指定的查询 – 客户端可以自由决定从服务器获取什么。

  • 强类型 – 您可以在执行之前在语法上和在 GraphQL 类型系统内验证查询。这也有助于利用可改善开发体验的强大工具,例如 GraphiQL。

  • 自省 – 您可以使用 GraphQL 语法本身查询类型系统。这对于将传入数据解析为强类型接口非常有用,而不必处理解析和手动将 JSON 转换为对象。

目标

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

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

在构建 GraphQL 服务器时,所有数据的获取和变异只需要一个 URL。因此,客户端可以通过向服务器发送一个查询字符串来请求一组数据,描述他们想要什么。

先决条件

第 1 步 – 使用 Node 设置 GraphQL

您将首先创建一个基本的文件结构和一个示例代码片段。

首先创建一个GraphQL目录:

  • mkdir GraphQL

切换到新目录:

  • cd GraphQL

初始化一个npm项目:

  • npm init -y

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

  • touch server.js

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

目录内容列表。

本教程将在实现时讨论必要的包。接下来,使用Expressexpress-graphqlHTTP 服务器中间件设置服务器:

  • npm i graphql express express-graphql -S

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

服务器.js
var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');

// Initialize a GraphQL schema
var schema = buildSchema(`
  type Query {
    hello: String
  }
`);

// Root resolver
var root = { 
  hello: () => 'Hello world!'
};

// Create an express server and a GraphQL endpoint
var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,  // Must be provided
  rootValue: root,
  graphiql: true,  // Enable GraphiQL when server endpoint is accessed in browser
}));
app.listen(4000, () => console.log('Now browse to localhost:4000/graphql'));

这个片段完成了几件事。它用于require包含已安装的软件包。它还初始化泛型schemaroot值。此外,它还创建了一个/graphql可以使用 Web 浏览器访问的端点

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

如果节点服务器未运行,则启动它:

  • node server.js

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

localhost:4000/graphql在网络浏览器中访问您将看到欢迎使用 GraphiQL Web 界面。

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

GraphiQL Web 界面的屏幕截图

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

第 2 步 – 定义架构

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

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

在GraphQL模式语言,你可能是一个user具有idnameage这样的例子:

type User {
  id: ID!
  name: String!
  age: Int
}

在 JavaScript 中,您将使用buildSchema从 GraphQL 模式语言构建 Schema 对象函数。如果您要表示user上述内容,则它看起来像这个例子:

var schema = buildSchema(`
  type User {
    id: Int
    name: String!
    age: Int
  }
`);

构造类型

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

通过添加一些合理的类型,您将使您的模式变得有点复杂。例如,你想返回user和数组的users类型的Person,谁拥有idnameage,和自己喜欢的shark属性。

用这个新的 Schema 对象替换schemain的预先存在的代码行server.js

服务器.js
// Initialize a GraphQL schema
var schema = buildSchema(`
  type Query {
    user(id: Int!): Person
    users(shark: String): [Person]
  },
  type Person {
    id: Int
    name: String
    age: Int
    shark: String
  }
`);

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

第 3 步 – 定义解析器

解析器负责将操作映射到实际功能。在里面type Query,您有一个名为users. 您将此操作映射到内部具有相同名称的函数root

您还将为此功能创建一些示例用户。

添加的代码,这些新行server.js权后buildSchema的代码行,但前root行的代码:

服务器.js
...
// Sample users
var users = [
  {
    id: 1,
    name: 'Brian',
    age: '21',
    shark: 'Great White Shark'
  },
  {
    id: 2,
    name: 'Kim',
    age: '22',
    shark: 'Whale Shark'
  },
  {
    id: 3,
    name: 'Faith',
    age: '23',
    shark: 'Hammerhead Shark'
  },
  {
    id: 4,
    name: 'Joseph',
    age: '23',
    shark: 'Tiger Shark'
  },
  {
    id: 5,
    name: 'Joy',
    age: '25',
    shark: 'Hammerhead Shark'
  }
];

// Return a single user
var getUser = function(args) {
  // ...
}

// Return a list of users
var retrieveUsers = function(args) { 
  // ...
}
...

用这个新对象替换rootin的预先存在的代码行server.js

服务器.js
// Root resolver
var root = { 
  user: getUser,  // Resolver function to return user with specific id
  users: retrieveUsers
};

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

您之前添加的getUser代码行替换为以下内容:retrieveUsersserver.js

服务器.js
// Return a single user (based on id)
var getUser = function(args) {
  var userID = args.id;
  return users.filter(user => user.id == userID)[0];
}

// Return a list of users (takes an optional shark parameter)
var retrieveUsers = function(args) {
  if (args.shark) {
    var shark = args.shark;
    return users.filter(user => user.shark === shark);
  } else {
    return users;
  }
}

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

query getSingleUser {
  user {
    name
    age
    shark
  }
}

您将收到以下输出:

Output
{ "errors": [ { "message": "Cannot query field \"user\" on type \"Query\".", "locations": [ { "line": 2, "column": 3 } ] } ] }

在上面的例子中,我们使用命名的操作getSingleUser来获得单个用户与他们的nameage和喜爱sharkname如果我们不需要ageand ,我们可以选择指定我们需要它们shark

根据官方文档,通过名称而不是解密内容来识别代码库中的查询是最简单的。

此查询未提供所需的信息,id并且 GraphQL 为我们提供了描述性错误消息。我们现在将进行正确的查询。注意变量和参数的使用。

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

query getSingleUser($userID: Int!) {
  user(id: $userID) {
    name
    age
    shark
  }
}

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

Query Variables
{ "userID": 1 }

您将收到以下输出:

Output
{ "data": { "user": { "name": "Brian", "age": 21, "shark": "Great White Shark" } } }

这将返回一个单一用户匹配id1Brian它还返回请求nameage以及shark多个领域。

第 4 步 – 定义别名

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

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

query getUsersWithAliasesError($userAID: Int!, $userBID: Int!) {
  user(id: $userAID) {
    name
    age
    shark
  },
  user(id: $userBID) {
    name
    age
    shark
  }
}

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

Query Variables
{ "userAID": 1, "userBID": 2 }

您将收到以下输出:

Output
{ "errors": [ { "message": "Fields \"user\" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.", "locations": [ { "line": 2, "column": 3 }, { "line": 7, "column": 3 } ] } ] }

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

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

query getUsersWithAliases($userAID: Int!, $userBID: Int!) {
  userA: user(id: $userAID) {
    name
    age
    shark
  },
  userB: user(id: $userBID) {
    name
    age
    shark
  }
}

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

Query Variables
{ "userAID": 1, "userBID": 2 }

您将收到以下输出:

Output
{ "data": { "userA": { "name": "Brian", "age": 21, "shark": "Great White Shark" }, "userB": { "name": "Kim", "age": 22, "shark": "Whale Shark" } } }

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

第 5 步 – 创建片段

上面的查询并没有那么糟糕,但它有一个问题;我们正在重复上是相同的领域userAuserB我们可以找到使我们的查询DRY 的东西GraphQL 包含称为片段的可重用单元,可让您构建字段集,然后将它们包含在需要的查询中。

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

query getUsersWithFragments($userAID: Int!, $userBID: Int!) {
  userA: user(id: $userAID) {
    ...userFields
  },
  userB: user(id: $userBID) {
    ...userFields
  }
}

fragment userFields on Person {
  name
  age
  shark
}

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

Query Variables
{ "userAID": 1, "userBID": 2 }

您将收到以下输出:

Output
{ "data": { "userA": { "name": "Brian", "age": 21, "shark": "Great White Shark" }, "userB": { "name": "Kim", "age": 22, "shark": "Whale Shark" } } }

您创建了一个userFields只能应用的片段type Person,然后用它来检索用户。

步骤 6 — 定义指令

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

  • @include(if: Boolean)– 如果参数为真,则仅在结果中包含此字段。
  • @skip(if: Boolean)– 如果参数为真,则跳过此字段。

假设您要检索 的粉丝Hammerhead Shark,但包括他们idage字段并跳过他们的字段。您可以使用变量来传递shark包含和跳过功能的指令和使用指令。

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

query getUsers($shark: String, $age: Boolean!, $id: Boolean!) {
  users(shark: $shark){
    ...userFields
  }
}

fragment userFields on Person {
  name
  age @skip(if: $age)
  id @include(if: $id)
}

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

Query Variables
{ "shark": "Hammerhead Shark", "age": true, "id": true }

您将收到以下输出:

Output
{ "data": { "users": [ { "name": "Faith", "id": 3 }, { "name": "Joy", "id": 5 } ] } }

这将返回两个shark值匹配的用户Hammerhead SharkFaithJoy

第 7 步 – 定义突变

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

让我们关注一些如何进行突变的例子。例如,我们想要更新用户id == 1并更改他们的age, name,然后返回新用户的详细信息。

更新您的架构以包含除预先存在的代码行之外的突变类型:

服务器.js
// Initialize a GraphQL schema
var schema = buildSchema(`
  type Query {
    user(id: Int!): Person
    users(shark: String): [Person]
  },
  type Person {
    id: Int
    name: String
    age: Int
    shark: String
  }
  # newly added code
  type Mutation {
    updateUser(id: Int!, name: String!, age: String): Person
  }
`);

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

服务器.js
// Update a user and return new user details
var updateUser = function({id, name, age}) {
  users.map(user => {
    if (user.id === id) {
      user.name = name;
      user.age = age;
      return user;
    }
  });
  return users.filter(user => user.id === id)[0];
}

还使用相关的解析器功能更新根解析器:

服务器.js
// Root resolver
var root = { 
  user: getUser,
  users: retrieveUsers,
  updateUser: updateUser  // Include mutation function in root resolver
};

假设这些是初始用户详细信息:

Output
{ "data": { "user": { "name": "Brian", "age": 21, "shark": "Great White Shark" } } }

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

mutation updateUser($id: Int!, $name: String!, $age: String) {
  updateUser(id: $id, name:$name, age: $age){
    ...userFields
  }
}

fragment userFields on Person {
  name
  age
  shark
}

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

Query Variables
{ "id": 1, "name": "Keavin", "age": "27" }

您将收到以下输出:

Output
{ "data": { "updateUser": { "name": "Keavin", "age": 27, "shark": "Great White Shark" } } }

在更改更新用户后,您将获得新用户的详细信息。

与用户id1已经从更新Brianage 21)至Keavinage 27)。

结论

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

要了解有关 GraphQL 的更多信息,请查看官方文档

觉得文章有用?

点个广告表达一下你的爱意吧 !😁