使用 Apollo Server 构建 GraphQL BFF

使用 GraphQL BFF 和 Apollo Server 优化前端 API

目录

前端专用后端(BFF) 模式结合 GraphQL 和 Apollo Server 为现代 Web 应用程序创建了强大的架构。

这种方法允许您构建针对客户端优化的 API,这些 API 可以从多个来源聚合数据,同时保持关注点的清晰分离。

git-flow

理解 BFF 模式

前端专用后端(BFF)模式作为解决支持多个前端应用程序(Web、移动、桌面)的不同数据需求的挑战而出现。而不是强制所有客户端使用一个通用的 API,BFF 创建了针对每个客户端特定需求的专用后端服务。

BFF 的关键优势

  • 客户端优化的 API:每个前端都能获得其需要的精确数据,以期望的格式呈现
  • 减少客户端复杂性:数据聚合和转换逻辑转移到后端
  • 独立演进:前端可以独立演进,而不会影响其他客户端或核心服务
  • 更好的性能:减少往返次数和更小的数据负载提高应用速度
  • 团队自主性:前端团队可以拥有自己的 BFF,实现更快的迭代

为什么 GraphQL 与 BFF 完美契合

GraphQL 灵活的查询语言使其非常适合 BFF 实现:

  1. 精确的数据获取:客户端仅请求其需要的字段
  2. 单次请求:在一个查询中合并来自多个来源的数据
  3. 强类型:模式在前端和后端之间提供清晰的合同
  4. 实时能力:订阅功能启用实时数据更新
  5. 开发人员体验:内省和 GraphQL 玩具简化开发

为 BFF 设置 Apollo Server

Apollo Server 为构建 GraphQL BFF 层提供了坚实的基础。让我们逐步创建一个生产就绪的实现。

安装和基本设置

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';

// 定义你的 GraphQL 模式
const typeDefs = gql`
  type User {
    id: ID!
    email: String!
    name: String!
    orders: [Order!]!
  }

  type Order {
    id: ID!
    status: String!
    total: Float!
    items: [OrderItem!]!
  }

  type OrderItem {
    id: ID!
    productId: ID!
    quantity: Int!
    price: Float!
  }

  type Query {
    me: User
    user(id: ID!): User
    orders(userId: ID!): [Order!]!
  }
`;

// 实现解析器
const resolvers = {
  Query: {
    me: async (_, __, { dataSources, user }) => {
      return dataSources.userAPI.getUser(user.id);
    },
    user: async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUser(id);
    },
    orders: async (_, { userId }, { dataSources }) => {
      return dataSources.orderAPI.getOrdersByUser(userId);
    },
  },
  User: {
    orders: async (parent, _, { dataSources }) => {
      return dataSources.orderAPI.getOrdersByUser(parent.id);
    },
  },
};

// 创建 Apollo Server 实例
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// 启动服务器
const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => ({
    token: req.headers.authorization,
    dataSources: {
      userAPI: new UserAPI(),
      orderAPI: new OrderAPI(),
    },
  }),
  listen: { port: 4000 },
});

console.log(`🚀 服务器就绪,地址为 ${url}`);

实现数据源

数据源为从各种后端获取数据提供了清晰的抽象:

import { RESTDataSource } from '@apollo/datasource-rest';

class UserAPI extends RESTDataSource {
  override baseURL = 'https://api.example.com/users/';

  async getUser(id: string) {
    return this.get(`${id}`);
  }

  async getUsersByIds(ids: string[]) {
    return Promise.all(ids.map(id => this.getUser(id)));
  }

  async updateUser(id: string, data: any) {
    return this.patch(`${id}`, { body: data });
  }
}

class OrderAPI extends RESTDataSource {
  override baseURL = 'https://api.example.com/orders/';

  async getOrdersByUser(userId: string) {
    return this.get('', {
      params: { userId },
    });
  }

  async getOrder(id: string) {
    return this.get(`${id}`);
  }
}

使用 DataLoader 优化

DataLoader 批处理和缓存请求以防止 N+1 查询问题:

import DataLoader from 'dataloader';

const createUserLoader = (userAPI: UserAPI) => 
  new DataLoader(async (ids: readonly string[]) => {
    const users = await userAPI.getUsersByIds([...ids]);
    return ids.map(id => users.find(user => user.id === id));
  });

// 在上下文中使用
const context = async ({ req }) => ({
  dataSources: {
    userAPI: new UserAPI(),
    orderAPI: new OrderAPI(),
  },
  loaders: {
    userLoader: createUserLoader(new UserAPI()),
  },
});

高级 BFF 模式

聚合多个服务

BFF 的核心优势之一是结合来自多个后端服务的数据:

const resolvers = {
  Query: {
    dashboard: async (_, __, { dataSources, user }) => {
      // 并行从多个服务获取数据
      const [userData, orders, recommendations, analytics] = await Promise.all([
        dataSources.userAPI.getUser(user.id),
        dataSources.orderAPI.getRecentOrders(user.id),
        dataSources.recommendationAPI.getRecommendations(user.id),
        dataSources.analyticsAPI.getUserStats(user.id),
      ]);

      return {
        user: userData,
        recentOrders: orders,
        recommendations,
        stats: analytics,
      };
    },
  },
};

