GraphQL

技术栈
后端框架
apiquery-languagefacebookapolloschema

概览

GraphQL 技术栈概览

GraphQL 是 Meta(Facebook)于 2015 年开源的API 查询语言与运行时,由 Lee Byron 设计。它让客户端可以精确指定需要哪些字段,而不是依赖服务端预定义的 REST 端点。一个请求获取所有需要的数据,不多不少。

解决什么问题

  • REST 过度获取(Over-fetching)→ 只需 3 个字段却返回 50 个字段
  • REST 获取不足(Under-fetching)→ 需要多次请求拼凑一页所需数据
  • 前后端强耦合 → Schema 作为契约,类型系统自动校验
  • 版本管理困境 → 添加字段不破坏已有查询(向后兼容)
  • 多个端点维护 → 单一 /graphql 端点

关键特性

  • 单一端点:所有查询走 /graphql
  • 类型系统(Schema):SDL 定义 Object Type/Query/Mutation/Subscription
  • 精确获取:客户端指定需要的字段,服务端只返回这些
  • 嵌套查询:一次请求获取关联数据(如用户 → 文章 → 评论)
  • 实时订阅(Subscription):WebSocket 推送数据变更
  • 自省(Introspection):自动生成 API 文档
  • 工具生态:Apollo、Relay、GraphiQL 调试器

安装

环境准备

  • Node.js:>= 16 LTS
  • npm / yarn / pnpm

GraphQL 是规范而非工具,需配合服务端库使用。以下以最流行的 Apollo Server + Express 为例。

安装命令

Node.js (Apollo Server)

npm init -y
npm install @apollo/server graphql express cors body-parser
npm install -D nodemon typescript @types/node

Python (Graphene + Django)

pip install graphene-django

Java (Spring for GraphQL)

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>

最小起手

const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');

// Schema 定义
const typeDefs = `#graphql
  type Query {
    hello: String
  }
`;

// Resolver 实现
const resolvers = {
  Query: {
    hello: () => 'Hello GraphQL!',
  },
};

const server = new ApolloServer({ typeDefs, resolvers });

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

常见安装问题

Q1: Apollo Server 和 apollo-server-express 区别

Apollo Server v4 重构为独立包 @apollo/server,不再依赖 Express。如需 Express 中间件用 expressMiddleware

Q2: CORS 错误

startStandaloneServer 默认无 CORS。生产环境用 Express + cors() 中间件。

Q3: Playground 打不开

