前言

参考文章: 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需要GET /users/1GET /users/1/posts两次请求,GraphQL只需一次查询同时获取用户和文章数据
  • 精确的数据获取: 客户端明确指定需要的字段 (如只要nameage,不要email),避免多余数据传输,特别适用于移动端或弱网环境

缺点

  • 查询解析复杂: 服务器需要解析客户端自定义的查询结构、验证类型、执行深度检查,比REST固定端点的处理逻辑更复杂
  • 缓存困难: REST使用URL作为缓存键 (如GET /users/1),而GraphQL所有请求都是POST /graphql,查询内容在请求体中,无法利用标准HTTP缓存,需要使用 Apollo Client 等工具自行实现缓存策略
  • 学习曲线: 需要掌握 Schema 设计、类型系统、Resolver 实现、N+1 查询问题等新概念,比REST的学习成本更高

2. 字段Fields

GraphQL中,字段 (fields) 是定义在类型 (type) 上的属性,表示类型包含的数据内容。字段有以下特点

  • 每个字段都有名称和返回类型,类型可以是标量(如StringInt)、对象、枚举、列表等
  • 字段可以接受参数,用于传递附加信息或过滤数据

字段的基本定义

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

type Query {
user(id: ID!): User
}
  • User类型: 定义用户数据结构,包含idnameage三个字段,其中!表示字段为必需(非空)
  • Query类型: 定义查询操作user,通过必需参数id查询并返回对应的User对象

客户端查询示例

query {
user(id: "1") {
id
name
age
}
}
  • 客户端传入参数id: "1",请求该用户的idnameage字段
  • 服务器返回对应用户的数据

什么是Schema

  • SchemaGraphQL的核心概念,用于描述API的结构。它定义了客户端可以查询的所有类型和操作
# 定义用户类型
type User {
id: ID!
name: String!
age: Int
}

# 定义查询操作
type Query {
user(id: ID!): User
users: [User]
}

# 定义变更操作
type Mutation {
createUser(name: String!, age: Int!): User
}

# Schema 将所有类型组合起来
schema {
query: Query
mutation: Mutation
}
  • Schema的组成部分
    • 类型定义Type: 定义数据结构,如User类型包含idnameage字段
    • 根类型Root Types: Query(查询)、Mutation(变更)、Subscription(订阅)
    • schema声明: 指定哪些类型作为入口点(通常可以省略,默认使用QueryMutationSubscription

解析器Resolver

  • 解析器定义了如何获取Schema中每个字段的实际数据。当客户端请求某个字段时,GraphQL会调用对应的解析器
// 模拟数据源(实际项目中通常来自数据库)
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: () => {
return users;
},
},
};
  • 解析器函数接收 4 个参数
    • parent: 上一级解析器的返回值(嵌套查询时使用)
    • args: 客户端传入的参数对象(如{ id: "1" }
    • context: 所有解析器共享的上下文对象(如数据库连接、用户认证信息)
    • info: 当前查询的元数据信息(字段名、路径等)

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: ID!): id是必需参数,必须提供才能查询
    • users(age: Int): age是可选参数,可以用于筛选特定年龄的用户
    • [User]: 方括号表示返回值是用户列表

解析器中的参数

  • 在解析器 (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
}
}
  • 查询说明
    • user(id: "1"): 请求id"1"的单个用户
    • users(age: 30): 请求所有age30的用户列表

Mutation中的参数

  • 参数在变更操作中用于提供执行所需的输入数据(如创建用户)

  • 示例代码(按执行顺序: Schema -> Resolver -> 客户端请求)

# Schema -----------------
type Mutation {
createUser(name: String!, age: Int): User
}

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

# Resolver (JavaScript) ---
const resolvers = {
Mutation: {
createUser: (parent, args, context, info) => {
const newUser = {
id: generateUniqueId(),
name: args.name,
age: args.age,
};
saveUserToDatabase(newUser);
return newUser;
},
},
};

