前言
学一学 GraphQL
参考文章: https://chenyitian.gitbooks.io/graphql/content/introduction.html
参考网站: https://www.howtographql.com/basics/0-introduction/
GraphQL
基础
1. 什么是GraphQL
GraphQL
是一种用于API
的查询语言,同时也是一种运行在服务端的执行引擎。它使用基于类型系统的模式定义和验证查询结构(类型系统由你的数据定义)。GraphQL
不依赖任何特定数据库或存储引擎,而是通过现有代码和数据来支持查询
为什么使用GraphQL
- 灵活的数据获取: 客户端可以请求确切的数据结构,避免数据过多或不足的问题
- 单一端点: 所有查询通过一个端点完成,简化了客户端与服务端的交互
- 强类型系统: 定义
API
时使用强类型,可以捕获开发错误,增强工具支持(如自动生成文档和代码) - 实时更新: 通过订阅(
subscriptions
),支持实时数据更新,客户端在数据变化时能自动接收更新
与REST
的对比
与REST
的对比 (优点)
- 避免多次请求:
REST
中可能需要多次请求不同资源才能获取所需数据,而GraphQL
能通过单一查询获取所有相关数据 - 精确的数据获取: 客户端明确指定需要的字段,避免多余数据传输,适用于带宽有限或网络不稳定的环境
与REST
的对比 (缺点)
- 查询解析复杂: 由于
GraphQL
查询的灵活性,解析和执行查询可能比REST
更复杂,尤其在大规模数据集或复杂业务逻辑中 - 缓存困难: 与
REST
固定URL
不同,GraphQL
查询不是静态的,因此标准HTTP
缓存机制难以直接应用,需要额外设计缓存策略 - 学习曲线: 😅
2. 字段Fields
在GraphQL
中,字段 (fields
) 是定义在类型 (type
) 上的属性,表示类型包含的数据内容。字段有以下特点
- 每个字段都有名称和返回类型,类型可以是标量(如
String
、Int
)、对象、枚举、列表等 - 字段可以接受参数,用于传递附加信息或过滤数据
字段的基本定义
type User { id: ID! name: String! age: Int }
type Query { user(id: ID!): User }
|
User
类型
- 包含
id
、name
和age
三个字段 - 每个字段定义了
User
类型的数据结构
Query
类型
- 定义了一个查询字段
user
,可以根据id
获取一个User
什么是Schema
Schema
是GraphQL
的核心概念,用于描述API
的结构,包括- 类型定义
Type
: 定义API
中的类型,例如标量类型 (String
、Int
)、对象类型 (User
)、枚举类型等 - 根类型
Root Types
: 包括Query
、Mutation
和Subscription
,分别定义了查询、变更和订阅操作
解析器Resolver
- 字段通常需要解析器来告诉
GraphQL
如何获取字段的值。当客户端请求某个字段时,GraphQL
会调用对应的解析器来获取数据
const resolvers = { Query: { user: (parent, args, context, info) => { const userId = args.id; return getUserById(userId); }, }, };
|
完整查询示例
query { user(id: "1") { id name age } }
|
- 定义
Schema
: 描述数据类型和操作 - 编写
Resolver
: 实现具体的数据获取逻辑 - 客户端查询: 通过
Query
请求数据,GraphQL
调用解析器返回结果
Mutation
示例
- 下面是一个使用变更操作 (
Mutation
) 创建用户的示例
type User { id: ID! name: String! age: Int }
type Mutation { createUser(name: String!, age: Int): User }
|
const resolvers = { Mutation: { createUser: (parent, args, context, info) => { const newUser = { id: generateUniqueId(), name: args.name, age: args.age, }; saveUserToDatabase(newUser); return newUser; }, }, };
|
mutation { createUser(name: "Alice", age: 30) { id name age } }
|
3. 参数Arguments
在GraphQL
中,参数 (Arguments
) 用于向字段或操作(如查询Query
和变更Mutation
)传递输入数据。参数允许客户端在请求中提供额外信息,用于筛选、过滤或指定要操作的数据
参数的定义
- 参数可以定义在
GraphQL schema
中的字段上。参数可以是- 必需参数: 使用
!
符号表示,必须提供 - 可选参数: 没有
!
符号的参数,可以省略
type User { id: ID! name: String! age: Int }
type Query { user(id: ID!): User users(age: Int): [User] }
|
user
字段有一个必需的id
参数,用于指定要查询的用户users
字段有一个可选的age
参数,用于筛选特定年龄的用户- 方括号
[ ]
表示返回值是一个列表类型
解析器中的参数
- 在解析器 (
Resolver
) 中,参数通过args
对象传递,可以使用这些参数来筛选或操作数据
const resolvers = { Query: { user: (parent, args, context, info) => { return getUserById(args.id); }, users: (parent, args, context, info) => { if (args.age) { return getUsersByAge(args.age); } return getAllUsers(); }, }, };
|
args.id
和args.age
是客户端请求中传递的参数- 在
user
查询中,通过id
获取单个用户 - 在
users
查询中,根据age
筛选用户列表,如果未提供age
参数,则返回所有用户
客户端查询
query { user(id: "1") { id name age } users(age: 30) { id name age } }
|
- 第一部分请求
id
为"1"
的用户 - 第二部分请求所有
age
为30
的用户
变更中的参数
参数在变更操作(Mutation
)中用于提供操作所需的输入数据,例如创建一个新用户
type Mutation { createUser(name: String!, age: Int): User }
|
const resolvers = { Mutation: { createUser: (parent, args, context, info) => { const newUser = { id: generateUniqueId(), name: args.name, age: args.age, }; saveUserToDatabase(newUser); return newUser; }, }, };
|
mutation { createUser(name: "Alice", age: 30) { id name age } }
|
- 客户端传递参数
name
和age
,服务器根据参数创建用户并返回新增用户的数据
GraphQL
核心功能
4. 别名Aliases
在GraphQL
中,别名 (Aliases
) 是一种用于重命名查询结果中字段名称的功能。它允许客户端
- 避免命名冲突: 当查询结果包含多个同名字段时,使用别名区分不同的字段调用
- 自定义响应格式: 通过别名修改返回数据中的字段名称,使其更符合客户端的需求
避免命名冲突
- 当需要多次查询同一个字段的不同实例时,别名可以为每个字段调用提供不同的名称。例如
query { user1: user(id: "1") { id name age } user2: user(id: "2") { id name age } }
|
user1
和user2
是别名,用于区分user
字段的两次调用, 这避免了因字段名称重复而导致的命名冲突
定义响应格式
- 别名可以改变返回数据中的字段名称,使其更符合业务需求或提高可读性
{ "data": { "user1": { "id": "1", "name": "Alice", "age": 30 }, "user2": { "id": "2", "name": "Bob", "age": 25 } } }
|
user1
和user2
成为响应数据中的字段名称,分别包含两个不同用户的信息
5. 片段Fragments
片段 (Fragments
) 是GraphQL
中的一种机制,用于在多个查询或操作中重用一组字段选择。通过定义片段,可以减少字段的重复定义,提高查询的可读性和可维护性
片段的语法
fragment FragmentName on TypeName { field1 field2 ... }
|
FragmentName
: 片段的名称,用于在查询中引用片段TypeName
: 片段适用的类型,指定字段所属的对象类型field1, field2, ...
: 片段中包含的字段集合
片段的使用示例
假设我们有一个User
类型,包含id
、name
和age
字段,以下示例展示了如何定义和使用片段
fragment UserFields on User { id name age }
|
query { user1: user(id: "1") { ...UserFields } user2: user(id: "2") { ...UserFields } }
|
{ "data": { "user1": { "id": "1", "name": "Alice", "age": 30 }, "user2": { "id": "2", "name": "Bob", "age": 25 } } }
|
嵌套片段
片段可以嵌套使用,在一个片段中引用另一个片段,从而灵活地组织字段
fragment AddressFields on Address { street city country }
fragment UserFields on User { id name age address { ...AddressFields } }
|
query { user(id: "1") { ...UserFields } }
|
{ "data": { "user": { "id": "1", "name": "Alice", "age": 30, "address": { "street": "123 Main St", "city": "New York", "country": "USA" } } } }
|
内联片段
内联片段 (Inline Fragments
) 是GraphQL
中的一种机制,用于在查询中根据对象的实际类型动态选择字段。它们主要用于处理接口 (Interface
) 或联合类型 (Union Type
),当返回的数据类型不确定时,可以根据具体类型选择不同的字段
... on TypeName { field1 field2 }
|
Schema
示例: 假设有一个Person
接口,两个实现类型Employee
和Customer
interface Person { id: ID! name: String! }
type Employee implements Person { id: ID! name: String! salary: Int }
type Customer implements Person { id: ID! name: String! purchaseHistory: [String] }
type Query { persons: [Person] }
|
- 查询示例: 根据
Person
的具体类型选择不同的字段
query { persons { id name ... on Employee { salary } ... on Customer { purchaseHistory } } }
|
{ "data": { "persons": [ { "id": "1", "name": "Alice", "salary": 50000 }, { "id": "2", "name": "Bob", "purchaseHistory": ["item1", "item2"] } ] } }
|
6. 操作名称
在GraphQL
中,操作名称 (Operation Name
) 是一个可选的标识,用于明确标注特定的查询 (Query
)、变更 (Mutation
) 或订阅 (Subscription
) 操作的名称。它有助于提高可读性、调试效率,以及在某些工具中实现特定功能
操作名称特点
- 操作名称的位置
- 紧跟在操作类型(如
query
、mutation
或subscription
)之后
- 命名规则
- 名称只能包含字母、数字和下划线
(_)
- 名称不能以数字开头
- 用途
- 提供查询的上下文,明确查询意图
- 在调试、日志记录和工具(如
Apollo Client
)中,用于识别特定的操作 - 当一个请求中包含多个操作时,必须使用操作名称
操作名称示例
- 查询示例:
GetUser
是操作名称,用于标识此查询操作
query GetUser { user(id: "1") { id name age } }
|
- 变更示例:
CreateUser
是操作名称,表明此变更的目的为创建用户
mutation CreateUser { createUser(name: "Alice", age: 30) { id name age } }
|
- 多操作示例: 当一个请求中包含多个操作时,必须使用操作名称进行区分
query GetUser { user(id: "1") { id name age } }
mutation UpdateUser { updateUser(id: "1", name: "Alice") { id name age } }
|
- 匿名操作: 如果操作名称被省略,该操作称为匿名操作, 但在一个请求中不能包含多个匿名操作,因为没有操作名称用于区分
query { user(id: "1") { id name age } }
|
7. 操作类型
在GraphQL
中,操作类型 (Operation Type
) 是指客户端可以在API
请求中执行的三种操作: 查询 (Query
)、变更 (Mutation
) 和 订阅 (Subscription
)。每种操作类型对应不同的用途,分别用于读取数据、修改数据和监听实时数据更新
操作类型介绍
- 查询
Query
- 用途: 用于读取和获取数据
- 特点: 不会对服务器数据产生任何修改,仅返回请求的数据 (类似于
REST API
的GET
请求)
query { user(id: "1") { id name age } }
<!-- 查询 user 数据,根据参数 id: "1" 获取特定用户的信息 --> <!-- 返回的字段包括 id、name 和 age -->
|
- 变更
Mutation
- 用途: 用于修改服务器上的数据,包括创建、更新或删除操作
- 特点: 与查询不同,变更操作会改变服务器的数据状态 (类似于
REST API
的POST
、PUT
、PATCH
和 DELETE
请求)
mutation { createUser(name: "Alice", age: 30) { id name } }
<!-- 调用 createUser 变更操作,创建一个新用户 --> <!-- 参数 name 和 age 提供新用户的基本信息 --> <!-- 返回结果包括新用户的 id 和 name -->
|
- 订阅
Subscription
- 用途: 用于监听服务器上的事件,并在事件发生时实时接收更新
- 特点: 用于实时数据更新场景,如实时聊天、股票行情等。订阅操作会保持一个持久的连接(通常使用
WebSocket
),当事件发生时,服务器会主动推送更新给客户端
subscription { userAdded { id name age } }
<!-- 订阅 userAdded 事件,当有新用户被添加时,服务器会推送更新数据 --> <!-- 返回结果包含新用户的 id、name 和 age -->
|
三种操作类型的比较
操作类型 | 用途 | 是否修改数据 | 使用场景 |
---|
查询 | 读取和获取数据 | 否 | 获取用户、产品列表、详情页数据 |
变更 | 修改服务器数据 | 是 | 创建、更新、删除资源 |
订阅 | 监听事件并实时接收数据更新 | 否 | 实时聊天、通知、数据流 |
8. 查询Query
查询 (Query
) 是GraphQL
中的一种操作类型,用于从服务器获取数据。它与传统REST API
的GET
请求类似,但在灵活性和强大性方面超越了后者
查询的定义
- 在
GraphQL
中,查询类型通常在schema
中定义为Query
类型。Query
类型描述了所有可执行的查询操作
type Query { user(id: ID!): User users: [User] }
type User { id: ID! name: String! age: Int }
|
查询解析器Resolver
- 解析器负责实现字段的具体逻辑,定义如何获取数据。以下是查询解析器的示例
const users = [ { id: '1', name: 'Alice', age: 30 }, { id: '2', name: 'Bob', age: 25 }, ];
const resolvers = { Query: { user: (parent, args, context, info) => { return users.find(user => user.id === args.id); }, users: () => users, }, };
|
user
: 根据传入的id
参数,从users
列表中查找并返回对应用户users
: 直接返回所有用户
查询的特点
- 精确的数据获取: 客户端可以指定所需字段,避免传输多余数据
query { user(id: "1") { name age } }
|
{ "data": { "user": { "name": "Alice", "age": 30 } } }
|
query { user(id: "1") { id name } users { id name } }
|
{ "data": { "user": { "id": "1", "name": "Alice" }, "users": [ { "id": "1", "name": "Alice" }, { "id": "2", "name": "Bob" } ] } }
|
9. 变更Mutations
变更 (Mutations
) 是GraphQL
中的一种操作类型,专门用于对服务器端的数据进行修改操作。与查询 (Query
) 仅用于读取数据不同,变更支持创建、更新或删除数据。每个变更操作都可以定义所需的输入参数和返回结果的结构
变更的定义
- 在
GraphQL
的schema
中,变更类型通常定义在Mutation
类型中,用来描述所有支持的数据修改操作
type Mutation { createUser(name: String!, age: Int!): User updateUser(id: ID!, name: String, age: Int): User deleteUser(id: ID!): Boolean }
type User { id: ID! name: String! age: Int }
|
createUser
: 接收name
和age
参数,返回新创建的User
对象updateUser
: 接收id
(必需)、name
和age
参数,用于更新用户信息,返回更新后的User
deleteUser
: 接收id
参数,用于删除指定用户,返回布尔值表示操作是否成功
变更的解析器
- 解析器定义了如何执行变更操作,通常与数据库或其他数据存储交互
const { v4: uuidv4 } = require('uuid');
const users = [ { id: '1', name: 'Alice', age: 30 }, { id: '2', name: 'Bob', age: 25 }, ];
const resolvers = { Mutation: { createUser: (parent, args, context, info) => { const newUser = { id: uuidv4(), name: args.name, age: args.age, }; users.push(newUser); return newUser; }, updateUser: (parent, args, context, info) => { const user = users.find(user => user.id === args.id); if (!user) { throw new Error("User not found"); } if (args.name !== undefined) { user.name = args.name; } if (args.age !== undefined) { user.age = args.age; } return user; }, deleteUser: (parent, args, context, info) => { const userIndex = users.findIndex(user => user.id === args.id); if (userIndex === -1) { throw new Error("User not found"); } users.splice(userIndex, 1); return true; }, }, };
|
变更的使用
客户端可以通过以下查询发送变更请求
mutation { createUser(name: "Alice", age: 30) { id name age } }
|
{ "data": { "createUser": { "id": "c81e728d-59f2-4d16-9f02-fcad84756789", "name": "Alice", "age": 30 } } }
|
mutation { updateUser(id: "1", name: "Alice Updated") { id name age } }
|
{ "data": { "updateUser": { "id": "1", "name": "Alice Updated", "age": 30 } } }
|
mutation { deleteUser(id: "2") }
|
{ "data": { "deleteUser": true } }
|
10. 订阅Subscription
订阅 (Subscription
) 是GraphQL
中的一种操作类型,允许客户端订阅服务器上的事件,并在事件发生时实时接收更新数据。这种机制特别适合实时更新场景,如聊天消息、通知、股票价格等
订阅的工作原理
- 建立订阅连接: 客户端发送订阅请求到服务器,指定要订阅的事件或数据
- 保持连接: 服务器与客户端之间通过
WebSocket
或其他双向通信协议维持持久连接 - 事件触发: 当服务器端的事件发生变化(如新增或更新数据)时,触发对应的订阅逻辑
- 数据推送: 服务器将更新的数据通过连接推送到订阅的客户端
订阅示例
type User { id: ID! name: String! age: Int }
type Subscription { userAdded: User }
type Mutation { createUser(name: String!, age: Int!): User }
|
const { PubSub } = require('graphql-subscriptions'); const pubsub = new PubSub();
const resolvers = { Subscription: { userAdded: { subscribe: () => pubsub.asyncIterator(['USER_ADDED']), }, }, Mutation: { createUser: (parent, args, context, info) => { const newUser = { id: generateUniqueId(), name: args.name, age: args.age, };
saveUserToDatabase(newUser);
pubsub.publish('USER_ADDED', { userAdded: newUser });
return newUser; }, }, };
|
subscription { userAdded { id name age } }
|
- 当有新用户通过
createUser
变更操作添加时,服务器会触发userAdded
事件,客户端将接收到用户信息
Apollo Client
订阅
- 配置
Apollo Client
: 创建一个支持查询、变更和订阅的客户端
import { ApolloClient, InMemoryCache, split } from '@apollo/client'; import { WebSocketLink } from '@apollo/client/link/ws'; import { HttpLink } from '@apollo/client'; import { getMainDefinition } from '@apollo/client/utilities';
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });
const wsLink = new WebSocketLink({ uri: 'ws://localhost:4000/graphql', options: { reconnect: true, }, });
const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ); }, wsLink, httpLink, );
const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache(), });
|
- 订阅操作: 使用
Apollo Client
的useSubscription Hook
实现订阅
import { gql, useSubscription } from '@apollo/client';
const USER_ADDED_SUBSCRIPTION = gql` subscription OnUserAdded { userAdded { id name age } } `;
function UserList() { const { data, loading, error } = useSubscription(USER_ADDED_SUBSCRIPTION);
if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>;
return ( <div> <h2>New User Added:</h2> <p>ID: {data.userAdded.id}</p> <p>Name: {data.userAdded.name}</p> <p>Age: {data.userAdded.age}</p> </div> ); }
export default UserList;
|
- 总而言之,
GraphQL
的Subscription
依赖于WebSocket
来实现。WebSocket
提供了双向持久连接的能力,使得服务器能够在事件发生时立即向客户端推送数据,而Subscription
则定义了这些事件和数据的结构。因此,Subscription
不是替代WebSocket
,而是利用WebSocket
来实现特定的功能和需求
GraphQL
进阶
11. 变量 Variables
在GraphQL
中,变量 (Variables
) 是一种机制,用于动态传递参数到查询 (Query
)、变更 (Mutation
) 或订阅 (Subscription
) 中。通过使用变量,可以使查询更加灵活、可重用,并且避免将用户输入直接嵌入到查询字符串中,从而提升安全性和可读性
变量的定义与使用
- 查询中的变量: 变量需要在查询语句中定义,并在执行查询时传入实际的值
query GetUser($id: ID!) { user(id: $id) { id name age } }
<!-- $id 是一个变量,表示用户的 ID --> <!-- 变量的类型为 ID!,! 表示这是一个必需字段 -->
|
- 变量在
React
中的使用: 使用Apollo Client
的useQuery Hook
动态传递变量值
import { gql, useQuery } from '@apollo/client';
const GET_USER = gql` query GetUser($id: ID!) { user(id: $id) { id name age } } `;
function User({ userId }) { const { loading, error, data } = useQuery(GET_USER, { variables: { id: userId }, });
if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>;
return ( <div> <h2>User Details</h2> <p>ID: {data.user.id}</p> <p>Name: {data.user.name}</p> <p>Age: {data.user.age}</p> </div> ); }
export default User;
|
- 变更中的变量: 在变更操作中,变量同样可以动态传递参数
mutation CreateUser($name: String!, $age: Int!) { createUser(name: $name, age: $age) { id name age } }
|
import { gql, useMutation } from '@apollo/client';
const CREATE_USER = gql` mutation CreateUser($name: String!, $age: Int!) { createUser(name: $name, age: $age) { id name age } } `;
function CreateUserForm() { const [createUser, { data, loading, error }] = useMutation(CREATE_USER);
const handleSubmit = async (e) => { e.preventDefault(); const name = e.target.name.value; const age = parseInt(e.target.age.value, 10);
await createUser({ variables: { name, age } }); };
if (loading) return <p>Submitting...</p>; if (error) return <p>Error: {error.message}</p>;
return ( <form onSubmit={handleSubmit}> <input name="name" placeholder="Name" required /> <input name="age" placeholder="Age" type="number" required /> <button type="submit">Create User</button> {data && <p>Created User: {data.createUser.name}</p>} </form> ); }
export default CreateUserForm;
|
12. 指令 Directives
指令 (Directives
) 是GraphQL
中的一种强大功能,用于动态影响查询、变更 (Mutation
) 或订阅 (Subscription
) 的执行行为。指令可以应用于字段、片段等,帮助客户端或服务器灵活调整查询结果的生成逻辑。例如,可以根据条件决定是否包含某字段或动态修改返回结果
内置指令
GraphQL
规范中定义了两个常用的内置指令: @include
和@skip
@include
: 条件包含- 用于根据条件动态包含字段或片段
- 接受一个名为
if
的参数,该参数的值为布尔类型
query GetUser($withAge: Boolean!) { user(id: "1") { id name age @include(if: $withAge) } }
<!-- 如果变量 $withAge 为 true,则返回结果中包含 age 字段 --> <!-- 如果 $withAge 为 false,则排除 age 字段 -->
|
@skip
: 条件跳过- 用于根据条件跳过字段或片段
- 同样接受一个名为
if
的布尔参数
query GetUser($withoutAge: Boolean!) { user(id: "1") { id name age @skip(if: $withoutAge) } }
<!-- 如果变量 $withoutAge 为 true,则跳过 age 字段 --> <!-- 如果 $withoutAge 为 false,则包含 age 字段 -->
|
自定义指令
除了内置指令,GraphQL
还支持自定义指令。这些指令可以用于更复杂的场景,比如数据验证、字段格式化等。自定义指令需要在schema
中声明,并提供对应的实现逻辑
directive @upper on FIELD_DEFINITION
type Query { greeting: String @upper }
<!-- @upper 是一个自定义指令,应用在字段定义上 --> <!-- on FIELD_DEFINITION 指定了该指令适用于字段定义 -->
|
- 自定义指令的实现: 使用
graphql-tools
创建一个@upper
指令,将字符串字段的返回值转换为大写
const { SchemaDirectiveVisitor } = require('graphql-tools'); const { defaultFieldResolver } = require('graphql');
class UpperCaseDirective extends SchemaDirectiveVisitor { visitFieldDefinition(field) { const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) { const result = await resolve.apply(this, args); if (typeof result === 'string') { return result.toUpperCase(); } return result; }; } }
module.exports = UpperCaseDirective;
|
- 整合指令到
Schema
: 自定义指令@upper
应用于greeting
字段,返回的值将自动转换为大写
const { gql } = require('apollo-server'); const UpperCaseDirective = require('./UpperCaseDirective');
const typeDefs = gql` directive @upper on FIELD_DEFINITION
type Query { greeting: String @upper } `;
const resolvers = { Query: { greeting: () => "hello world", }, };
const { makeExecutableSchema } = require('graphql-tools');
const schema = makeExecutableSchema({ typeDefs, resolvers, schemaDirectives: { upper: UpperCaseDirective, }, });
module.exports = schema;
|
{ "data": { "greeting": "HELLO WORLD" } }
|
附录
参考文章: https://chenyitian.gitbooks.io/graphql/content/introduction.html
参考网站: https://www.howtographql.com/basics/0-introduction/