Node.js和Express的实用GraphQL入门指南
介绍
GraphQL 是 Facebook 创建的一种查询语言,旨在基于直观和灵活的语法构建客户端应用程序,以描述其数据需求和交互。 通过在这些类型上定义类型和字段,然后为每种类型上的每个字段提供函数来创建 GraphQL 服务。
一旦 GraphQL 服务运行(通常在 Web 服务的 URL 上),它可以接收 GraphQL 查询以验证和执行。 首先检查接收到的查询以确保它仅引用定义的类型和字段,然后运行提供的函数以产生结果。
在本教程中,我们将使用 Express 实现一个 GraphQL 服务器,并使用它来学习重要的 GraphQL 特性。
GraphQL 的一些功能包括:
- 分层 - 查询看起来与它们返回的数据完全一样。
- 客户端指定的查询 - 客户端可以自由决定从服务器获取什么。
- 强类型 - 您可以在执行前在 GraphQL 类型系统中从语法上验证查询。 这也有助于利用强大的工具来改善开发体验,例如 GraphiQL。
- Introspective - 您可以使用 GraphQL 语法本身来查询类型系统。 这非常适合将传入数据解析为强类型接口,而不必处理解析和手动将 JSON 转换为对象的问题。
目标
传统 REST 调用的主要挑战之一是客户端无法请求自定义(有限或扩展)数据集。 在大多数情况下,一旦客户端向服务器请求信息,它要么获取所有字段,要么不获取任何字段。
另一个困难是工作和维护多个端点。 随着平台的发展,数量也会随之增加。 因此,客户端经常需要从不同的端点请求数据。 GraphQL API 是根据类型和字段组织的,而不是端点。 您可以从单个端点访问数据的全部功能。
在构建 GraphQL 服务器时,只需要一个 URL 用于所有数据获取和变异。 因此,客户端可以通过向服务器发送一个描述他们想要什么的查询字符串来请求一组数据。
先决条件
- Node.js 安装在本地,您可以按照【X57X】如何安装Node.js 并创建本地开发环境【X126X】进行。
第 1 步 — 使用 Node 设置 GraphQL
您将从创建基本文件结构和示例代码片段开始。
首先,创建一个GraphQL目录:
mkdir GraphQL
切换到新目录:
cd GraphQL
初始化一个 npm 项目:
npm init -y
然后创建 server.js 文件,这将是主文件:
touch server.js
您的项目应类似于以下内容:
必要的包将在本教程中讨论,因为它们已实现。 接下来,使用 HTTP 服务器中间件 Express 和 express-graphql 设置服务器:
npm install graphql express express-graphql
在文本编辑器中打开 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'));
注意: 此代码是用 express-graphql 的早期版本编写的。 在 v0.10.0 之前,您可以使用 var graphqlHTTP = require('express-graphql');。 v0.10.0之后,需要使用var { graphqlHTTP } = require('express-graphql');。
这个片段完成了几件事。 它使用 require 来包含已安装的软件包。 它还初始化通用 schema 和 root 值。 此外,它在 /graphql 处创建一个端点,可以使用 Web 浏览器访问该端点。
进行这些更改后保存并关闭文件。
如果节点服务器未运行,请启动它:
node server.js
注意: 在本教程中,您将对 server.js 进行更新,这将需要重新启动节点服务器以反映最新更改。
在网络浏览器中访问 localhost:4000/graphql。 您将看到一个 Welcome to GraphiQL Web 界面。
左侧将有一个窗格,您将在其中输入查询。 有一个额外的窗格用于输入您可能需要拖动和调整大小才能查看的查询变量。 右侧的窗格将显示执行查询的结果。 此外,可以通过按下带有 play 图标的按钮来执行查询。
到目前为止,我们已经探索了 GraphQL 的一些特性和优势。 在下一节中,我们将深入研究 GraphQL 中一些技术特性的不同术语和实现。 我们将使用 Express 服务器来练习这些功能。
第 2 步 — 定义模式
在 GraphQL 中,Schema 管理查询和突变,定义允许在 GraphQL 服务器中执行的内容。 模式定义了 GraphQL API 的类型系统。 它描述了客户可以访问的一整套可能的数据(对象、字段、关系等)。 来自客户端的调用会根据模式进行验证和执行。 客户端可以通过 introspection 找到有关模式的信息。 架构驻留在 GraphQL API 服务器上。
GraphQL 接口定义语言 (IDL) 或模式定义语言 (SDL) 是指定 GraphQL Schema 的最简洁方法。 GraphQL 模式最基本的组件是对象类型,它表示我们可以从服务中获取的一种对象,以及它具有哪些字段。
在 GraphQL 模式语言中,您可以用 id、name 和 age 来表示 user,如下例所示:
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 和一个 Person 类型的 users 数组,它们有一个 id、name、 age,以及他们最喜欢的 shark 属性。
用这个新的 Schema 对象替换 server.js 中 schema 的现有代码行:
服务器.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 变量。
第三步——定义解析器
解析器负责将操作映射到实际功能。 在 type Query 内部,有一个称为 users 的操作。 您将此操作映射到 root 中具有相同名称的函数。
您还将为此功能创建一些示例用户。
在 buildSchema 代码行之后、root 代码行之前将这些新代码行添加到 server.js:
服务器.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 中 root 的现有代码行:
服务器.js
// Root resolver
var root = {
user: getUser, // Resolver function to return user with specific id
users: retrieveUsers
};
为了使代码更具可读性,请创建单独的函数,而不是将所有内容都堆积在根解析器中。 这两个函数都采用可选的 args 参数,该参数携带来自客户端查询的变量。 让我们为解析器提供一个实现并测试它们的功能。
将您之前添加到 server.js 的 getUser 和 retrieveUsers 的代码行替换为以下代码:
服务器.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 的操作来获取单个用户及其 name、age 和最喜欢的 shark。 仅当我们不需要 age 和 shark 时,我们可以选择指定我们需要它们的 name。
根据 官方文档 ,通过名称而不是通过解密内容来识别代码库中的查询是最容易的。
此查询未提供所需的 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"
}
}
}
这将返回与 1、Brian 的 id 匹配的单个用户。 它还返回请求的 name、age 和 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"
}
}
}
现在我们可以使用他们的字段正确识别每个用户。
第 5 步 - 创建片段
上面的查询并没有那么糟糕,但它有一个问题; 我们为 userA 和 userB 重复相同的字段。 我们可以找到使我们的查询 DRY 的东西。 GraphQL 包含称为 fragments 的可重用单元,可让您构建字段集,然后将它们包含在您需要的查询中。
在 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 的粉丝,但包含他们的 id 并跳过他们的 age 字段。 您可以使用变量来传递 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 Shark–Faith 和 Joy 匹配。
第 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
}
`);
在 getUser 和 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"
}
}
}
在更新用户的突变后,您将获得新的用户详细信息。
1 的 id 用户已从 Brian (age 21) 更新为 Keavin (age 27) .
结论
在本指南中,您从一些相当复杂的示例中介绍了 GraphQL 的基本概念。 这些示例中的大多数都为与 REST 交互的用户揭示了 GraphQL 和 REST 之间的差异。
要了解更多关于 GraphQL 的信息,请查看 官方文档 。