人人都会AI编程

24.4 GraphQL 接口网关:Apollo Server 方案

更新时间:2026-07-10

在第24章的前几节,我们已经看到了 BFF 层如何通过聚合接口、适配多端来简化前端的数据获取逻辑。然而,基于 REST 的 BFF 仍然需要为每个页面或组件手动编写聚合逻辑,当数据来源变多、字段需求频繁变化时,维护成本会迅速上升。GraphQL 提供了一种更优雅的解法:由前端声明所需数据,后端统一暴露一个灵活的查询入口。而将 GraphQL 用作 BFF 网关时,Apollo Server 是目前 Node.js 生态中最成熟、最完整的方案。

24.4.1 为什么需要 GraphQL 网关

在一个典型的微服务架构中,前端一个页面可能需要从用户服务、商品服务、订单服务、库存服务等分别获取数据。传统的 REST BFF 通常需要:

  1. 在后端详细定义每个聚合接口的路径与返回格式。
  2. 前端按照约定好的 JSON 结构解析数据,但往往需要多个请求或者包含大量用不上的字段。
  3. 需求变更时,BFF 后端必须同步调整,前后端紧密耦合。

GraphQL 网关则通过单一端点声明式查询解决了这些问题:

  • 前端自主选择所需字段,不多取不少取。
  • 网关负责将一条 GraphQL 查询拆解为对各个下游服务的调用,聚合后返回。
  • 类型系统(Schema)成为前后端的契约,字段变化可在编译期被发现。

把 GraphQL 放在 BFF 层,上游是各种 REST/gRPC 微服务,下游是 Web/移动客户端,这就是一个典型的 GraphQL 网关场景。

24.4.2 Apollo Server 的角色与能力

Apollo Server 是一个开源的、符合 GraphQL 规范的 JavaScript 服务端库,由 Apollo 团队维护。它的核心定位是:

  • 提供快速搭建 GraphQL 服务的能力,支持 Express、Koa、Fastify 等多种 Web 框架,也可以独立运行。
  • 内置 Playground 探索器,方便开发者调试。
  • 支持 Apollo Federation(联邦架构),专门用于构建跨多个 GraphQL 服务的网关。

但在 BFF 网关的场景下,我们更常使用 Apollo Server 的自定义 DataSource 能力,将下游 REST 服务包装为 GraphQL 数据源,而不是把每个微服务都做成独立的 GraphQL 子图。这样既可以获得 GraphQL 的优势,又无需重构下游服务。

24.4.3 Apollo Server 网关的典型架构

整体架构可以简化为三层:

[ 客户端 ]
     │ GraphQL 查询
     ▼
[ Apollo Server 网关 ]
     │ REST / gRPC 调用
     ▼
[ 微服务A ] [ 微服务B ] [ 微服务C ]

Apollo Server 在中间承担了schema 拼接、查询解析、数据加载、缓存、错误处理等职责。具体实现上,我们通常会:

  1. 定义 GraphQL Schema,将领域实体(User、Product、Order 等)映射出来。
  2. 为每个实体编写对应的 Resolver,在 Resolver 内部调用下游服务。
  3. 使用 DataLoader 解决 N+1 查询问题。
  4. 利用 Apollo Server 插件机制完成错误格式化、日志等。

24.4.4 实战:搭建一个 GraphQL 网关

假设我们有两个下游 REST 服务:

  • 用户服务 http://user-service/ 提供 /users/{id} 接口
  • 订单服务 http://order-service/ 提供 /orders?userId={id} 接口

目标是为前端提供一个组合查询:根据用户 ID 获取用户信息和该用户的订单列表。

第一步:安装依赖

npm install @apollo/server graphql @apollo/datasource-rest

第二步:定义 Schema

type User {
  id: ID!
  name: String!
  email: String
  orders: [Order]
}

type Order {
  id: ID!
  amount: Float
  status: String
}

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

第三步:编写 DataSource 封装 REST 调用

const { RESTDataSource } = require('@apollo/datasource-rest');

class UserAPI extends RESTDataSource {
  baseURL = 'http://user-service/';

  async getUser(id) {
    return this.get(`users/${id}`);
  }
}

class OrderAPI extends RESTDataSource {
  baseURL = 'http://order-service/';

  async getOrdersByUserId(userId) {
    return this.get(`orders`, { params: { userId } });
  }
}

第四步:编写 Resolvers 与组装服务

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