# Client Request ----------
mutation {
createUser(name: "Alice", age: 30) {
id
name
age
}
}
  • 执行流程说明
    • 客户端传递name和可选age参数到createUser
    • 解析器生成包含唯一id的新用户对象
    • 保存用户数据到持久化存储
    • 返回新增用户的完整数据(idnameage

GraphQL核心功能

4. 别名Aliases

  • 别名 (Aliases) 用于在响应中重命名字段,解决同字段多次请求的冲突并调整返回结构

  • 使用场景

    • 避免命名冲突: 多次查询同一字段但参数不同
    • 自定义响应结构: 让返回键名更贴近业务语义

避免命名冲突

  • 为每次对同一字段的不同参数请求添加不同别名,避免响应中键名重复
query {
user1: user(id: "1") {
id
name
age
}
user2: user(id: "2") {
id
name
age
}
}
  • 示例说明
    • user1: user(id: "1"): 使用别名user1标识第一条user查询
    • user2: user(id: "2"): 使用别名user2标识第二条user查询
    • 服务端仍调用同一个user解析器两次,客户端通过不同键名接收结果,无命名冲突

定义响应格式

  • 别名可以改变返回数据中的字段名称,使其更符合业务需求或提高可读性
{
"data": {
"user1": {
"id": "1",
"name": "Alice",
"age": 30
},
"user2": {
"id": "2",
"name": "Bob",
"age": 25
}
}
}
  • user1user2成为响应数据中的字段名称,分别包含两个不同用户的信息

5. 片段Fragments

  • 片段用于复用一组字段选择,避免在多个查询/变更/订阅中重复书写相同字段,提升可读性与维护性
  • 使用价值
    • 减少重复: 一处定义,多处引用
    • 保持一致: 保证不同查询返回字段结构一致
    • 易于修改: 修改片段即可同步更新所有使用位置

片段的语法

fragment FragmentName on TypeName {
field1
field2
...
}
  • 语法说明
    • FragmentName: 片段名称,后续通过...FragmentName引用
    • on TypeName: 指定该片段绑定的类型(只能在该类型或其实现处使用)
    • 片段体: 需要复用的字段集合

片段的使用示例

  • 场景: 多次查询User不同实例但需要相同字段集合
  • 示例代码(片段定义 -> 查询使用 -> 返回结果)
# Fragment Definition -----------------
fragment UserFields on User {
id
name
age
}

# Query Using Fragment ----------------
query {
user1: user(id: "1") {
...UserFields
}
user2: user(id: "2") {
...UserFields
}
}

# Result JSON -------------------------
{
"data": {
"user1": {
"id": "1",
"name": "Alice",
"age": 30
},
"user2": {
"id": "2",
"name": "Bob",
"age": 25
}
}
}

嵌套片段

  • 通过在一个片段内部引用另一个片段,实现复杂对象字段的分层复用
  • 适用场景: 对象包含子对象,需要分别维护子对象字段集合
  • 示例代码(片段定义 -> 查询使用 -> 返回结果)
# Fragment Definitions -----------------
fragment AddressFields on Address {
street
city
country
}

fragment UserFields on User {
id
name
age
address {
...AddressFields
}
}

# Query Using Nested Fragment ----------
query {
user(id: "1") {
...UserFields
}
}

# Result JSON --------------------------
{
"data": {
"user": {
"id": "1",
"name": "Alice",
"age": 30,
"address": {
"street": "123 Main St",
"city": "New York",
"country": "USA"
}
}
}
}

内联片段

  • 用途: 针对接口或联合类型中不同具体实现返回不同字段集合
  • 使用场景
    • 接口/联合类型多态数据
    • 需要根据实际类型选择附加字段
  • 语法: ... on TypeName { fields }
  • 示例代码(语法 -> Schema -> 查询使用 -> 返回结果)
# Inline Fragment Syntax ---------------
... on TypeName {
field1
field2
}

# Schema --------------------------------
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]
}

# Query Using Inline Fragments ---------
query {
persons {
id
name
... on Employee {
salary
}
... on Customer {
purchaseHistory
}
}
}

# Result JSON --------------------------
{
"data": {
"persons": [
{
"id": "1",
"name": "Alice",
"salary": 50000
},
{
"id": "2",
"name": "Bob",
"purchaseHistory": ["item1", "item2"]
}
]
}
}

6. 操作名称

GraphQL中,操作名称 (Operation Name) 是一个可选的标识,用于明确标注特定的查询 (Query)、变更 (Mutation) 或订阅 (Subscription) 操作的名称。它有助于提高可读性、调试效率,以及在某些工具中实现特定功能

操作名称特点

  • 操作名称的位置
    • 紧跟在操作类型(如querymutationsubscription)之后
  • 命名规则
    • 名称只能包含字母、数字和下划线(_)
    • 名称不能以数字开头
  • 用途
    • 提供查询的上下文,明确查询意图
    • 在调试、日志记录和工具(如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 APIGET请求)
query {
user(id: "1") {
id
name
age
}
}

<!-- 查询 user 数据,根据参数 id: "1" 获取特定用户的信息 -->
<!-- 返回的字段包括 id、name 和 age -->
  • 变更 Mutation
    • 用途: 用于修改服务器上的数据,包括创建、更新或删除操作
    • 特点: 与查询不同,变更操作会改变服务器的数据状态 (类似于REST APIPOSTPUTPATCHDELETE请求)
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 APIGET请求类似,但在灵活性和强大性方面超越了后者