Apollo Server v4 移除内置 GraphQL Playground,使用 Apollo Sandbox(https://studio.apollographql.com/sandbox)。

示例

GraphQL 例程:图书管理 API

目标

实现一个完整的图书管理 GraphQL 服务,涵盖:Schema 定义、Query(查询)、Mutation(变更)、嵌套类型(Author ↔ Book)。

完整代码

const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');

// ── 数据 ──
const authors = [
  { id: '1', name: '余华' },
  { id: '2', name: '村上春树' },
];

const books = [
  { id: '1', title: '活着', authorId: '1', year: 1992 },
  { id: '2', title: '许三观卖血记', authorId: '1', year: 1995 },
  { id: '3', title: '挪威的森林', authorId: '2', year: 1987 },
];

// ── Schema ──
const typeDefs = `#graphql
  type Author {
    id: ID!
    name: String!
    books: [Book!]!
  }

  type Book {
    id: ID!
    title: String!
    year: Int!
    author: Author!
  }

  type Query {
    books: [Book!]!
    book(id: ID!): Book
    authors: [Author!]!
    author(id: ID!): Author
  }

  type Mutation {
    addBook(title: String!, authorId: ID!, year: Int!): Book!
    deleteBook(id: ID!): Boolean!
  }
`;

// ── Resolver ──
const resolvers = {
  Query: {
    books: () => books,
    book: (_, { id }) => books.find(b => b.id === id),
    authors: () => authors,
    author: (_, { id }) => authors.find(a => a.id === id),
  },
  Book: {
    author: (book) => authors.find(a => a.id === book.authorId),
  },
  Author: {
    books: (author) => books.filter(b => b.authorId === author.id),
  },
  Mutation: {
    addBook: (_, { title, authorId, year }) => {
      const id = String(books.length + 1);
      const newBook = { id, title, authorId, year };
      books.push(newBook);
      return newBook;
    },
    deleteBook: (_, { id }) => {
      const idx = books.findIndex(b => b.id === id);
      if (idx === -1) return false;
      books.splice(idx, 1);
      return true;
    },
  },
};

// ── 启动 ──
const server = new ApolloServer({ typeDefs, resolvers });

startStandaloneServer(server, { listen: { port: 4000 } }).then(({ url }) => {
  console.log(`🚀 服务运行在: ${url}`);
});

测试查询

访问 http://localhost:4000 Apollo Sandbox,执行:

# 查询所有图书 + 嵌套作者信息
query {
  books {
    title
    year
    author {
      name
      books {
        title
      }
    }
  }
}
# 添加图书
mutation {
  addBook(title: "兄弟", authorId: "1", year: 2005) {
    id
    title
    author { name }
  }
}

预期输出

{
  "data": {
    "books": [
      {
        "title": "活着",
        "year": 1992,
        "author": { "name": "余华", "books": [ ... ] }
      },
      ...
    ]
  }
}

关键点

概念 说明
! 表示 Non-Null,该字段不能为 null
[Book!]! 列表不能为 null,列表内的元素也不能为 null
嵌套 Resolver Book.author 独立解析,可单独优化
Mutation 返回 返回变更后的实体(最佳实践)

教程

GraphQL API 设计入门教程

第一章:GraphQL vs REST

REST 的痛点

假设要渲染一个博客首页,需要:

  1. GET /posts — 获取文章列表
  2. GET /users/1 — 获取每篇文章的作者头像
  3. GET /posts/1/comments — 获取评论数

三次请求 + 大量无关字段。这叫 Under-fetchingOver-fetching 双重困境。

GraphQL 的答案

query {
  posts {
    title
    author { name avatar }
    commentCount
  }
}

一次请求,精确字段,不多不少。

第二章:Schema 定义语言(SDL)

标量类型

  • IntFloatStringBooleanID

对象类型

type User {
  id: ID!
  name: String!
  email: String
  posts: [Post!]!
}

Query / Mutation / Subscription

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

第三章:Resolver 解析链

每个字段都可以有自己的 Resolver:

const resolvers = {
  Query: {
    user: (parent, args, context, info) => db.findUser(args.id),
  },
  User: {
    posts: (user) => db.findPostsByUserId(user.id),
  },
};

parent 是上一级 Resolver 的返回值,这让嵌套查询可以逐级解析、按需加载。

第四章:N+1 问题与 DataLoader

query {
  books {        # 1 次查询得到 100 本书
    author {     # 每本书再查 1 次作者 = 100 次查询!
      name
    }
  }
}

解决方案:DataLoader 批量 + 缓存。

const DataLoader = require('dataloader');

const authorLoader = new DataLoader(async (ids) => {
  const authors = await db.findAuthorsByIds(ids);
  return ids.map(id => authors.find(a => a.id === id));
});

// Book.author resolver
author: (book) => authorLoader.load(book.authorId),

第五章:最佳实践

  1. 错误处理:返回 errors 数组而非 HTTP 状态码
  2. 分页:使用 Relay Cursor Connections 规范
  3. 限流:限制查询深度(防止递归炸服务端)
  4. 持久化查询:客户端上传查询 hash,服务端只执行已知查询

思考题

  1. GraphQL 的 POST 请求(body 带 query)和 REST POST 有什么本质不同?
  2. 为什么 GraphQL 缓存比 REST 复杂?CDN 缓存还能用吗?
  3. 什么场景不适合用 GraphQL?(提示:简单 CRUD、文件上传、流式响应)

参考资料

暂无参考文献