📖 第11课:GraphQL概述

阶段三:GraphQL

GraphQL是Facebook于2015年开源的查询语言和运行时。与REST的"服务端决定返回什么"不同,GraphQL让客户端精确声明需要什么数据——不多不少。本课从核心概念出发,对比REST与GraphQL,搭建第一个GraphQL服务。

🔍 REST的痛点 → GraphQL的解法

REST的三大痛点

痛点REST场景GraphQL解法
过度获取(Over-fetching)GET /users 返回20个字段,只需3个客户端精确选择字段
获取不足(Under-fetching)获取用户→再获取文章→再获取评论,3次请求一次查询嵌套关联数据
接口膨胀/users, /users/brief, /users/with-posts, /users/with-everything一个端点,灵活组合
// REST: 获取用户及其文章评论 → 3次请求
GET /api/users/42                    // 用户信息
GET /api/users/42/posts              // 用户文章
GET /api/posts/1/comments            // 文章1的评论
GET /api/posts/2/comments            // 文章2的评论
// N+1问题!

// GraphQL: 一次查询搞定
query {
  user(id: 42) {
    name
    email
    posts {
      title
      comments {
        body
        author { name }
      }
    }
  }
}

📐 GraphQL核心概念

六大核心概念

Schema定义

# GraphQL Schema语言(SDL)

# 标量类型
scalar DateTime

# 对象类型
type User {
  id: ID!
  name: String!
  email: String!
  role: Role!
  posts: [Post!]!      # 关联字段,由resolver解析
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!        # 反向关联
  comments: [Comment!]!
  tags: [String!]!
  createdAt: DateTime!
}

type Comment {
  id: ID!
  body: String!
  author: User!
  post: Post!
  createdAt: DateTime!
}

# 枚举类型
enum Role {
  ADMIN
  EDITOR
  VIEWER
}

# 查询入口
type Query {
  user(id: ID!): User
  users(limit: Int, offset: Int): [User!]!
  post(id: ID!): Post
  posts(authorId: ID, tag: String): [Post!]!
  search(query: String!): [SearchResult!]!
}

# 变更入口
type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
  createPost(input: CreatePostInput!): Post!
  addComment(postId: ID!, body: String!): Comment!
}

# 订阅入口
type Subscription {
  postCreated(authorId: ID): Post!
  commentAdded(postId: ID!): Comment!
}

# 输入类型
input CreateUserInput {
  name: String!
  email: String!
  role: Role = VIEWER
}

input UpdateUserInput {
  name: String
  email: String
  role: Role
}

input CreatePostInput {
  title: String!
  content: String!
  tags: [String!]!
}

# 联合类型(搜索结果可以是多种类型)
union SearchResult = User | Post | Comment

🛠️ 实战:第一个GraphQL服务

// graphql-server.js - GraphQL服务实现
const express = require('express');
const { createHandler } = require('graphql-http/lib/use/express');
const { buildSchema, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLID } = require('graphql');

const app = express();

// ===== 数据 =====

const users = [
  { id: '1', name: '张三', email: 'zhang@example.com', role: 'ADMIN' },
  { id: '2', name: '李四', email: 'li@example.com', role: 'EDITOR' },
  { id: '3', name: '王五', email: 'wang@example.com', role: 'VIEWER' },
];

const posts = [
  { id: '1', title: 'GraphQL入门', content: 'GraphQL是一种查询语言...', authorId: '1', tags: ['GraphQL', 'API'], createdAt: '2025-01-10' },
  { id: '2', title: 'REST vs GraphQL', content: '两种API风格的对比...', authorId: '1', tags: ['REST', 'GraphQL'], createdAt: '2025-01-11' },
  { id: '3', title: 'Schema设计实践', content: '如何设计好的Schema...', authorId: '2', tags: ['GraphQL', '设计'], createdAt: '2025-01-12' },
];

const comments = [
  { id: '1', body: '非常实用的入门教程!', authorId: '2', postId: '1', createdAt: '2025-01-10' },
  { id: '2', body: '期待更多深入内容', authorId: '3', postId: '1', createdAt: '2025-01-10' },
  { id: '3', body: '对比很客观', authorId: '3', postId: '2', createdAt: '2025-01-11' },
];

// ===== Schema(SDL方式)=====

const schemaStr = `
  scalar DateTime

  enum Role {
    ADMIN
    EDITOR
    VIEWER
  }

  type User {
    id: ID!
    name: String!
    email: String!
    role: Role!
    posts: [Post!]!
    postCount: Int!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    comments: [Comment!]!
    tags: [String!]!
    commentCount: Int!
    createdAt: DateTime!
  }

  type Comment {
    id: ID!
    body: String!
    author: User!
    post: Post!
    createdAt: DateTime!
  }

  input CreateUserInput {
    name: String!
    email: String!
    role: Role = VIEWER
  }

  input CreatePostInput {
    title: String!
    content: String!
    tags: [String!]! = []
  }

  type Query {
    user(id: ID!): User
    users(limit: Int = 20, offset: Int = 0): [User!]!
    post(id: ID!): Post
    posts(authorId: ID, tag: String, limit: Int = 20): [Post!]!
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    createPost(input: CreatePostInput!): Post!
    addComment(postId: ID!, body: String!): Comment!
  }
`;

