Skip to content

GraphQL

欢迎来到 GraphQL 知识库!

⚡ GraphQL 简介

GraphQL 是一种用于 API 的查询语言,由 Facebook 于 2012 年开发并在 2015 年开源。它提供了一种更高效、强大和灵活的替代 REST 的方案。

🎯 GraphQL vs REST

特性GraphQLREST
获取数据一次请求获取多个资源多次请求多个端点
数据获取精确获取需要的字段获取整个资源
版本管理无需版本,字段可废弃需要 v1、v2 版本
开发体验强类型,自动文档需手动维护文档
学习曲线较陡较平缓
缓存复杂简单(HTTP 缓存)

✅ GraphQL 优势

1. 避免过度获取和不足获取

REST 问题

javascript
// 只需要用户名和邮箱,却获取了所有字段
GET /users/123
{
  "id": 123,
  "username": "zhangsan",
  "email": "zhang@example.com",
  "age": 25,
  "address": {...},
  "profile": {...}
  // ... 很多不需要的字段
}

// 需要用户信息和他的文章,要请求两次
GET /users/123
GET /users/123/posts

GraphQL 解决

graphql
# 一次请求,精确获取需要的数据
query {
  user(id: 123) {
    username
    email
    posts {
      title
      createdAt
    }
  }
}

2. 强类型系统

GraphQL 使用 Schema 定义 API,提供类型检查和自动文档。

3. 实时更新

通过 Subscription 支持实时数据推送。

4. 单一端点

所有请求都发送到同一个端点,简化路由。

🎯 核心概念

1. Schema(模式)

定义 API 的类型系统。

graphql
# 定义类型
type User {
  id: ID!          # ! 表示必填
  username: String!
  email: String
  age: Int
  posts: [Post!]!  # 数组类型
}

type Post {
  id: ID!
  title: String!
  content: String
  author: User!
  createdAt: String
}

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

# 定义变更
type Mutation {
  createUser(username: String!, email: String!): User
  updateUser(id: ID!, username: String): User
  deleteUser(id: ID!): Boolean
}

# 定义订阅
type Subscription {
  userCreated: User
  postAdded: Post
}

2. Query(查询)

读取数据。

graphql
# 基本查询
query {
  users {
    id
    username
    email
  }
}

# 带参数查询
query {
  user(id: "123") {
    username
    email
    posts {
      title
    }
  }
}

# 使用变量
query GetUser($id: ID!) {
  user(id: $id) {
    username
    email
  }
}

# 别名
query {
  user1: user(id: "123") {
    username
  }
  user2: user(id: "456") {
    username
  }
}

# 片段
fragment UserInfo on User {
  username
  email
}

query {
  user(id: "123") {
    ...UserInfo
    posts {
      title
    }
  }
}

3. Mutation(变更)

修改数据。

graphql
# 创建用户
mutation {
  createUser(username: "lisi", email: "li@example.com") {
    id
    username
    email
  }
}

# 使用变量
mutation CreateUser($username: String!, $email: String!) {
  createUser(username: $username, email: $email) {
    id
    username
    email
  }
}

# 多个变更
mutation {
  createUser(username: "lisi", email: "li@example.com") {
    id
  }
  createPost(title: "标题", authorId: "123") {
    id
    title
  }
}

4. Subscription(订阅)

实时数据推送。

graphql
subscription {
  userCreated {
    id
    username
    email
  }
}

🚀 快速开始

Node.js + Apollo Server

javascript
const { ApolloServer, gql } = require('apollo-server');

// 1. 定义 Schema
const typeDefs = gql`
  type User {
    id: ID!
    username: String!
    email: String
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
  }

  type Mutation {
    createUser(username: String!, email: String!): User
  }
`;

// 2. 模拟数据
let users = [
  { id: '1', username: 'zhangsan', email: 'zhang@example.com' },
  { id: '2', username: 'lisi', email: 'li@example.com' }
];

// 3. 定义 Resolver
const resolvers = {
  Query: {
    users: () => users,
    user: (parent, args) => users.find(u => u.id === args.id)
  },
  Mutation: {
    createUser: (parent, args) => {
      const user = {
        id: String(users.length + 1),
        username: args.username,
        email: args.email
      };
      users.push(user);
      return user;
    }
  }
};

