15%

全场主机优惠15%

测试技能,享折扣

使用代码:

Skills
开始使用
01.11.2024
1 +1

GraphQL 完整指南:在高性能托管上构建更快、更智能的 API

GraphQL 从根本上改变了开发人员设计和使用 API 的方式。GraphQL 诞生于 Facebook 工程团队在 2012 年的内部项目,并在 2015 年发布给开源社区,已经成长为现代网络开发中应用最广泛的 API 查询语言之一。无论您是在构建实时聊天应用程序、数据密集型仪表板,还是具有严格带宽限制的移动优先产品,GraphQL 都能让您精确控制在网络上传输的数据。

在这份全面的指南中,您将学习什么是 GraphQL、为什么它在许多场景中优于传统 REST API、如何设置您的第一个 GraphQL 服务器,以及如何将其部署到能够处理生产工作负载需求的基础设施上。

目录

  1. 什么是 GraphQL?
  2. GraphQL 与 REST:为什么重要
  3. GraphQL 的关键特性
  4. 设置 GraphQL 服务器
  5. 定义您的模式
  6. 实现解析器
  7. 查询和变更数据
  8. 使用订阅进行实时更新
  9. GraphQL 内省和开发者工具
  10. 在生产环境中部署 GraphQL
  11. 安全最佳实践
  12. 结论