const resolvers = {
  Query: {
    user: (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUser(id);
    },
  },
  User: {
    orders: (user, _, { dataSources }) => {
      return dataSources.orderAPI.getOrdersByUserId(user.id);
    },
  },
};

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

  const { url } = await startStandaloneServer(server, {
    context: async () => {
      const { cache } = server;
      return {
        dataSources: {
          userAPI: new UserAPI({ cache }),
          orderAPI: new OrderAPI({ cache }),
        },
      };
    },
  });

  console.log(`🚀 GraphQL 网关运行在 ${url}`);
}

start();

启动之后,前端就可以用一条查询拿到组合数据:

query {
  user(id: "123") {
    name
    orders {
      id
      amount
    }
  }
}

网关内部会先调用用户服务获取 user,然后利用 User.orders 解析器调用订单服务,最终返回组合好的 JSON。

24.4.5 避免 N+1 查询:DataLoader

上面的例子中,订单服务是按 userId 单次查询的,但当我们要查询多个用户及其订单列表时,就可能会出现多个 getOrdersByUserId 调用。如果下游服务支持批量接口(如 /orders?userIds=1,2,3),我们可以用 DataLoader 将多次调用合并为一次。

const DataLoader = require('dataloader');

// 在 context 中构造 DataLoader
const orderLoader = new DataLoader(async (userIds) => {
  const ordersArray = await orderService.getOrdersBatch(userIds);
  return userIds.map(id => ordersArray[id] || []);
});

然后在 User.orders 解析器中改为 orderLoader.load(user.id),DataLoader 会自动收集同一帧内的所有 user.id,合并成一个批量请求。

24.4.6 缓存与性能优化

Apollo Server 内置了查询级别的缓存机制,但在网关层,更值得做的是针对下游服务的响应缓存。RESTDataSource 底层使用了 Apache Server 的缓存接口,我们可以配置 Redis 缓存来存储下游数据,从而减少微服务压力并加快响应。

简单定制缓存策略:

class CachedUserAPI extends RESTDataSource {
  async getUser(id) {
    return this.get(`users/${id}`, {
      cacheOptions: { ttl: 60 } // 缓存 60 秒
    });
  }
}

对于不会频繁变动的数据(如用户昵称、商品详情),合理设置 TTL 可以极大提升网关吞吐量。

24.4.7 错误处理与降级

当某个下游服务超时或异常时,GraphQL 网关不应该直接抛出 500,而是应该返回部分数据和错误信息。Apollo Server 支持在 Resolver 中捕获并抛出自定义错误,也可以利用插件对错误进行统一格式化。

const resolvers = {
  User: {
    orders: async (user, _, { dataSources }) => {
      try {
        return await dataSources.orderAPI.getOrdersByUserId(user.id);
      } catch (err) {
        console.error('订单服务异常', err);
        return null; // 字段级降级,不影响其他数据
      }
    }
  }
};

Apollo Server 在响应中会包含一个 errors 数组,记录退化字段的原因,客户端可以据此处理。

24.4.8 是否要引入 Apollo Federation?

上述方案被称为“单体式 GraphQL 网关”,适合团队较小、微服务数量不多的情况。当组织内有多个团队各自拥有独立的 GraphQL 服务时,可以考虑 Apollo Federation。它允许每个团队维护独立的子图(Subgraph),由 Gateway 自动组合成一个超级图(Supergraph),并智能地路由查询到对应的子服务。

但 Federation 会额外引入 schema 注册、服务发现等复杂度。对大多数以 REST 服务为下游的 BFF 网关来说,使用 Apollo Server 搭配自定义 DataSource 已经足够灵活和高效,也是更务实的入门方案。

24.4.9 小结与选型建议

Apollo Server 为 Node.js 生态提供了一套成熟且易于使用的 GraphQL 服务端框架。将其作为 BFF 层的接口网关,可以获得:

  • 灵活的数据聚合:Resolver 内部可以是 REST、数据库、甚至是另一个 GraphQL 调用。
  • 前端友好的开发体验:类型提示、即时查询、按需获取。
  • 可扩展性:支持自定义插件、持久化查询、缓存、安全校验等一系列生产特性。

在选择时,我们建议评估团队对 GraphQL 的熟悉程度、下游服务的接口一致性、以及前端的真实需求。如果团队已经有一定 GraphQL 经验,或者客户端需要同时适配 Web、iOS、Android 且页面数据需求多变,那么用 Apollo Server 构建 GraphQL 网关是一个值得投资的方向;如果只是简单的 BFF 聚合,Express + 手动聚合也能快速满足需求。

结合第23章的微服务与分布式架构,GraphQL 网关可以作为服务间通信的聚合层,进一步掩盖后端的复杂性,为前端提供一致且高效的数据访问能力。