使用 Apollo Server 构建 GraphQL BFF
使用 GraphQL BFF 和 Apollo Server 优化前端 API
目录
前端专用后端(BFF) 模式结合 GraphQL 和 Apollo Server 为现代 Web 应用程序创建了强大的架构。
这种方法允许您构建针对客户端优化的 API,这些 API 可以从多个来源聚合数据,同时保持关注点的清晰分离。

理解 BFF 模式
前端专用后端(BFF)模式作为解决支持多个前端应用程序(Web、移动、桌面)的不同数据需求的挑战而出现。而不是强制所有客户端使用一个通用的 API,BFF 创建了针对每个客户端特定需求的专用后端服务。
BFF 的关键优势
- 客户端优化的 API:每个前端都能获得其需要的精确数据,以期望的格式呈现
- 减少客户端复杂性:数据聚合和转换逻辑转移到后端
- 独立演进:前端可以独立演进,而不会影响其他客户端或核心服务
- 更好的性能:减少往返次数和更小的数据负载提高应用速度
- 团队自主性:前端团队可以拥有自己的 BFF,实现更快的迭代
为什么 GraphQL 与 BFF 完美契合
GraphQL 灵活的查询语言使其非常适合 BFF 实现:
- 精确的数据获取:客户端仅请求其需要的字段
- 单次请求:在一个查询中合并来自多个来源的数据
- 强类型:模式在前端和后端之间提供清晰的合同
- 实时能力:订阅功能启用实时数据更新
- 开发人员体验:内省和 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 }),
});
性能优化技巧
- 使用 DataLoader:始终实现 DataLoader 以批处理和缓存请求
- 实现字段级缓存:在字段级别缓存昂贵的计算
- 查询复杂度分析:限制查询深度和复杂度以防止滥用
- 持久化查询:在生产环境中使用持久化查询以减少负载大小
- 响应压缩:启用 gzip/brotli 压缩以减少响应大小
- 监控查询性能:跟踪慢查询并优化解析器
- 使用 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`);
},
};
},
},
],
});
需要避免的常见陷阱
- N+1 查询问题:始终使用 DataLoader 获取相关数据
- 从后端过度获取数据:根据 GraphQL 选择集优化后端查询
- 缺少错误处理:实现适当的错误处理和日志记录
- 没有速率限制:使用速率限制保护您的 BFF 免受滥用
- 忽略安全性:验证输入,实现认证,并限制查询复杂度
- 差的模式设计:设计模式时要考虑客户端需求
- 没有缓存策略:在多个级别实现缓存