1. 什么是 GraphQL? {#what-is-graphql}

GraphQL 是一个开源的API 查询语言和用于对您的数据执行这些查询的运行时。与 REST 不同,REST 公开一组固定的端点,每个端点返回预定的数据结构,而 GraphQL 公开一个单一端点,客户端可以通过它精确请求他们需要的字段和关系——不多不少。

这种方法消除了 REST API 设计中两个最持久的问题:

  • 过度获取——接收远超客户端实际需要的数据,浪费带宽和处理时间。
  • 获取不足——在单个请求中接收的数据太少,迫使客户端进行多次后续调用来获得完整的信息。

使用 GraphQL,客户端驱动响应的形状。服务器履行由模式定义的契约,客户端只请求它打算使用的内容。

2. GraphQL 与 REST:为什么重要 {#graphql-vs-rest}

理解何时选择 GraphQL 而不是 REST(反之亦然)对于做出合理的架构决策至关重要。

维度RESTGraphQL
端点多个(每个资源一个)单一统一端点
数据获取固定响应结构客户端指定的字段
过度获取常见按设计消除
获取不足常见(N+1 问题)通过嵌套查询解决
版本控制需要 URL 或标头版本控制无需版本控制的模式演进
实时支持需要 WebSockets 或轮询原生订阅
类型系统可选(OpenAPI/Swagger)内置、强制模式
缓存HTTP 级缓存直观需要查询感知缓存

GraphQL 特别适用于:

  • 复杂、相互关联的数据模型,其中单个视图需要来自多个资源的数据。
  • 移动应用程序,其中最小化有效负载大小直接改善用户体验并降低数据成本。
  • 快速产品迭代,其中前端团队需要演进其数据需求,而无需等待后端 API 更改。
  • 微服务聚合,其中 GraphQL 网关将多个下游服务拼接成统一的 API 表面。

REST 仍然是简单 CRUD API、由第三方使用且受益于可预测 HTTP 语义的公共 API,以及 HTTP 级缓存是硬性要求的场景的不错选择。

3. GraphQL 的关键特性 {#key-features}

3.1 精确数据获取

GraphQL 的定义特征是客户端在查询本身中声明其数据需求。对单个端点的单个请求可以检索深层嵌套的相关对象图,只有客户端指定的字段在响应中被填充。

这对于跨多个平台(网络、iOS、Android、智能电视)构建产品的团队来说是变革性的,其中每个表面有不同的数据需求。与其维护单独的 REST 端点或接受臃肿的有效负载,每个客户端发送定制的查询并接收定制的响应。

3.2 强类型模式

每个 GraphQL API 都由用 GraphQL 模式定义语言 (SDL) 编写的模式支持。模式是服务器和使用它的每个客户端之间的权威契约。它定义:

  • 类型——数据模型中每个对象的形状。
  • 查询——客户端可以执行的读操作。
  • 变更——写操作(创建、更新、删除)。
  • 订阅——实时事件流。
  • 关系——类型如何相互引用。

由于模式是强类型的,整个类别的错误在开发时而不是在生产中被捕获。工具可以在查询执行之前根据模式验证查询,IDE 可以提供准确的自动完成和内联文档。

3.3 使用订阅进行实时更新

GraphQL 的订阅机制启用了客户端和服务器之间的持久、事件驱动连接——通常通过 WebSockets 实现。当订阅的事件在服务器上发生时(发布了新消息、股票价格变化、订单状态更新),服务器立即将相关数据推送给所有订阅的客户端。

这使 GraphQL 自然适合:

  • 实时聊天和消息应用程序
  • 协作编辑工具
  • 具有实时市场数据的财务仪表板
  • 实时通知和活动源
  • 多人游戏状态同步

3.4 内省

GraphQL API 按设计是自文档化的。内省系统允许客户端查询模式本身——在运行时发现可用的类型、字段、查询、变更及其描述。此功能为 GraphiQLApollo Studio 等开发者工具提供支持,这些工具提供交互式 API 浏览器、查询生成器和自动文档生成,无需 API 作者的任何额外工作。

3.5 无需版本控制的模式演进

GraphQL 最实用的方面之一是它如何优雅地处理变化。由于客户端只请求他们需要的字段,您可以向模式添加新字段和类型而不会破坏现有客户端。弃用旧字段通过模式注释而不是 URL 版本控制来处理,保持您的 API 表面清洁和客户端稳定。

4. 设置 GraphQL 服务器 {#setting-up}

GraphQL 与语言无关。成熟的服务器库存在于整个技术栈中。以下是最广泛使用的选项:

语言 / 运行时库 / 框架
Node.jsApollo Server、GraphQL Yoga、Express-GraphQL
PythonStrawberry、Graphene
JavaSpring for GraphQL、graphql-java
Gogqlgen、graphql-go
Rubygraphql-ruby
PHPLighthouse (Laravel)、webonyx/graphql-php
Rustasync-graphql
.NET / C#Hot Chocolate、GraphQL.NET

分步指南:Node.js 与 Apollo Server

Apollo Server 是 Node.js 生态系统中部署最广泛的 GraphQL 服务器。以下演练让您从零开始运行服务器。

前提条件:

  • 已安装 Node.js 18 或更高版本
  • npm 或 yarn 包管理器

步骤 1:初始化您的项目

mkdir graphql-api && cd graphql-api
npm init -y
npm install @apollo/server graphql

步骤 2:创建您的服务器文件

创建一个名为 index.js 的文件(或 index.mjs 用于 ES 模块):

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

// Step 1: Define your type definitions (schema)
const typeDefs = `#graphql
  type Book {
    id: ID!
    title: String!
    author: String!
    publishedYear: Int
    genre: String
  }

  type Query {
    books: [Book!]!
    book(id: ID!): Book
  }
`;

// Step 2: Define your data source (in-memory for this example)
const books = [
  {
    id: '1',
    title: 'The Pragmatic Programmer',
    author: 'David Thomas & Andrew Hunt',
    publishedYear: 1999,
    genre: 'Technology',
  },
  {
    id: '2',
    title: 'Clean Code',
    author: 'Robert C. Martin',
    publishedYear: 2008,
    genre: 'Technology',
  },
];

// Step 3: Define your resolvers
const resolvers = {
  Query: {
    books: () => books,
    book: (_, { id }) => books.find((b) => b.id === id),
  },
};

// Step 4: Create and start the server
const server = new ApolloServer({ typeDefs, resolvers });

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log(`🚀 GraphQL server ready at: ${url}`);

步骤 3:启动服务器

node index.js
# Output: 🚀 GraphQL server ready at: http://localhost:4000/

在浏览器中导航到 http://localhost:4000/ 以打开 Apollo Sandbox——一个交互式查询浏览器,您可以立即测试您的 API。

5. 定义您的模式 {#defining-schema}

模式是每个 GraphQL API 的骨干。投入时间设计一个精心设计的模式在应用程序的整个生命周期中都会获得回报。

标量类型

GraphQL 包括五个内置标量类型:

    Int ——32 位有符号整数
    Float ——双精度浮点数
    String ——UTF-8 字符序列
    Boolean ——true 或 false
  • ID ——唯一标识符,序列化为字符串
  • 您还可以为 DateDateTimeEmailURLJSON 等类型定义自定义标量

    对象类型

    type Author {
      id: ID!
      name: String!
      biography: String
      books: [Book!]!
    }
    
    type Book {
      id: ID!
      title: String!
      author: Author!
      publishedYear: Int
      genre: String
      tags: [String!]
    }

    ! 修饰符表示非空字段。[Book!]! 表示非空 Book 对象的非空列表。

    查询

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

    变更

    type Mutation {
      createBook(title: String!, authorId: ID!, genre: String): Book!
      updateBook(id: ID!, title: String, genre: String): Book
      deleteBook(id: ID!): Boolean!
    }

    输入类型

    对于具有多个参数的变更,输入类型保持您的模式清洁和可重用:

    input CreateBookInput {
      title: String!
      authorId: ID!
      publishedYear: Int
      genre: String
      tags: [String!]
    }
    
    type Mutation {
      createBook(input: CreateBookInput!): Book!
    }

    6. 实现解析器 {#implementing-resolvers}

    解析器是满足模式中每个字段的函数。GraphQL 模式中的每个字段都可以有一个解析器。如果没有为字段定义解析器,GraphQL 会回退到默认解析器,该解析器只是从父对象返回同名属性。

    解析器签名

    fieldName: (parent, args, context, info) => value
    • parent ——父类型的已解析值(对嵌套解析器有用)。
    • args ——传递给查询中字段的参数。
    • context ——通过整个解析器链传递的共享对象,通常包含已认证的用户、数据库连接或数据加载器。
    • info ——关于查询执行的元数据,包括字段名称和模式。

    示例:带有数据库的解析器

    const resolvers = {
      Query: {
        books: async (_, __, { db }) => {
          return db.collection('books').find().toArray();
        },
        book: async (_, { id }, { db }) => {
          return db.collection('books').findOne({ _id: id });
        },
      },
    
      Mutation: {
        createBook: async (_, { input }, { db, user }) => {
          if (!user) throw new Error('Authentication required');
          const result = await db.collection('books').insertOne(input);
          return { id: result.insertedId, ...input };
        },
      },
    
      Book: {
        // Nested resolver: fetch the author for each book
        author: async (book, _, { db }) => {
          return db.collection('authors').findOne({ _id: book.authorId });
        },
      },
    };

    使用 DataLoader 避免 N+1 问题

    嵌套解析器可能会触发 N+1 查询问题——获取 100 本书的列表,然后进行 100 次单独的数据库调用来解析每本书的作者。解决方案是 DataLoader,一个批处理和缓存实用程序:

    import DataLoader from 'dataloader';
    
    // In your context factory:
    const authorLoader = new DataLoader(async (authorIds) => {
      const authors = await db.collection('authors')
        .find({ _id: { $in: authorIds } })
        .toArray();
      return authorIds.map((id) => authors.find((a) => a._id === id));
    });
    
    // In your resolver:
    Book: {
      author: (book, _, { authorLoader }) => authorLoader.load(book.authorId),
    }

    DataLoader 将事件循环单个时刻内的所有 author 查找批处理为单个数据库查询,将 100 个查询减少到 1 个。

    7. 查询和变更数据 {#querying-data}

    基本查询

    query GetAllBooks {
      books {
        id
        title
        author {
          name
        }
        genre
      }
    }

    带参数的查询

    query GetBook {
      book(id: "1") {
        title
        author {
          name
          biography
        }
        publishedYear
        tags
      }
    }

    带变量的查询

    变量保持您的查询动态并防止注入漏洞:

    query GetBook($bookId: ID!) {
      book(id: $bookId) {
        title
        author {
          name
        }
      }
    }
    {
      "bookId": "1"
    }

    变更示例

    mutation AddBook($input: CreateBookInput!) {
      createBook(input: $input) {
        id
        title
        author {
          name
        }
      }
    }
    {
      "input": {
        "title": "Designing Data-Intensive Applications",
        "authorId": "42",
        "publishedYear": 2017,
        "genre": "Technology"
      }
    }

    片段

    片段允许您在多个查询中重用字段选择:

    fragment BookDetails on Book {
      id
      title
      genre
      publishedYear
    }
    
    query {
      books {
        ...BookDetails
        author {
          name
        }
      }
    }

    8. 使用订阅进行实时更新 {#subscriptions}

    GraphQL 订阅维护持久连接——通常通过 WebSockets——并在服务器上发生特定事件时将数据推送给客户端。

    模式定义

    type Subscription {
      bookAdded: Book!
      bookUpdated(id: ID!): Book!
    }

    服务器实现(Apollo Server 与 WebSockets)

    npm install graphql-ws ws @graphql-tools/schema
    import { createServer } from 'http';
    import { WebSocketServer } from 'ws';
    import { useServer } from 'graphql-ws/lib/use/ws';
    import { makeExecutableSchema } from '@graphql-tools/schema';
    import { PubSub } from 'graphql-subscriptions';
    
    const pubsub = new PubSub();
    
    const resolvers = {
      Mutation: {
        createBook: async (_, { input }, { db }) => {
          const book = await db.collection('books').insertOne(input);
          pubsub.publish('BOOK_ADDED', { bookAdded: book });
          return book;
        },
      },
      Subscription: {
        bookAdded: {
          subscribe: () => pubsub.asyncIterator(['BOOK_ADDED']),
        },
      },
    };
    
    const schema = makeExecutableSchema({ typeDefs, resolvers });
    const httpServer = createServer();
    const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' });
    useServer({ schema }, wsServer);

    客户端订阅

    subscription OnBookAdded {
      bookAdded {
        id
        title
        author {
          name
        }
      }
    }

    9. GraphQL 内省和开发者工具 {#introspection}

    GraphQL 的内省系统是其最开发者友好的功能之一。通过查询 __schema__type 元字段,客户端和工具可以在运行时发现您的 API 的完整结构。

    内省查询示例

    {
      __schema {
        types {
          name
          kind
          description
        }
      }
    }

    必要的开发者工具

    工具用途
    GraphiQL用于编写和测试查询的浏览器内 IDE
    Apollo Studio完整的 API 管理、性能监控、模式注册表
    PostmanGraphQL 查询支持和集合管理
    Insomnia具有 GraphQL 支持的轻量级 API 客户端
    GraphQL Code Generator从您的模式自动生成 TypeScript 类型
    Apollo Client DevTools用于调试 Apollo Client 缓存的浏览器扩展

    > 安全说明:在生产环境中禁用内省以避免向潜在攻击者公开您的 API 模式。Apollo Server 使这变得简单:

    >

    > “`javascript

    > new ApolloServer({ typeDefs, resolvers, introspection: false });

    > “`

    10. 在生产环境中部署 GraphQL {#deploying}

    将 GraphQL API 从开发环境迁移到生产环境需要仔细关注基础设施、性能和可靠性。

    选择合适的托管基础设施

    您运行 GraphQL API 的基础设施直接影响其性能、可靠性和可扩展性。对于生产工作负载,您有几个强大的选择:

    VPS 托管是大多数 GraphQL API 的绝佳起点。VPS 托管计划为您提供专用资源、root 访问权限以及完全按照您的需要配置 Node.js 运行时、反向代理和进程管理器的自由。AlexHost VPS 计划专为性能敏感的工作负载而构建,包括 SSD 存储和高带宽连接。

    专用服务器是当您的 GraphQL API 处理高查询量、复杂订阅工作负载或充当聚合多个微服务的网关时的正确选择。使用专用服务器,您可以获得对所有 CPU、RAM 和 I/O 资源的独占访问——没有嘈杂的邻居、没有资源争用,以及处理数千个订阅并发 WebSocket 连接的原始能力。

    GPU 托管值得考虑,如果您的 GraphQL API 充当机器学习推理、实时数据处理管道或 AI 驱动功能的接口层。AlexHost 的 GPU 托管为您提供 NVIDIA GPU 资源,使您的 API 能够以低延迟提供计算密集型结果。

    生产部署栈

    GraphQL API 的强大生产部署通常如下所示:

    Client → CDN / Load Balancer → Nginx (Reverse Proxy) → Node.js (PM2) → Database
                                                          ↘ Redis (Caching / PubSub)

    步骤 1:安装并配置 Nginx 作为反向代理

    server {
        listen 80;
        server_name api.yourdomain.com;
    
        location /graphql {
            proxy_pass http://localhost:4000;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_cache_bypass $http_upgrade;
        }
    }

    UpgradeConnection 标头对于 WebSocket 支持至关重要,该支持为 GraphQL 订阅提供支持。

    步骤 2:使用 PM2 管理您的 Node.js 进程

    npm install -g pm2
    pm2 start index.js --name graphql-api --instances max
    pm2 save
    pm2 startup

    --instances max 启用集群模式,为每个 CPU 核心生成一个工作进程以最大化吞吐量。

    步骤 3:使用 SSL 保护

    每个生产 API 都必须通过 HTTPS 提供。来自 AlexHost 的 SSL 证书确保客户端和您的 GraphQL 端点之间的所有传输中数据都被加密。这对于处理身份验证令牌、个人数据或财务信息的 API 尤其重要。

    # Install Certbot and obtain a certificate
    sudo apt install certbot python3-certbot-nginx
    sudo certbot --nginx -d api.yourdomain.com

    步骤 4:注册您的域名

    您的 API 需要一个令人难忘的、专业的域名。通过 AlexHost 的 域名注册让您可以访问所有主要 TLD,并提供直观的 DNS 管理,使您可以轻松将域名指向您的服务器并为暂存和生产环境配置子域。

    缓存策略

    GraphQL 的单端点模型意味着 HTTP 级缓存(依赖于 URL 区分)不能开箱即用。改用这些策略:

    • 持久化查询——客户端发送查询的哈希而不是完整查询字符串,通过哈希启用 CDN 缓存。
    • 响应缓存——基于查询哈希和变量在 Redis 中缓存解析器结果。
    • DataLoader——在单个请求执行中批处理和缓存数据库调用。
    • Apollo 缓存——客户端规范化缓存,消除冗余网络请求。

    11. 安全最佳实践 {#security}

    GraphQL 的灵活性是一把双刃剑。没有适当的防护措施,单个恶意查询可能会耗尽服务器的资源。

    查询深度限制

    防止深层嵌套查询导致递归数据库查找:

    import depthLimit from 'graphql-depth-limit';
    
    new ApolloServer({
      typeDefs,
      resolvers,
      validationRules: [depthLimit(7)],
    });

    查询复杂性分析

    为每个字段分配成本并拒绝超过复杂性预算的查询:

    import { createComplexityLimitRule } from 'graphql-validation-complexity';
    
    new ApolloServer({
      validationRules: [createComplexityLimitRule(1000)],
    });

    速率限制

    在 Nginx 级别或应用程序内使用 express-rate-limit 等库应用速率限制以防止滥用。

    身份验证和授权

    使用 context 函数将已认证的用户附加到每个请求:

    const server = new ApolloServer({
      typeDefs,
      resolvers,
      context: async ({ req }) => {
        const token = req.headers.authorization?.split('Bearer ')[1];
        const user = token ? await verifyToken(token) : null;
        return { user, db, authorLoader };
      },
    });

    然后在解析器中强制执行授权:

    ###

    15%

    全场主机优惠15%

    测试技能,享折扣

    使用代码:

    Skills
    开始使用