// 4. 创建服务器
const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

查询示例

graphql
# 查询所有用户
query {
  users {
    id
    username
    email
  }
}

# 查询单个用户
query {
  user(id: "1") {
    username
    email
  }
}

# 创建用户
mutation {
  createUser(username: "wangwu", email: "wang@example.com") {
    id
    username
    email
  }
}

🔗 关联查询

graphql
# Schema 定义
type User {
  id: ID!
  username: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  author: User!
}

type Query {
  user(id: ID!): User
}
javascript
// Resolver 实现
const resolvers = {
  Query: {
    user: (parent, args) => {
      return db.users.findById(args.id);
    }
  },
  User: {
    // 解析 user.posts 字段
    posts: (parent) => {
      return db.posts.findByAuthorId(parent.id);
    }
  },
  Post: {
    // 解析 post.author 字段
    author: (parent) => {
      return db.users.findById(parent.authorId);
    }
  }
};
graphql
# 查询用户及其文章
query {
  user(id: "123") {
    username
    email
    posts {
      title
      createdAt
    }
  }
}

🎯 DataLoader 解决 N+1 问题

N+1 问题:查询一个用户列表及其文章时,会产生 N+1 次数据库查询。

javascript
const DataLoader = require('dataloader');

// 批量加载用户
const userLoader = new DataLoader(async (userIds) => {
  const users = await db.users.findByIds(userIds);
  return userIds.map(id => users.find(u => u.id === id));
});

// Resolver 中使用
const resolvers = {
  Post: {
    author: (parent) => {
      return userLoader.load(parent.authorId);  // 批量加载
    }
  }
};

🔒 认证与授权

javascript
const { ApolloServer } = require('apollo-server');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    // 从 header 获取 token
    const token = req.headers.authorization || '';
    const user = getUserFromToken(token);
    return { user };
  }
});

// Resolver 中检查权限
const resolvers = {
  Query: {
    me: (parent, args, context) => {
      if (!context.user) {
        throw new Error('未登录');
      }
      return context.user;
    }
  },
  Mutation: {
    createPost: (parent, args, context) => {
      if (!context.user) {
        throw new Error('未登录');
      }
      // 创建文章
    }
  }
};

📊 分页

基于偏移量

graphql
type Query {
  users(offset: Int, limit: Int): UserConnection!
}

type UserConnection {
  nodes: [User!]!
  totalCount: Int!
}

Relay 风格(推荐)

graphql
type Query {
  users(first: Int, after: String): UserConnection!
}

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

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

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

🔍 错误处理

javascript
const { ApolloError } = require('apollo-server');

const resolvers = {
  Query: {
    user: (parent, args) => {
      const user = db.users.findById(args.id);
      if (!user) {
        throw new ApolloError('用户不存在', 'USER_NOT_FOUND');
      }
      return user;
    }
  }
};

响应:

json
{
  "errors": [
    {
      "message": "用户不存在",
      "extensions": {
        "code": "USER_NOT_FOUND"
      }
    }
  ]
}

💡 最佳实践

  1. 合理设计 Schema

    • 遵循命名规范
    • 使用描述性名称
    • 合理使用必填(!)
  2. 避免 N+1 问题

    • 使用 DataLoader
    • 批量查询
  3. 字段级别权限

    • 在 Resolver 中检查权限
    • 隐藏敏感字段
  4. 查询复杂度限制

    • 限制查询深度
    • 限制查询复杂度
  5. 缓存策略

    • HTTP 缓存
    • Apollo Client 缓存
    • Redis 缓存
  6. 错误处理

    • 统一错误格式
    • 提供有意义的错误信息
  7. 文档化

    • 使用描述字段
    • 保持 Schema 清晰

📖 学习资源

官方资源

工具推荐

  • GraphQL Playground - 交互式查询工具
  • Apollo Studio - API 管理平台
  • GraphQL Voyager - Schema 可视化

推荐教程


准备好了吗?开始你的 GraphQL 学习之旅!