引言

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

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

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

一些 GraphQL 特性包括:

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

目标

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

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

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

先决条件

  • Js 在本地安装,你可以按照如何安装 Node.js 和创建一个本地开发环境来做。

步骤1ー使用 Node 设置 GraphQL

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

首先创建一个 GraphQL 目录:

  • mkdir GraphQL

切换到新目录:

  • cd GraphQL

初始化 npm 项目:

  • npm init -y

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

  • touch server.js

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

必要的软件包将在本教程中讨论,因为它们是实现的。 接下来,使用 Express 和 Express-graphql (HTTP 服务器中间件)设置一个服务器:

  • npm i graphql express express-graphql -S

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

server.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'));

这个代码片段完成了几件事情。 它使用要求包括已安装的包。 它还初始化一般的模式和根值。 此外,它在 / graphql 上创建了一个端点,可以通过 web 浏览器访问这个端点。

做出这些更改后保存并关闭文件。

如果 Node 服务器没有运行,请启动它:

  • node server.js

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

访问 localhost: 4000 / graphql in a web browser. You will see a Welcome to graphql web interface。

在左侧会有一个窗格,您将在那里输入查询。 有一个额外的窗格用于输入查询变量,您可能需要拖动并调整查看的大小。 右边的窗格将显示执行查询的结果。 此外,执行查询可以通过按下播放图标的按钮来完成。

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

第二步ー定义模式

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

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

在 GraphQL 模式语言中,你可以用 id、姓名和年龄来表示一个用户,就像下面这个例子:

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

在 JavaScript 中,您将使用 buildSchema 函数,该函数从 GraphQL 模式语言构建 Schema 对象。 如果你代表上面的同一个用户,它看起来像这个例子:

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

构造类型

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

通过添加一些合理的类型,您将使模式变得有点复杂。 例如,您希望返回一个用户和 Person 类型的用户数组,这些用户具有 id、姓名、年龄和他们最喜欢的 shark 属性。

用这个新的 Schema 对象替换 server.js 中模式的原有代码行:

server.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!) 也就是说必须提供身份证。 用户查询接受一个可选的 shark 变量。

第三步ー定义解析器

解析器负责将操作映射到实际的函数。 在查询类型内部,有一个称为用户的操作。 您可以将此操作映射到根中具有相同名称的函数。

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

将这些新的代码行添加到 server.js,在 buildSchema 代码行之后,但在代码根行之前:

server.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) { 
  // ...
}
...

用这个新对象替换 server.js 中根目录的原有代码行:

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

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

将前面添加到 server.js 中的 getUser 和 retrieveUsers 代码行替换为:

server.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 的操作来获得一个具有他们的名字、年龄和最喜欢的鲨鱼的用户。 我们可以有选择地指定,我们只需要他们的名字,如果我们不需要年龄和鲨鱼。

根据官方文档,通过名称识别代码库中的查询比通过解密内容更容易。

这个查询没有提供所需的 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" } } }

这将返回一个 id 为1,Brian 的用户。 它还返回请求的名称、年龄和 shark 字段。

第四步ー定义别名

在需要检索两个不同用户的情况下,您可能想知道如何识别每个用户。 在 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" } } }

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

第五步: 创建片段

上面的查询没有那么糟糕,但是它有一个问题; 我们正在为 userA 和 userB 重复相同的字段。 我们可以找到一些可以让我们的查询干燥的东西。 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 的片段,该片段只能应用于 Person 类型,然后使用它来检索用户。

第六步ー定义指令

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

  • @ include (if: Boolean)-如果参数为真,则只在结果中包含该字段。
  • @ Skip (if: Boolean)-如果参数为 true,则跳过该字段。

假设你想找回锤头鲨的粉丝,但是要包括他们的 id 并跳过他们的年龄段。 您可以使用变量来传入 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 } ] } }

这将返回两个用户与鲨鱼价值匹配的锤头鲨鱼-信念和喜悦。

第七步ー定义突变

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

让我们集中讨论一些如何进行突变的例子。 例如,我们希望用 id 1更新用户并更改其年龄、名称,然后返回新的用户详细信息。

更新您的模式,除了预先存在的代码行之外,还包含一个变异类型:

server.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
  }
`);

在 getUser 和 retrieveUsers 之后,添加一个新的 updateUser 函数来处理更新用户:

server.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];
}

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

server.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" } } }

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

Id 为1的用户已从 Brian (21岁)更新为 Keavin (27岁)。

总结

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

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