MD 状态:🌿 分类:系统与架构 更新:2026/5/29

GraphQL 使用笔记

[!abstract] 笔记目标 本笔记旨在提供 GraphQL 的实战使用指南,涵盖从 Schema 设计到客户端集成的完整流程,帮助开发者快速上手并避免常见陷阱。

一、核心概念速览

GraphQL 的核心是 Schema-first 的设计哲学。所有操作都围绕 Schema 展开:

graph LR
    A[客户端] -->|发送 Query/Mutation| B[GraphQL 服务器]
    B -->|解析 Schema| C[类型系统]
    B -->|调用| D[解析器 Resolvers]
    D -->|访问| E[数据源 DB/API]
    E -->|返回数据| D
    D -->|组装数据| B
    B -->|返回 JSON| A

关键术语

  • Schema:API 的契约,定义所有可查询的数据和操作
  • Query:读取数据(幂等)
  • Mutation:写入数据(可能产生副作用)
  • Resolver:为每个字段提供数据获取逻辑的函数
  • Type:数据结构的定义(对象类型、标量类型、枚举等)

二、Schema 设计最佳实践

1. 类型定义原则

# 好的设计:使用对象类型而不是标量类型
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!  # 关联关系
  createdAt: DateTime!
}

# 避免:使用 JSON 标量类型(失去类型安全)
type User {
  id: ID!
  data: JSON  # 不推荐
}

2. 查询与变更设计

type Query {
  # 单个资源查询
  user(id: ID!): User
  
  # 列表查询(带分页)
  users(first: Int, after: String): UserConnection!
  
  # 搜索查询
  searchUsers(query: String!): [User!]!
}

type Mutation {
  # 创建
  createUser(input: CreateUserInput!): CreateUserPayload!
  
  # 更新
  updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
  
  # 删除
  deleteUser(id: ID!): DeleteUserPayload!
}

# 使用 Input 类型封装输入参数
input CreateUserInput {
  name: String!
  email: String!
}

# 使用 Payload 类型封装返回结果
type CreateUserPayload {
  user: User
  errors: [Error!]
}

3. 分页设计(Relay 风格)

type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  node: User!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

三、查询语法详解

1. 基本查询

# 获取用户及其文章
query GetUserWithPosts {
  user(id: "1") {
    name
    email
    posts {
      title
      content
      createdAt
    }
  }
}

2. 使用变量

query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
  }
}

# 变量传递
{
  "userId": "1"
}

3. 片段(Fragments)复用

fragment UserBasicInfo on User {
  id
  name
  email
}

fragment PostInfo on Post {
  id
  title
  content
  createdAt
}

query GetUserWithPosts {
  user(id: "1") {
    ...UserBasicInfo
    posts {
      ...PostInfo
    }
  }
}

4. 指令(Directives)

query GetUser($includePosts: Boolean!) {
  user(id: "1") {
    name
    email
    posts @include(if: $includePosts) {
      title
    }
  }
}

四、服务端实现要点

1. 解析器(Resolver)结构

const resolvers = {
  Query: {
    user: async (parent, { id }, context, info) => {
      // parent: 父解析器的结果
      // args: 查询参数
      // context: 请求上下文(数据库连接、用户信息等)
      // info: 查询字段信息
      return context.db.users.findById(id);
    },
  },
  
  User: {
    // 字段级解析器
    posts: async (parent, args, context) => {
      return context.db.posts.findByUserId(parent.id);
    },
  },
  
  Mutation: {
    createUser: async (parent, { input }, context) => {
      try {
        const user = await context.db.users.create(input);
        return { user, errors: [] };
      } catch (error) {
        return { user: null, errors: [error.message] };
      }
    },
  },
};

2. 上下文(Context)管理

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    // 从请求头获取 token
    const token = req.headers.authorization || '';
    
    // 验证用户
    const user = getUserFromToken(token);
    
    // 返回上下文对象
    return {
      user,
      db: database,
      loaders: createLoaders(), // DataLoader 实例
    };
  },
});