查询的定义

  • 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 (Precise Fields) ---------------
query {
user(id: "1") {
name
age
}
}

# Result JSON (Precise Fields) ---------
{
"data": {
"user": {
"name": "Alice",
"age": 30
}
}
}

# Query (Multiple Resources Single Request)
query {
user(id: "1") {
id
name
}
users {
id
name
}
}

# Result JSON (Multiple Resources) -----
{
"data": {
"user": {
"id": "1",
"name": "Alice"
},
"users": [
{
"id": "1",
"name": "Alice"
},
{
"id": "2",
"name": "Bob"
}
]
}
}

9. 变更Mutations

变更 (Mutations) 是GraphQL中的一种操作类型,专门用于对服务器端的数据进行修改操作。与查询 (Query) 仅用于读取数据不同,变更支持创建、更新或删除数据。每个变更操作都可以定义所需的输入参数和返回结果的结构

变更的定义

  • GraphQLschema中,变更类型通常定义在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: 接收nameage参数,返回新创建的User对象
  • updateUser: 接收id(必需)、nameage参数,用于更新用户信息,返回更新后的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(), // 生成唯一 ID
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;
},
},
};

变更的使用

客户端可以通过以下查询发送变更请求

  • 示例代码(创建用户 -> 更新用户 -> 删除用户 -> 各自结果)
# Create User Mutation -----------------
mutation {
createUser(name: "Alice", age: 30) {
id
name
age
}
}

# Result JSON (Create) -----------------
{
"data": {
"createUser": {
"id": "c81e728d-59f2-4d16-9f02-fcad84756789",
"name": "Alice",
"age": 30
}
}
}

# Update User Mutation -----------------
mutation {
updateUser(id: "1", name: "Alice Updated") {
id
name
age
}
}

# Result JSON (Update) -----------------
{
"data": {
"updateUser": {
"id": "1",
"name": "Alice Updated",
"age": 30
}
}
}

# Delete User Mutation -----------------
mutation {
deleteUser(id: "2")
}

# Result JSON (Delete) -----------------
{
"data": {
"deleteUser": true
}
}

10. 订阅Subscription

订阅 (Subscription) 是GraphQL中的一种操作类型,允许客户端订阅服务器上的事件,并在事件发生时实时接收更新数据。这种机制特别适合实时更新场景,如聊天消息、通知、股票价格等

订阅的工作原理

  • 建立订阅连接: 客户端发送订阅请求到服务器,指定要订阅的事件或数据
  • 保持连接: 服务器与客户端之间通过WebSocket或其他双向通信协议维持持久连接
  • 事件触发: 当服务器端的事件发生变化(如新增或更新数据)时,触发对应的订阅逻辑
  • 数据推送: 服务器将更新的数据通过连接推送到订阅的客户端

订阅示例

  • Schema定义
type User {
id: ID!
name: String!
age: Int
}

type Subscription {
userAdded: User
}

type Mutation {
createUser(name: String!, age: Int!): User
}
  • Resolvers实现
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();

const resolvers = {
Subscription: {
userAdded: {
// 定义订阅器,监听 USER_ADDED 事件
subscribe: () => pubsub.asyncIterator(['USER_ADDED']),
},
},
Mutation: {
createUser: (parent, args, context, info) => {
const newUser = {
id: generateUniqueId(),
name: args.name,
age: args.age,
};

// 保存用户数据(模拟数据库操作)
saveUserToDatabase(newUser);

// 触发 USER_ADDED 事件
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';

// 配置 HTTP 链接用于查询和变更
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });

// 配置 WebSocket 链接用于订阅
const wsLink = new WebSocketLink({
uri: 'ws://localhost:4000/graphql',
options: {
reconnect: true,
},
});

// 使用 split 根据操作类型分别选择链接
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);

// 创建 Apollo 客户端
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
  • 订阅操作: 使用Apollo ClientuseSubscription 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;
  • 总而言之,GraphQLSubscription依赖于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 ClientuseQuery 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;

// 变量`$id`的值由组件的`props`动态传递
// 查询使用`useQuery Hook`,并通过`variables`选项传递实际的变量值
  • 变更中的变量: 在变更操作中,变量同样可以动态传递参数
mutation CreateUser($name: String!, $age: Int!) {
createUser(name: $name, age: $age) {
id
name
age
}
}
  • Apollo Client示例
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;
  • 客户端使用示例
query {
greeting
}
{
"data": {
"greeting": "HELLO WORLD"
}
}