错误处理和弹性

为生产环境的 BFF 实现强大的错误处理:

import { GraphQLError } from 'graphql';

const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      try {
        const user = await dataSources.userAPI.getUser(id);
        
        if (!user) {
          throw new GraphQLError('用户未找到', {
            extensions: {
              code: 'NOT_FOUND',
              http: { status: 404 },
            },
          });
        }
        
        return user;
      } catch (error) {
        if (error instanceof GraphQLError) throw error;
        
        throw new GraphQLError('获取用户失败', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR',
            originalError: error.message,
          },
        });
      }
    },
  },
};

缓存策略

实现高效的缓存以提高性能:

import { KeyValueCache } from '@apollo/utils.keyvaluecache';
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  cache: new InMemoryLRUCache({
    maxSize: Math.pow(2, 20) * 100, // 100 MB
    ttl: 300, // 5 分钟
  }),
  plugins: [
    {
      async requestDidStart() {
        return {
          async willSendResponse({ response, contextValue }) {
            // 设置缓存控制头
            response.http.headers.set(
              'Cache-Control',
              'max-age=300, public'
            );
          },
        };
      },
    },
  ],
});

认证和授权

使用适当的认证和授权保护您的 BFF:

import jwt from 'jsonwebtoken';

const context = async ({ req }) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  
  if (!token) {
    return { user: null };
  }
  
  try {
    const user = jwt.verify(token, process.env.JWT_SECRET);
    return { user };
  } catch (error) {
    throw new GraphQLError('无效的认证令牌', {
      extensions: { code: 'UNAUTHENTICATED' },
    });
  }
};

// 保护解析器
const resolvers = {
  Query: {
    me: (_, __, { user }) => {
      if (!user) {
        throw new GraphQLError('您必须登录', {
          extensions: { code: 'UNAUTHENTICATED' },
        });
      }
      return user;
    },
  },
};

Apollo 联邦用于微服务

在多个团队和服务中工作时,Apollo 联邦启用分布式 GraphQL 架构:

import { buildSubgraphSchema } from '@apollo/subgraph';

const typeDefs = gql`
  extend schema
    @link(url: "https://specs.apollo.dev/federation/v2.3",
          import: ["@key", "@shareable"])

  type User @key(fields: "id") {
    id: ID!
    email: String!
    name: String!
  }
`;

const resolvers = {
  User: {
    __resolveReference: async (reference, { dataSources }) => {
      return dataSources.userAPI.getUser(reference.id);
    },
  },
};

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

性能优化技巧

  1. 使用 DataLoader:始终实现 DataLoader 以批处理和缓存请求
  2. 实现字段级缓存:在字段级别缓存昂贵的计算
  3. 查询复杂度分析:限制查询深度和复杂度以防止滥用
  4. 持久化查询:在生产环境中使用持久化查询以减少负载大小
  5. 响应压缩:启用 gzip/brotli 压缩以减少响应大小
  6. 监控查询性能:跟踪慢查询并优化解析器
  7. 使用 CDN 缓存静态模式:在边缘缓存内省查询

测试您的 BFF

为您的 GraphQL BFF 编写全面的测试:

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

describe('用户查询', () => {
  let server: ApolloServer;

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

  it('通过 id 获取用户', async () => {
    const result = await server.executeOperation({
      query: `
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            email
            name
          }
        }
      `,
      variables: { id: '123' },
    });

    expect(result.body.kind).toBe('single');
    if (result.body.kind === 'single') {
      expect(result.body.singleResult.data?.user).toEqual({
        id: '123',
        email: 'test@example.com',
        name: 'Test User',
      });
    }
  });
});

部署注意事项

容器化

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 4000

CMD ["node", "dist/index.js"]

环境配置

使用环境变量进行配置:

const config = {
  port: process.env.PORT || 4000,
  userServiceUrl: process.env.USER_SERVICE_URL,
  orderServiceUrl: process.env.ORDER_SERVICE_URL,
  jwtSecret: process.env.JWT_SECRET,
  nodeEnv: process.env.NODE_ENV || 'development',
};

监控和可观测性

实现全面的监控:

import { ApolloServerPluginInlineTrace } from '@apollo/server/plugin/inlineTrace';
import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    ApolloServerPluginInlineTrace(),
    process.env.NODE_ENV === 'production'
      ? ApolloServerPluginLandingPageDisabled()
      : ApolloServerPluginLandingPageLocalDefault(),
    {
      async requestDidStart() {
        const start = Date.now();
        return {
          async willSendResponse({ operationName, contextValue }) {
            const duration = Date.now() - start;
            console.log(`操作 ${operationName} 耗时 ${duration}ms`);
          },
        };
      },
    },
  ],
});

需要避免的常见陷阱

  1. N+1 查询问题:始终使用 DataLoader 获取相关数据
  2. 从后端过度获取数据:根据 GraphQL 选择集优化后端查询
  3. 缺少错误处理:实现适当的错误处理和日志记录
  4. 没有速率限制:使用速率限制保护您的 BFF 免受滥用
  5. 忽略安全性:验证输入,实现认证,并限制查询复杂度
  6. 差的模式设计:设计模式时要考虑客户端需求
  7. 没有缓存策略:在多个级别实现缓存

有用的链接