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