3. 数据源(Data Sources)集成

// 使用 REST API 作为数据源
class UserAPI extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = 'https://api.example.com/';
  }
  
  async getUser(id) {
    return this.get(`users/${id}`);
  }
  
  async createUser(userData) {
    return this.post('users', userData);
  }
}

// 配置数据源
const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources: () => ({
    userAPI: new UserAPI(),
  }),
});

五、客户端使用

1. 主流客户端库对比

特点适用场景
Apollo Client功能全面,缓存强大,社区活跃中大型项目,需要复杂缓存逻辑
RelayFacebook 出品,性能优化极致,学习曲线陡大型应用,对性能要求极高
urql轻量级,可扩展,插件系统中小型项目,需要灵活定制
graphql-request极简,无缓存,仅发送请求简单场景,脚本,SSR

2. Apollo Client 基本使用

import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

// 初始化客户端
const client = new ApolloClient({
  uri: 'https://api.example.com/graphql',
  cache: new InMemoryCache(),
});

// 查询
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

client.query({
  query: GET_USER,
  variables: { id: '1' },
}).then(result => console.log(result.data));

// 变更
const CREATE_USER = gql`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      user {
        id
        name
      }
      errors
    }
  }
`;

client.mutate({
  mutation: CREATE_USER,
  variables: { input: { name: 'John', email: 'john@example.com' } },
});

3. React 集成(Apollo Client)

import { useQuery, useMutation, gql } from '@apollo/client';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
    }
  }