const schema = buildSchema(schemaStr);

// ===== Resolvers =====

const root = {
  // Query resolvers
  user: ({ id }) => users.find(u => u.id === id),
  users: ({ limit, offset }) => users.slice(offset, offset + limit),
  post: ({ id }) => posts.find(p => p.id === id),
  posts: ({ authorId, tag, limit }) => {
    let result = [...posts];
    if (authorId) result = result.filter(p => p.authorId === authorId);
    if (tag) result = result.filter(p => p.tags.includes(tag));
    return result.slice(0, limit);
  },

  // Type resolvers(字段级解析)
  User: {
    posts: (user) => posts.filter(p => p.authorId === user.id),
    postCount: (user) => posts.filter(p => p.authorId === user.id).length,
  },
  Post: {
    author: (post) => users.find(u => u.id === post.authorId),
    comments: (post) => comments.filter(c => c.postId === post.id),
    commentCount: (post) => comments.filter(c => c.postId === post.id).length,
  },
  Comment: {
    author: (comment) => users.find(u => u.id === comment.authorId),
    post: (comment) => posts.find(p => p.id === comment.postId),
  },

  // Mutation resolvers
  createUser: ({ input }) => {
    const id = String(users.length + 1);
    const user = { id, ...input };
    users.push(user);
    return user;
  },
  createPost: ({ input }, context) => {
    const id = String(posts.length + 1);
    const post = { id, ...input, authorId: context?.userId || '1', createdAt: new Date().toISOString() };
    posts.push(post);
    return post;
  },
  addComment: ({ postId, body }, context) => {
    const id = String(comments.length + 1);
    const comment = { id, body, authorId: context?.userId || '2', postId, createdAt: new Date().toISOString() };
    comments.push(comment);
    return comment;
  },
};

// ===== 路由 =====

app.all('/graphql', createHandler({
  schema,
  rootValue: root,
  context: (req) => ({ userId: req.headers['x-user-id'] || '1' }),
}));

// GraphiQL IDE
app.get('/graphiql', (req, res) => {
  res.send(`GraphiQL
    
    
    
    
    
`); }); app.listen(3000, () => console.log('🔮 GraphQL服务运行在 http://localhost:3000/graphql'));

测试GraphQL查询

# 查询1: 获取用户基本信息
curl -s -X POST http://localhost:3000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ user(id: \"1\") { name email role } }"}' | jq .
{
  "data": {
    "user": {
      "name": "张三",
      "email": "zhang@example.com",
      "role": "ADMIN"
    }
  }
}
# 查询2: 嵌套关联数据(一次查询解决REST的N+1问题)
curl -s -X POST http://localhost:3000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ user(id: \"1\") { name posts { title commentCount comments { body author { name } } } } }"}' | jq .
{
  "data": {
    "user": {
      "name": "张三",
      "posts": [
        {
          "title": "GraphQL入门",
          "commentCount": 2,
          "comments": [
            {"body":"非常实用的入门教程!","author":{"name":"李四"}},
            {"body":"期待更多深入内容","author":{"name":"王五"}}
          ]
        }
      ]
    }
  }
}
# 查询3: 过滤文章
curl -s -X POST http://localhost:3000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ posts(tag: \"GraphQL\") { title author { name } } }"}' | jq .

# 变更1: 创建用户
curl -s -X POST http://localhost:3000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"mutation { createUser(input: { name: \"赵六\", email: \"zhao@example.com\" }) { id name email role } }"}' | jq .

# 变更2: 添加评论
curl -s -X POST http://localhost:3000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"mutation { addComment(postId: \"1\", body: \"太棒了!\") { id body author { name } } }"}' | jq .

⚖️ REST vs GraphQL选型指南

维度RESTGraphQL
适用场景资源导向、CRUD为主复杂关联、前端驱动
学习曲线中等
缓存✅ HTTP原生支持⚠️ 需额外方案
文件上传✅ multipart原生⚠️ 需规范扩展
调试工具curl/PostmanGraphiQL/Apollo Explorer
性能多请求开销单请求复杂解析
版本管理URL版本化Schema演进
生态成熟完善快速发展中
💡 不是替代关系:GraphQL不替代REST,它们是不同场景的工具。管理后台用REST,复杂前端用GraphQL,微服务间用gRPC。很多团队选择REST为主+GraphQL为辅的混合架构。

📝 本课小结

  1. GraphQL解决REST的过度获取、获取不足和接口膨胀问题
  2. 核心概念:Schema + Query + Mutation + Subscription + Resolver
  3. SDL(Schema Definition Language)是类型定义的标准方式
  4. 嵌套查询一次解决N+1问题,但需注意N+1 resolver问题
  5. GraphQL和REST是互补的,不是替代的
  6. 用curl直接测试GraphQL endpoint

💪 练习

练习1:设计电商GraphQL Schema

为电商系统设计Schema,包含:商品、分类、购物车、订单、支付。注意输入类型和关联设计。

练习2:对比查询

分别用REST和GraphQL实现"获取用户的所有订单及其商品详情",对比请求次数和数据量。

🏆 本课成就:GraphQL探索者