`;

function UserList() {
  const { loading, error, data } = useQuery(GET_USERS);
  
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  
  return (
    <ul>
      {data.users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

六、性能优化

1. DataLoader 解决 N+1 问题

import DataLoader from 'dataloader';

// 创建 DataLoader 实例
const createLoaders = () => ({
  userLoader: new DataLoader(async (userIds) => {
    // 批量查询用户
    const users = await db.users.findByIds(userIds);
    
    // 按 ID 顺序返回结果
    return userIds.map(id => users.find(user => user.id === id));
  }),
});

// 在解析器中使用
const resolvers = {
  Post: {
    author: async (parent, args, context) => {
      // 使用 DataLoader 自动批量加载
      return context.loaders.userLoader.load(parent.authorId);
    },
  },
};

2. 查询复杂度分析

import { createComplexityRule } from 'graphql-query-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    createComplexityRule({
      maximumComplexity: 1000,
      estimators: [
        fieldExtensionsEstimator(),
        simpleEstimator({ defaultComplexity: 1 }),
      ],
      onComplete: (complexity) => {
        console.log('Query complexity:', complexity);
      },
    }),
  ],
});

3. 缓存策略

// Apollo Client 缓存配置
const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        users: {
          // 合并分页数据
          keyArgs: false,
          merge(existing = { edges: [] }, incoming) {
            return {
              ...incoming,
              edges: [...existing.edges, ...incoming.edges],
            };
          },
        },
      },
    },
  },
});

七、安全考虑

1. 认证与授权

// 在上下文中验证用户
const context = ({ req }) => {
  const token = req.headers.authorization;
  const user = verifyToken(token);
  
  if (!user) {
    throw new AuthenticationError('Not authenticated');
  }
  
  return { user };
};

// 在解析器中检查权限
const resolvers = {
  Mutation: {
    deleteUser: async (parent, { id }, { user }) => {
      if (!user.isAdmin) {
        throw new ForbiddenError('Not authorized');
      }
      return db.users.delete(id);
    },
  },
};

2. 查询深度限制

import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(10)], // 限制查询深度为 10
});

3. 速率限制

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 100, // 每个 IP 最多 100 个请求
});

app.use('/graphql', limiter);

八、常用工具与生态

1. 开发工具

  • GraphQL Playground:交互式查询 IDE
  • GraphiQL:浏览器内 IDE
  • Apollo DevTools:Chrome 扩展,调试缓存和查询
  • GraphQL Code Generator:自动生成 TypeScript 类型

2. 服务端框架

  • Apollo Server:最流行,功能全面
  • GraphQL Yoga:轻量级,基于 Express
  • Mercurius:Fastify 插件,高性能
  • Strapi:Headless CMS,内置 GraphQL

3. 客户端库

  • Apollo Client:React/Vue/Angular 支持
  • Relay:React 专用
  • urql:轻量级,框架无关
  • graphql-request:极简请求库

九、与 REST 迁移策略

1. 渐进式迁移

graph TD
    A[现有 REST API] --> B[添加 GraphQL 层]
    B --> C[新功能使用 GraphQL]
    C --> D[逐步迁移旧功能]
    D --> E[完全迁移]

2. 混合架构

// REST 和 GraphQL 共存
app.use('/api', restRouter);  // 旧 REST 端点
app.use('/graphql', graphqlMiddleware);  // 新 GraphQL 端点

// GraphQL 解析器调用 REST API
const resolvers = {
  Query: {
    user: async (parent, { id }) => {
      const response = await fetch(`/api/users/${id}`);
      return response.json();
    },
  },
};

十、考试相关考点

1. GraphQL vs REST 对比

维度GraphQLREST
数据获取精确获取,一次请求固定结构,多次请求
版本控制演进式,无需版本号URL 版本控制
类型系统强类型 Schema无强制类型
工具链自动生成文档需要 Swagger 等
学习曲线较陡平缓
缓存客户端缓存复杂HTTP 缓存简单

2. 常见面试题

  1. GraphQL 解决了 REST 的什么问题?

    • 过度获取(Over-fetching)
    • 不足获取(Under-fetching)
    • 多次请求
  2. GraphQL 的缺点是什么?

    • N+1 查询问题
    • 缓存复杂
    • 学习曲线陡峭
    • 文件上传复杂
  3. 如何优化 GraphQL 性能?

    • DataLoader 批量加载
    • 查询复杂度分析
    • 深度限制
    • 缓存策略
  4. GraphQL 适合什么场景?

    • 移动应用(带宽敏感)
    • 复杂数据关系
    • 快速迭代前端
    • API 聚合层

十一、实战检查清单

Schema 设计检查

  • 使用强类型,避免 JSON 标量
  • 输入参数使用 Input 类型
  • 返回结果使用 Payload 类型
  • 实现 Relay 风格分页
  • 定义错误类型

性能优化检查

  • 实现 DataLoader 解决 N+1
  • 设置查询深度限制
  • 配置查询复杂度分析
  • 实现适当的缓存策略

安全检查

  • 实现认证(Authentication)
  • 实现授权(Authorization)
  • 设置速率限制
  • 验证输入参数
  • 记录查询日志

开发体验检查

  • 启用 GraphQL Playground
  • 配置 Code Generator
  • 设置 Apollo DevTools
  • 编写单元测试

十二、常见问题与解决方案

1. N+1 查询问题

问题:查询用户列表时,每个用户的文章触发单独查询。

解决:使用 DataLoader 批量加载。

2. 循环引用

问题:用户引用文章,文章引用用户,导致无限循环。

解决:在 Schema 中明确指定深度限制,或使用惰性加载。

3. 文件上传

问题:GraphQL 原生不支持文件上传。

解决:使用 graphql-upload 包或单独的 REST 端点处理文件。

4. 实时数据

问题:如何实现实时更新?

解决:使用 GraphQL 订阅(Subscriptions)配合 WebSocket。


关联笔记

前置知识REST API · 强类型系统 · JSON 同族概念gRPC · API 网关 · OData 应用场景前后端分离 · 微服务 · 移动应用开发 相关工具Apollo Server · Apollo Client · Relay · DataLoader