乐闻世界logo
搜索文章和话题

面试题手册

GraphQL 项目开发有哪些最佳实践

GraphQL 项目开发最佳实践在实际项目中使用 GraphQL 时,遵循最佳实践可以帮助团队构建可维护、可扩展和高质量的 GraphQL API。以下是项目开发中的关键最佳实践。1. 项目结构组织推荐的项目结构graphql-project/├── src/│ ├── graphql/│ │ ├── schema/│ │ │ ├── index.graphql│ │ │ ├── types/│ │ │ │ ├── user.graphql│ │ │ │ ├── post.graphql│ │ │ │ └── comment.graphql│ │ │ ├── queries/│ │ │ │ └── index.graphql│ │ │ ├── mutations/│ │ │ │ └── index.graphql│ │ │ └── subscriptions/│ │ │ └── index.graphql│ │ ├── resolvers/│ │ │ ├── index.ts│ │ │ ├── user.resolver.ts│ │ │ ├── post.resolver.ts│ │ │ └── comment.resolver.ts│ │ ├── directives/│ │ │ ├── auth.directive.ts│ │ │ ├── cache.directive.ts│ │ │ └── index.ts│ │ ├── loaders/│ │ │ ├── user.loader.ts│ │ │ ├── post.loader.ts│ │ │ └── index.ts│ │ ├── utils/│ │ │ ├── schema-merger.ts│ │ │ ├── validator.ts│ │ │ └── error-handler.ts│ │ └── context.ts│ ├── models/│ │ ├── User.ts│ │ ├── Post.ts│ │ └── Comment.ts│ ├── services/│ │ ├── userService.ts│ │ ├── postService.ts│ │ └── commentService.ts│ └── index.ts├── tests/│ ├── unit/│ │ └── resolvers/│ └── integration/│ └── api/├── package.json└── tsconfig.json模块化 Schema# types/user.graphqltype User { id: ID! name: String! email: String! createdAt: DateTime! updatedAt: DateTime!}input CreateUserInput { name: String! email: String!}input UpdateUserInput { name: String email: String}# queries/index.graphqlextend type Query { user(id: ID!): User users(limit: Int, offset: Int): [User!]!}# mutations/index.graphqlextend type Mutation { createUser(input: CreateUserInput!): User! updateUser(id: ID!, input: UpdateUserInput!): User! deleteUser(id: ID!): Boolean!}2. 代码规范命名约定// 类型定义使用 PascalCasetype User { id: ID! name: String!}// 字段使用 camelCasetype User { firstName: String! lastName: String!}// 输入类型以 Input 结尾input CreateUserInput { name: String! email: String!}// 枚举使用 PascalCaseenum UserRole { ADMIN USER GUEST}Resolver 命名// Resolver 文件命名: *.resolver.ts// user.resolver.tsexport const userResolvers = { Query: { user: () => {}, users: () => {} }, Mutation: { createUser: () => {}, updateUser: () => {}, deleteUser: () => {} }, User: { posts: () => {}, comments: () => {} }};3. 错误处理统一错误格式class GraphQLError extends Error { constructor( public message: string, public code: string, public extensions?: Record<string, any> ) { super(message); this.name = 'GraphQLError'; }}class ValidationError extends GraphQLError { constructor(message: string, public field?: string) { super(message, 'VALIDATION_ERROR', { field }); }}class NotFoundError extends GraphQLError { constructor(resource: string, id: string) { super(`${resource} with id ${id} not found`, 'NOT_FOUND'); }}class AuthenticationError extends GraphQLError { constructor(message = 'Authentication required') { super(message, 'AUTHENTICATION_ERROR'); }}class AuthorizationError extends GraphQLError { constructor(message = 'Not authorized') { super(message, 'AUTHORIZATION_ERROR'); }}错误处理中间件const formatError = (error: any) => { if (error instanceof GraphQLError) { return { message: error.message, code: error.code, extensions: error.extensions }; } // 生产环境不暴露详细错误 if (process.env.NODE_ENV === 'production') { return { message: 'Internal server error', code: 'INTERNAL_SERVER_ERROR' }; } return error;};4. 日志记录结构化日志import winston from 'winston';const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'combined.log' }) ]});// 在 Resolver 中使用export const resolvers = { Query: { user: async (_, { id }, context) => { logger.info('Fetching user', { userId: id }); try { const user = await userService.findById(id); logger.info('User fetched successfully', { userId: id }); return user; } catch (error) { logger.error('Error fetching user', { userId: id, error }); throw error; } } }};5. 测试策略单元测试import { userResolvers } from './user.resolver';import { UserService } from '../services/userService';describe('User Resolvers', () => { describe('Query.user', () => { it('should return user by id', async () => { const mockUser = { id: '1', name: 'John', email: 'john@example.com' }; jest.spyOn(UserService, 'findById').mockResolvedValue(mockUser); const result = await userResolvers.Query.user(null, { id: '1' }); expect(result).toEqual(mockUser); }); it('should throw NotFoundError if user not found', async () => { jest.spyOn(UserService, 'findById').mockResolvedValue(null); await expect( userResolvers.Query.user(null, { id: '1' }) ).rejects.toThrow('User with id 1 not found'); }); });});集成测试import { ApolloServer } from 'apollo-server';import { createTestClient } from 'apollo-server-testing';import { typeDefs, resolvers } from './schema';describe('GraphQL API Integration Tests', () => { const server = new ApolloServer({ typeDefs, resolvers }); const { query, mutate } = createTestClient(server); describe('Query.users', () => { it('should return all users', async () => { const GET_USERS = ` query GetUsers { users { id name email } } `; const { data, errors } = await query(GET_USERS); expect(errors).toBeUndefined(); expect(data.users).toBeDefined(); expect(Array.isArray(data.users)).toBe(true); }); }); describe('Mutation.createUser', () => { it('should create a new user', async () => { const CREATE_USER = ` mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email } } `; const variables = { input: { name: 'John Doe', email: 'john@example.com' } }; const { data, errors } = await mutate(CREATE_USER, { variables }); expect(errors).toBeUndefined(); expect(data.createUser).toBeDefined(); expect(data.createUser.name).toBe('John Doe'); }); });});6. 文档生成使用 GraphQL Code Generator// codegen.ymlschema: ./src/graphql/schema/**/*.graphqldocuments: ./src/graphql/documents/**/*.graphqlgenerates: ./src/generated/graphql.ts: plugins: - typescript - typescript-resolvers config: contextType: ./context#Context scalars: DateTime: Date自动生成文档import { ApolloServer } from 'apollo-server';const server = new ApolloServer({ typeDefs, resolvers, introspection: true, // 启用内省 playground: true, // 启用 Playground plugins: [ { requestDidStart: () => ({ didResolveOperation: (context) => { // 记录查询信息 console.log('Operation:', context.request.operationName); console.log('Variables:', context.request.variables); } }) } ]});7. 性能监控使用 Apollo Studioimport { ApolloServerPluginUsageReporting } from 'apollo-server-core';const server = new ApolloServer({ typeDefs, resolvers, plugins: [ ApolloServerPluginUsageReporting({ apiKey: process.env.APOLLO_KEY, graphRef: 'my-graph@current' }) ]});自定义监控import { promisify } from 'util';const metrics = { queryDuration: new Map<string, number[]>(), queryCount: new Map<string, number>()};const server = new ApolloServer({ typeDefs, resolvers, plugins: [ { requestDidStart: () => ({ didResolveOperation: (context) => { const operationName = context.request.operationName || 'anonymous'; const duration = context.metrics.duration; if (!metrics.queryDuration.has(operationName)) { metrics.queryDuration.set(operationName, []); } metrics.queryDuration.get(operationName)!.push(duration); metrics.queryCount.set( operationName, (metrics.queryCount.get(operationName) || 0) + 1 ); } }) } ]});8. 部署策略Docker 化# DockerfileFROM node:18-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --only=productionCOPY . .RUN npm run buildEXPOSE 4000CMD ["npm", "start"]# docker-compose.ymlversion: '3.8'services: graphql: build: . ports: - "4000:4000" environment: - NODE_ENV=production - DATABASE_URL=postgresql://user:pass@db:5432/graphql depends_on: - db - redis db: image: postgres:14 environment: - POSTGRES_DB=graphql - POSTGRES_USER=user - POSTGRES_PASSWORD=pass redis: image: redis:7CI/CD 流程# .github/workflows/ci.ymlname: CIon: [push, pull_request]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - run: npm ci - run: npm run lint - run: npm run test - run: npm run build deploy: needs: test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - run: docker build -t ghcr.io/${{ github.repository }}:latest . - run: docker push ghcr.io/${{ github.repository }}:latest9. 版本控制Schema 演进策略# 添加新字段 - 向后兼容type User { id: ID! name: String! email: String! # 新增字段 phoneNumber: String}# 废弃字段 - 提供替代方案type User { id: ID! name: String! # 废弃字段 fullName: String @deprecated(reason: "Use 'name' instead") email: String!}# 修改类型 - 需要谨慎# 不好的做法type User { id: ID! age: String # 从 Int 改为 String}# 好的做法 - 添加新字段,逐步迁移type User { id: ID! age: Int ageString: String @deprecated(reason: "Use 'age' instead")}10. 团队协作Schema 评审流程设计阶段: 团队讨论 Schema 设计文档阶段: 编写详细的 Schema 文档评审阶段: 团队成员评审 Schema实现阶段: 实现 Resolver 和业务逻辑测试阶段: 编写测试用例部署阶段: 部署到测试环境监控阶段: 监控 API 性能和错误代码审查清单[ ] Schema 设计合理[ ] 命名符合规范[ ] 错误处理完善[ ] 性能优化到位[ ] 安全措施完备[ ] 测试覆盖充分[ ] 文档完整准确[ ] 日志记录合理11. 最佳实践总结| 方面 | 最佳实践 ||------|----------|| 项目结构 | 模块化、分层清晰 || 代码规范 | 统一命名约定、代码风格 || 错误处理 | 统一错误格式、详细错误信息 || 日志记录 | 结构化日志、关键操作记录 || 测试策略 | 单元测试、集成测试、E2E 测试 || 文档生成 | 自动生成、保持更新 || 性能监控 | 实时监控、性能分析 || 部署策略 | 容器化、CI/CD 自动化 || 版本控制 | 向后兼容、渐进式演进 || 团队协作 | 代码审查、知识共享 |
阅读 0·2月21日 17:00

GraphQL 错误处理有哪些最佳实践

GraphQL 错误处理最佳实践GraphQL 提供了灵活的错误处理机制,但正确实现错误处理对于构建健壮的 API 至关重要。以下是 GraphQL 错误处理的关键策略和最佳实践。1. GraphQL 错误结构基本错误响应格式{ "data": { "user": null }, "errors": [ { "message": "User not found", "locations": [ { "line": 2, "column": 3 } ], "path": ["user"], "extensions": { "code": "NOT_FOUND", "timestamp": "2024-01-01T12:00:00Z" } } ]}错误字段说明message: 错误描述信息locations: 错误在查询中的位置path: 错误发生的字段路径extensions: 自定义扩展信息2. 自定义错误类创建错误类层次结构class GraphQLError extends Error { constructor(message, code, extensions = {}) { super(message); this.name = 'GraphQLError'; this.code = code; this.extensions = extensions; }}class ValidationError extends GraphQLError { constructor(message, field) { super(message, 'VALIDATION_ERROR', { field }); }}class NotFoundError extends GraphQLError { constructor(resource, id) { super(`${resource} with id ${id} not found`, 'NOT_FOUND', { resource, id }); }}class AuthenticationError extends GraphQLError { constructor(message = 'Authentication required') { super(message, 'AUTHENTICATION_ERROR'); }}class AuthorizationError extends GraphQLError { constructor(message = 'Not authorized') { super(message, 'AUTHORIZATION_ERROR'); }}class ConflictError extends GraphQLError { constructor(message) { super(message, 'CONFLICT_ERROR'); }}class RateLimitError extends GraphQLError { constructor(retryAfter) { super('Rate limit exceeded', 'RATE_LIMIT_ERROR', { retryAfter }); }}3. 在 Resolver 中抛出错误基本错误抛出const resolvers = { Query: { user: async (_, { id }) => { const user = await User.findById(id); if (!user) { throw new NotFoundError('User', id); } return user; } }, Mutation: { createUser: async (_, { input }) => { // 验证输入 if (!input.email || !isValidEmail(input.email)) { throw new ValidationError('Invalid email address', 'email'); } // 检查用户是否已存在 const existingUser = await User.findByEmail(input.email); if (existingUser) { throw new ConflictError('User with this email already exists'); } return User.create(input); } }};部分错误处理const resolvers = { Mutation: { createUsers: async (_, { inputs }) => { const results = []; const errors = []; for (const input of inputs) { try { const user = await User.create(input); results.push({ success: true, user }); } catch (error) { errors.push({ success: false, input, error: error.message }); } } return { results, errors, total: inputs.length, successCount: results.length, failureCount: errors.length }; } }};4. 错误格式化自定义错误格式化器const formatError = (error) => { // 处理自定义错误 if (error.originalError instanceof GraphQLError) { return { message: error.message, code: error.originalError.code, extensions: error.originalError.extensions }; } // 处理验证错误 if (error.originalError instanceof ValidationError) { return { message: error.message, code: 'VALIDATION_ERROR', field: error.originalError.field }; } // 生产环境不暴露详细错误 if (process.env.NODE_ENV === 'production') { return { message: 'Internal server error', code: 'INTERNAL_SERVER_ERROR' }; } // 开发环境返回完整错误信息 return { message: error.message, code: 'INTERNAL_SERVER_ERROR', stack: error.stack };};const server = new ApolloServer({ typeDefs, resolvers, formatError});5. 错误日志记录结构化错误日志const winston = require('winston');const logger = winston.createLogger({ level: 'error', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'error.log' }) ]});const server = new ApolloServer({ typeDefs, resolvers, plugins: [ { requestDidStart: () => ({ didEncounterErrors: (context) => { context.errors.forEach((error) => { logger.error('GraphQL Error', { message: error.message, code: error.extensions?.code, path: error.path, locations: error.locations, query: context.request.query, variables: context.request.variables }); }); } }) } ]});6. 错误恢复策略降级处理const resolvers = { Query: { userProfile: async (_, { id }, { dataSources }) => { try { // 尝试从主数据源获取 return await dataSources.userAPI.getUser(id); } catch (error) { // 如果主数据源失败,使用缓存数据 const cachedUser = await redis.get(`user:${id}`); if (cachedUser) { logger.warn('Using cached user data due to API failure', { userId: id }); return JSON.parse(cachedUser); } // 如果缓存也没有,返回默认数据 logger.error('Failed to fetch user data', { userId: id, error }); return { id, name: 'Unknown User', isFallback: true }; } } }};重试机制async function retryOperation(operation, maxRetries = 3, delay = 1000) { let lastError; for (let i = 0; i < maxRetries; i++) { try { return await operation(); } catch (error) { lastError = error; // 如果是可重试的错误,等待后重试 if (isRetryableError(error) && i < maxRetries - 1) { await new Promise(resolve => setTimeout(resolve, delay * (i + 1))); continue; } throw error; } } throw lastError;}function isRetryableError(error) { const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'SERVICE_UNAVAILABLE']; return retryableCodes.includes(error.code);}const resolvers = { Query: { externalData: async () => { return retryOperation(async () => { return await externalAPI.fetchData(); }); } }};7. 错误类型设计错误结果类型type Error { code: String! message: String! field: String details: String}type UserResult { user: User errors: [Error!]! success: Boolean!}type Mutation { createUser(input: CreateUserInput!): UserResult! updateUser(id: ID!, input: UpdateUserInput!): UserResult!}实现const resolvers = { Mutation: { createUser: async (_, { input }) => { const errors = []; // 验证输入 if (!input.name) { errors.push({ code: 'REQUIRED_FIELD', message: 'Name is required', field: 'name' }); } if (!input.email) { errors.push({ code: 'REQUIRED_FIELD', message: 'Email is required', field: 'email' }); } else if (!isValidEmail(input.email)) { errors.push({ code: 'INVALID_EMAIL', message: 'Invalid email format', field: 'email' }); } // 如果有错误,返回错误信息 if (errors.length > 0) { return { user: null, errors, success: false }; } // 创建用户 try { const user = await User.create(input); return { user, errors: [], success: true }; } catch (error) { return { user: null, errors: [{ code: 'INTERNAL_ERROR', message: 'Failed to create user', details: error.message }], success: false }; } } }};8. 错误监控和告警错误监控集成const Sentry = require('@sentry/node');Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV});const server = new ApolloServer({ typeDefs, resolvers, plugins: [ { requestDidStart: () => ({ didEncounterErrors: (context) => { context.errors.forEach((error) => { // 发送错误到 Sentry Sentry.captureException(error, { tags: { graphql: true, operation: context.request.operationName }, extra: { query: context.request.query, variables: context.request.variables } }); }); } }) } ]});错误告警const alertThreshold = { errorRate: 0.05, // 5% 错误率 errorCount: 100 // 100 个错误};let errorCount = 0;let requestCount = 0;const server = new ApolloServer({ typeDefs, resolvers, plugins: [ { requestDidStart: () => ({ willSendResponse: (context) => { requestCount++; if (context.response.errors && context.response.errors.length > 0) { errorCount += context.response.errors.length; // 检查是否需要告警 const errorRate = errorCount / requestCount; if (errorRate > alertThreshold.errorRate || errorCount > alertThreshold.errorCount) { sendAlert({ message: 'High error rate detected', errorRate, errorCount, requestCount }); } } } }) } ]});9. 错误处理最佳实践总结| 实践 | 说明 ||------|------|| 使用自定义错误类 | 创建清晰的错误层次结构 || 提供详细的错误信息 | 包含错误代码、消息和上下文 || 部分错误处理 | 允许部分成功的操作 || 错误格式化 | 统一错误响应格式 || 错误日志记录 | 记录所有错误用于分析 || 错误恢复 | 实现降级和重试机制 || 错误监控 | 实时监控错误率 || 错误告警 | 及时通知团队 |10. 常见错误场景及处理| 场景 | 错误类型 | 处理方式 ||------|----------|----------|| 资源不存在 | NotFoundError | 返回 404 错误 || 验证失败 | ValidationError | 返回字段级错误 || 认证失败 | AuthenticationError | 返回 401 错误 || 授权失败 | AuthorizationError | 返回 403 错误 || 数据冲突 | ConflictError | 返回 409 错误 || 速率限制 | RateLimitError | 返回 429 错误 || 网络错误 | NetworkError | 重试或降级 || 服务不可用 | ServiceUnavailableError | 使用缓存或降级 |
阅读 0·2月21日 17:00

GraphQL 订阅(Subscriptions)如何实现

GraphQL 订阅(Subscriptions)实现详解GraphQL 订阅允许客户端实时接收服务器推送的数据更新,是构建实时应用的关键功能。以下是 GraphQL 订阅的详细实现方案。1. 订阅基础概念订阅的工作原理客户端通过 WebSocket 建立持久连接客户端发送订阅查询服务器保持连接并监听事件当事件发生时,服务器推送数据到客户端客户端接收并处理更新订阅 vs 轮询| 特性 | 订阅 | 轮询 ||------|------|------|| 实时性 | 高 | 低 || 服务器负载 | 低(事件驱动) | 高(持续查询) || 网络开销 | 低(按需推送) | 高(定期请求) || 实现复杂度 | 高 | 低 || 适用场景 | 实时更新 | 定期检查 |2. 服务器端实现使用 graphql-subscriptionsconst { PubSub } = require('graphql-subscriptions');const pubsub = new PubSub();const POST_CREATED = 'POST_CREATED';const POST_UPDATED = 'POST_UPDATED';const COMMENT_ADDED = 'COMMENT_ADDED';const typeDefs = ` type Post { id: ID! title: String! content: String! author: User! createdAt: DateTime! } type Comment { id: ID! text: String! author: User! post: Post! createdAt: DateTime! } type Subscription { postCreated: Post! postUpdated(postId: ID!): Post! commentAdded(postId: ID!): Comment! }`;const resolvers = { Subscription: { postCreated: { subscribe: () => pubsub.asyncIterator([POST_CREATED]) }, postUpdated: { subscribe: (_, { postId }) => { const asyncIterator = pubsub.asyncIterator([POST_UPDATED]); return { [Symbol.asyncIterator]() { return (async function* () { for await (const event of asyncIterator) { // 过滤:只返回指定帖子的更新 if (event.postUpdated.id === postId) { yield event; } } })(); } }; } }, commentAdded: { subscribe: (_, { postId }) => { const asyncIterator = pubsub.asyncIterator([COMMENT_ADDED]); return { [Symbol.asyncIterator]() { return (async function* () { for await (const event of asyncIterator) { if (event.commentAdded.postId === postId) { yield event; } } })(); } }; } } }, Mutation: { createPost: async (_, { input }) => { const post = await Post.create(input); pubsub.publish(POST_CREATED, { postCreated: post }); return post; }, updatePost: async (_, { id, input }) => { const post = await Post.update(id, input); pubsub.publish(POST_UPDATED, { postUpdated: post }); return post; }, addComment: async (_, { input }) => { const comment = await Comment.create(input); pubsub.publish(COMMENT_ADDED, { commentAdded: comment }); return comment; } }};使用 Redis PubSubconst { RedisPubSub } = require('graphql-redis-subscriptions');const pubsub = new RedisPubSub({ connection: { host: 'localhost', port: 6379, retry_strategy: (options) => { if (options.error && options.error.code === 'ECONNREFUSED') { return new Error('Redis connection refused'); } if (options.total_retry_time > 1000 * 60 * 60) { return new Error('Redis retry time exhausted'); } if (options.attempt > 10) { return undefined; } return Math.min(options.attempt * 100, 3000); } }});// 使用方式与内存 PubSub 相同const resolvers = { Subscription: { postCreated: { subscribe: () => pubsub.asyncIterator([POST_CREATED]) } }};3. Apollo Server 订阅实现配置 Apollo Serverconst { ApolloServer } = require('apollo-server-express');const { createServer } = require('http');const { WebSocketServer } = require('ws');const { useServer } = require('graphql-ws/lib/use/ws');const server = new ApolloServer({ typeDefs, resolvers, context: ({ req, connection }) => { // HTTP 请求的 context if (req) { return { token: req.headers.authorization }; } // WebSocket 连接的 context if (connection) { return { token: connection.context.authorization }; } }});const httpServer = createServer(server);// 创建 WebSocket 服务器const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql'});useServer( { schema: server.schema, context: (ctx) => { // 验证连接 const token = ctx.connectionParams?.authorization; if (!token) { throw new Error('Unauthorized'); } return { token }; }, onConnect: (ctx) => { console.log('Client connected'); return { authorization: ctx.connectionParams?.authorization }; }, onDisconnect: (ctx, code, reason) => { console.log('Client disconnected', { code, reason }); } }, wsServer);server.listen().then(({ url }) => { console.log(`Server ready at ${url}`);});4. 客户端实现Apollo Client 订阅import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';import { WebSocketLink } from '@apollo/client/link/ws';import { getMainDefinition } from '@apollo/client/utilities';// HTTP 链接const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql'});// WebSocket 链接const wsLink = new WebSocketLink({ uri: 'ws://localhost:4000/graphql', options: { reconnect: true, connectionParams: { authToken: localStorage.getItem('token') }, lazy: true, connectionCallback: (error) => { if (error) { console.error('WebSocket connection error:', error); } else { console.log('WebSocket connected'); } } }});// 分割链接const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ); }, wsLink, httpLink);const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache()});// 使用订阅import { gql, useSubscription } from '@apollo/client';const POST_CREATED_SUBSCRIPTION = gql` subscription OnPostCreated { postCreated { id title content author { name } createdAt } }`;function PostList() { const { data, loading, error } = useSubscription(POST_CREATED_SUBSCRIPTION); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> <h3>New Post Created:</h3> <p>{data.postCreated.title}</p> </div> );}React Hooks 订阅import { useSubscription, useMutation } from '@apollo/client';function ChatRoom({ roomId }) { const MESSAGE_ADDED = gql` subscription OnMessageAdded($roomId: ID!) { messageAdded(roomId: $roomId) { id text author { name } createdAt } } `; const SEND_MESSAGE = gql` mutation SendMessage($roomId: ID!, $text: String!) { sendMessage(roomId: $roomId, text: $text) { id text } } `; const { data: messageData, loading } = useSubscription(MESSAGE_ADDED, { variables: { roomId } }); const [sendMessage] = useMutation(SEND_MESSAGE); const handleSendMessage = (text) => { sendMessage({ variables: { roomId, text } }); }; return ( <div> {loading ? ( <div>Connecting...</div> ) : ( <div> <MessageList messages={messageData?.messageAdded} /> <MessageInput onSend={handleSendMessage} /> </div> )} </div> );}5. 订阅过滤基于参数的过滤const resolvers = { Subscription: { notification: { subscribe: (_, { userId, types }) => { const asyncIterator = pubsub.asyncIterator(['NOTIFICATION']); return { [Symbol.asyncIterator]() { return (async function* () { for await (const event of asyncIterator) { const notification = event.notification; // 过滤用户 if (userId && notification.userId !== userId) { continue; } // 过滤类型 if (types && !types.includes(notification.type)) { continue; } yield event; } })(); } }; } } }};基于权限的过滤const resolvers = { Subscription: { userUpdate: { subscribe: async (_, __, context) => { // 验证用户权限 if (!context.user) { throw new Error('Unauthorized'); } const asyncIterator = pubsub.asyncIterator(['USER_UPDATE']); return { [Symbol.asyncIterator]() { return (async function* () { for await (const event of asyncIterator) { const update = event.userUpdate; // 只返回当前用户的更新 if (update.userId !== context.user.id) { continue; } // 只返回有权限查看的字段 const filteredUpdate = filterSensitiveFields(update, context.user.role); yield { userUpdate: filteredUpdate }; } })(); } }; } } }};6. 订阅错误处理连接错误处理const wsLink = new WebSocketLink({ uri: 'ws://localhost:4000/graphql', options: { reconnect: true, retryAttempts: 5, connectionParams: async () => { const token = await getAuthToken(); return { token }; }, on: { connected: () => console.log('WebSocket connected'), error: (error) => { console.error('WebSocket error:', error); // 尝试重新连接 }, closed: (event) => { console.log('WebSocket closed:', event); // 清理资源 } } }});订阅错误处理function useSubscriptionWithErrorHandling(query, options) { const { data, error, loading } = useSubscription(query, options); useEffect(() => { if (error) { console.error('Subscription error:', error); // 根据错误类型处理 if (error.networkError) { // 网络错误,尝试重连 handleNetworkError(error); } else if (error.graphQLErrors) { // GraphQL 错误 handleGraphQLError(error); } } }, [error]); return { data, error, loading };}7. 订阅性能优化批量发布class BatchPublisher { constructor(pubsub, eventName, batchSize = 10, flushInterval = 100) { this.pubsub = pubsub; this.eventName = eventName; this.batchSize = batchSize; this.flushInterval = flushInterval; this.batch = []; this.flushTimer = null; } add(event) { this.batch.push(event); if (this.batch.length >= this.batchSize) { this.flush(); } else if (!this.flushTimer) { this.flushTimer = setTimeout(() => this.flush(), this.flushInterval); } } flush() { if (this.batch.length === 0) return; // 批量发布 this.pubsub.publish(this.eventName, { batch: this.batch }); this.batch = []; if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null; } }}// 使用批量发布器const batchPublisher = new BatchPublisher(pubsub, 'BATCH_EVENTS');// 添加事件到批次batchPublisher.add({ type: 'event1', data: {} });batchPublisher.add({ type: 'event2', data: {} });订阅节流function useThrottledSubscription(query, options, throttleMs = 1000) { const { data, loading } = useSubscription(query, options); const [throttledData, setThrottledData] = useState(null); const lastUpdate = useRef(0); useEffect(() => { if (data) { const now = Date.now(); if (now - lastUpdate.current > throttleMs) { setThrottledData(data); lastUpdate.current = now; } } }, [data, throttleMs]); return { data: throttledData, loading };}8. 订阅监控连接监控const connectionMetrics = { activeConnections: 0, totalConnections: 0, disconnections: 0};const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql'});useServer( { schema: server.schema, onConnect: () => { connectionMetrics.totalConnections++; connectionMetrics.activeConnections++; console.log('Connection metrics:', connectionMetrics); }, onDisconnect: () => { connectionMetrics.activeConnections--; connectionMetrics.disconnections++; console.log('Connection metrics:', connectionMetrics); } }, wsServer);订阅指标const subscriptionMetrics = new Map();function trackSubscription(eventName) { if (!subscriptionMetrics.has(eventName)) { subscriptionMetrics.set(eventName, { count: 0, lastPublished: null }); } const metrics = subscriptionMetrics.get(eventName); metrics.count++; metrics.lastPublished = new Date();}// 在发布事件时追踪pubsub.publish(POST_CREATED, { postCreated: post });trackSubscription(POST_CREATED);9. 订阅最佳实践| 实践 | 说明 ||------|------|| 使用 Redis PubSub | 支持分布式部署 || 实现连接认证 | 确保订阅安全 || 添加错误处理 | 提高稳定性 || 实现过滤机制 | 减少不必要的数据推送 || 监控连接状态 | 及时发现问题 || 使用批量发布 | 提高性能 || 实现重连机制 | 提高可靠性 || 限制订阅数量 | 防止资源耗尽 || 设置超时时间 | 避免僵尸连接 || 记录订阅日志 | 便于调试和分析 |10. 常见问题及解决方案| 问题 | 原因 | 解决方案 ||------|------|----------|| 连接频繁断开 | 网络不稳定、超时 | 实现自动重连、增加超时时间 || 订阅延迟高 | 服务器负载高、处理慢 | 优化性能、使用批量发布 || 内存泄漏 | 未正确清理订阅 | 确保取消订阅、清理资源 || 数据不一致 | 缓存未更新 | 实现缓存失效机制 || 安全问题 | 未验证连接 | 实现连接认证和授权 |
阅读 0·2月21日 17:00

GraphQL 缓存策略有哪些实现方式

GraphQL 缓存策略与实现GraphQL 的缓存机制对于提高性能、减少服务器负载和改善用户体验至关重要。以下是 GraphQL 缓存的各种策略和实现方法。1. 客户端缓存Apollo Client 缓存import { ApolloClient, InMemoryCache } from '@apollo/client';const client = new ApolloClient({ uri: 'https://api.example.com/graphql', cache: new InMemoryCache({ typePolicies: { Query: { fields: { posts: { keyArgs: ['filter'], merge(existing, incoming) { return incoming; } } } }, Post: { keyFields: ['id', 'slug'] } } })});缓存策略配置const cache = new InMemoryCache({ typePolicies: { Query: { fields: { // 缓存单个结果 user: { read(_, { args, toReference }) { return toReference({ __typename: 'User', id: args.id }); } }, // 缓存列表 posts: { keyArgs: ['filter', 'sort'], merge(existing = [], incoming) { return [...existing, ...incoming]; } }, // 缓存分页数据 paginatedPosts: { keyArgs: false, merge(existing = { edges: [] }, incoming) { return { ...incoming, edges: [...existing.edges, ...incoming.edges] }; } } } } }});2. 服务器端缓存Redis 缓存实现const Redis = require('ioredis');const redis = new Redis();async function cachedResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; try { // 尝试从缓存获取 const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // 执行实际查询 const result = await fetchData(args); // 缓存结果(5 分钟过期) await redis.setex(cacheKey, 300, JSON.stringify(result)); return result; } catch (error) { console.error('Cache error:', error); // 缓存失败时直接查询 return await fetchData(args); }}const resolvers = { Query: { user: cachedResolver, posts: cachedResolver, post: cachedResolver }};Memcached 缓存const Memcached = require('memcached');const memcached = new Memcached('localhost:11211');async function memcachedResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; return new Promise((resolve, reject) => { memcached.get(cacheKey, async (err, data) => { if (err) { console.error('Memcached error:', err); return resolve(await fetchData(args)); } if (data) { return resolve(JSON.parse(data)); } // 执行查询并缓存 const result = await fetchData(args); memcached.set(cacheKey, JSON.stringify(result), 300, (err) => { if (err) console.error('Memcached set error:', err); }); resolve(result); }); });}3. 缓存失效策略基于时间的失效const TTL = { SHORT: 60, // 1 分钟 MEDIUM: 300, // 5 分钟 LONG: 3600, // 1 小时 VERY_LONG: 86400 // 24 小时};async function timeBasedResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; const ttl = getTTL(info.fieldName); const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } const result = await fetchData(args); await redis.setex(cacheKey, ttl, JSON.stringify(result)); return result;}function getTTL(fieldName) { const ttlMap = { 'user': TTL.MEDIUM, 'posts': TTL.SHORT, 'post': TTL.LONG, 'comments': TTL.SHORT }; return ttlMap[fieldName] || TTL.MEDIUM;}基于事件的失效const eventBus = new EventEmitter();// 监听数据变更事件eventBus.on('user.updated', async (userId) => { const pattern = `graphql:user:*${userId}*`; const keys = await redis.keys(pattern); if (keys.length > 0) { await redis.del(keys); }});eventBus.on('post.created', async () => { const pattern = 'graphql:posts*'; const keys = await redis.keys(pattern); if (keys.length > 0) { await redis.del(keys); }});// 在 Mutation 中触发事件const resolvers = { Mutation: { updateUser: async (_, { id, input }) => { const user = await User.update(id, input); eventBus.emit('user.updated', id); return user; }, createPost: async (_, { input }) => { const post = await Post.create(input); eventBus.emit('post.created'); return post; } }};4. 缓存预热预加载热门数据async function warmupCache() { console.log('Warming up cache...'); // 预加载热门用户 const popularUsers = await User.findPopular(100); for (const user of popularUsers) { const cacheKey = `graphql:user:${JSON.stringify({ id: user.id })}`; await redis.setex(cacheKey, 3600, JSON.stringify(user)); } // 预加载最新帖子 const latestPosts = await Post.findLatest(50); const cacheKey = `graphql:posts:${JSON.stringify({ limit: 50 })}`; await redis.setex(cacheKey, 300, JSON.stringify(latestPosts)); console.log('Cache warmed up successfully');}// 在应用启动时执行warmupCache();5. 缓存穿透保护布隆过滤器const { BloomFilter } = require('bloom-filters');// 创建布隆过滤器const userBloomFilter = new BloomFilter(1000000, 0.01);// 初始化时填充布隆过滤器async function initBloomFilter() { const userIds = await User.getAllIds(); userIds.forEach(id => userBloomFilter.add(id));}async function protectedResolver(parent, args, context, info) { const { id } = args; // 检查 ID 是否可能存在 if (!userBloomFilter.has(id)) { // ID 肯定不存在,直接返回 null return null; } // 可能存在,查询缓存或数据库 const cacheKey = `graphql:user:${JSON.stringify(args)}`; const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } const user = await User.findById(id); if (user) { await redis.setex(cacheKey, 300, JSON.stringify(user)); } else { // 缓存空值,防止缓存穿透 await redis.setex(cacheKey, 60, JSON.stringify(null)); } return user;}6. 缓存雪崩保护随机过期时间function getRandomTTL(baseTTL, variance = 0.2) { const randomFactor = 1 + (Math.random() * variance * 2 - variance); return Math.floor(baseTTL * randomFactor);}async function avalancheProtectedResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; const baseTTL = getTTL(info.fieldName); const ttl = getRandomTTL(baseTTL); const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } const result = await fetchData(args); await redis.setex(cacheKey, ttl, JSON.stringify(result)); return result;}互斥锁async function lockedResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; const lockKey = `lock:${cacheKey}`; // 尝试获取缓存 const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // 尝试获取锁 const lock = await redis.set(lockKey, '1', 'NX', 'EX', 10); if (lock) { try { // 获取锁成功,执行查询 const result = await fetchData(args); await redis.setex(cacheKey, 300, JSON.stringify(result)); return result; } finally { // 释放锁 await redis.del(lockKey); } } else { // 获取锁失败,等待并重试 await new Promise(resolve => setTimeout(resolve, 100)); return lockedResolver(parent, args, context, info); }}7. CDN 缓存使用 persisted queriesimport { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';import { sha256 } from 'crypto-hash';const persistedQueryLink = createPersistedQueryLink({ sha256, useGETForHashedQueries: true});const client = new ApolloClient({ link: persistedQueryLink.concat(httpLink), cache: new InMemoryCache()});配置 CDNconst server = new ApolloServer({ typeDefs, resolvers, cacheControl: { defaultMaxAge: 60, stripFormattedExtensions: false, calculateHttpHeaders: true }, plugins: [ require('apollo-cache-control')({ defaultMaxAge: 60 }) ]});8. 缓存监控缓存命中率监控const cacheMetrics = { hits: 0, misses: 0, errors: 0};async function monitoredResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; try { const cached = await redis.get(cacheKey); if (cached) { cacheMetrics.hits++; return JSON.parse(cached); } cacheMetrics.misses++; const result = await fetchData(args); await redis.setex(cacheKey, 300, JSON.stringify(result)); return result; } catch (error) { cacheMetrics.errors++; throw error; }}// 定期报告缓存指标setInterval(() => { const total = cacheMetrics.hits + cacheMetrics.misses; const hitRate = total > 0 ? cacheMetrics.hits / total : 0; console.log('Cache Metrics:', { hits: cacheMetrics.hits, misses: cacheMetrics.misses, errors: cacheMetrics.errors, hitRate: `${(hitRate * 100).toFixed(2)}%` });}, 60000);9. 缓存策略总结| 策略 | 适用场景 | 优势 | 劣势 ||------|----------|------|------|| 客户端缓存 | 重复查询相同数据 | 减少网络请求 | 占用客户端内存 || 服务器端缓存 | 高频查询 | 减少数据库负载 | 需要维护缓存一致性 || 基于时间失效 | 数据变化不频繁 | 实现简单 | 可能返回过期数据 || 基于事件失效 | 数据变化频繁 | 数据实时性高 | 实现复杂 || 缓存预热 | 热门数据 | 提升首次访问性能 | 需要识别热门数据 || CDN 缓存 | 静态数据 | 减少服务器负载 | 不适合动态数据 |10. 缓存最佳实践[ ] 根据数据特性选择合适的缓存策略[ ] 设置合理的缓存过期时间[ ] 实现缓存失效机制[ ] 监控缓存命中率[ ] 防止缓存穿透、雪崩、击穿[ ] 使用缓存预热提升性能[ ] 考虑使用 CDN 加速静态数据[ ] 定期清理无效缓存[ ] 实现缓存降级机制[ ] 记录缓存操作日志
阅读 0·2月21日 17:00

GraphQL 测试有哪些策略和最佳实践

GraphQL 测试策略与最佳实践GraphQL API 的测试对于确保代码质量和系统稳定性至关重要。以下是 GraphQL 测试的全面策略和最佳实践。1. 测试类型单元测试测试单个 Resolver 函数的逻辑。import { userResolvers } from './user.resolver';describe('User Resolvers', () => { describe('Query.user', () => { it('should return user by id', async () => { const mockUser = { id: '1', name: 'John', email: 'john@example.com' }; User.findById = jest.fn().mockResolvedValue(mockUser); const result = await userResolvers.Query.user(null, { id: '1' }); expect(result).toEqual(mockUser); expect(User.findById).toHaveBeenCalledWith('1'); }); it('should throw error if user not found', async () => { User.findById = jest.fn().mockResolvedValue(null); await expect( userResolvers.Query.user(null, { id: '1' }) ).rejects.toThrow('User not found'); }); }); describe('Mutation.createUser', () => { it('should create a new user', async () => { const input = { name: 'John Doe', email: 'john@example.com' }; const createdUser = { id: '1', ...input }; User.create = jest.fn().mockResolvedValue(createdUser); const result = await userResolvers.Mutation.createUser( null, { input } ); expect(result).toEqual(createdUser); expect(User.create).toHaveBeenCalledWith(input); }); });});集成测试测试多个组件协同工作。import { ApolloServer } from 'apollo-server';import { createTestClient } from 'apollo-server-testing';import { typeDefs, resolvers } from './schema';describe('GraphQL API Integration Tests', () => { const server = new ApolloServer({ typeDefs, resolvers }); const { query, mutate } = createTestClient(server); describe('Query.users', () => { it('should return all users', async () => { const GET_USERS = ` query GetUsers { users { id name email } } `; const { data, errors } = await query(GET_USERS); expect(errors).toBeUndefined(); expect(data.users).toBeDefined(); expect(Array.isArray(data.users)).toBe(true); }); }); describe('Mutation.createUser', () => { it('should create a new user', async () => { const CREATE_USER = ` mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email } } `; const variables = { input: { name: 'John Doe', email: 'john@example.com' } }; const { data, errors } = await mutate(CREATE_USER, { variables }); expect(errors).toBeUndefined(); expect(data.createUser).toBeDefined(); expect(data.createUser.name).toBe('John Doe'); }); });});E2E 测试测试完整的用户流程。import { createHttpLink } from 'apollo-link-http';import { ApolloClient } from 'apollo-client';import { InMemoryCache } from 'apollo-cache-inmemory';import fetch from 'node-fetch';describe('E2E Tests', () => { let client; beforeAll(() => { client = new ApolloClient({ link: createHttpLink({ uri: 'http://localhost:4000/graphql', fetch }), cache: new InMemoryCache() }); }); it('should complete user registration and login flow', async () => { // 注册用户 const REGISTER = ` mutation Register($input: RegisterInput!) { register(input: $input) { id name email } } `; const registerResult = await client.mutate({ mutation: REGISTER, variables: { input: { name: 'John Doe', email: 'john@example.com', password: 'password123' } } }); expect(registerResult.data.register).toBeDefined(); // 登录用户 const LOGIN = ` mutation Login($email: String!, $password: String!) { login(email: $email, password: $password) { token user { id name } } } `; const loginResult = await client.mutate({ mutation: LOGIN, variables: { email: 'john@example.com', password: 'password123' } }); expect(loginResult.data.login.token).toBeDefined(); expect(loginResult.data.login.user.name).toBe('John Doe'); });});2. 测试工具Jest// jest.config.jsmodule.exports = { testEnvironment: 'node', setupFilesAfterEnv: ['<rootDir>/tests/setup.js'], testMatch: ['**/tests/**/*.test.js'], collectCoverageFrom: [ 'src/**/*.js', '!src/**/*.test.js' ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } }};Mocha + Chaiconst { expect } = require('chai');const { ApolloServer } = require('apollo-server');const { createTestClient } = require('apollo-server-testing');describe('GraphQL API Tests', () => { let server; let client; beforeEach(() => { server = new ApolloServer({ typeDefs, resolvers }); client = createTestClient(server); }); it('should return user', async () => { const { data } = await client.query({ query: 'query { user(id: "1") { id name } }' }); expect(data.user).to.exist; expect(data.user.name).to.be.a('string'); });});3. Mock 数据使用 Mock 数据库const mockDatabase = { users: [ { id: '1', name: 'John', email: 'john@example.com' }, { id: '2', name: 'Jane', email: 'jane@example.com' } ], posts: [ { id: '1', title: 'Post 1', authorId: '1' }, { id: '2', title: 'Post 2', authorId: '2' } ]};const mockUserModel = { findById: (id) => { return Promise.resolve( mockDatabase.users.find(user => user.id === id) ); }, findAll: () => { return Promise.resolve(mockDatabase.users); }, create: (data) => { const newUser = { id: String(mockDatabase.users.length + 1), ...data }; mockDatabase.users.push(newUser); return Promise.resolve(newUser); }};// 在测试中使用jest.mock('../models/User', () => mockUserModel);使用 Faker.js 生成测试数据const faker = require('faker');function generateMockUser() { return { id: faker.random.uuid(), name: `${faker.name.firstName()} ${faker.name.lastName()}`, email: faker.internet.email(), age: faker.datatype.number({ min: 18, max: 80 }) };}function generateMockUsers(count = 10) { return Array.from({ length: count }, generateMockUser);}describe('User Tests', () => { it('should handle multiple users', async () => { const mockUsers = generateMockUsers(5); User.findAll = jest.fn().mockResolvedValue(mockUsers); const result = await resolvers.Query.users(); expect(result).toHaveLength(5); expect(result[0].email).toMatch(/@/); });});4. 测试 Context测试认证 Contextdescribe('Authenticated Resolvers', () => { it('should return current user', async () => { const mockUser = { id: '1', name: 'John' }; const context = { user: mockUser }; const result = await resolvers.Query.me(null, {}, context); expect(result).toEqual(mockUser); }); it('should throw error if not authenticated', async () => { const context = { user: null }; await expect( resolvers.Query.me(null, {}, context) ).rejects.toThrow('Authentication required'); });});测试数据源 Contextdescribe('Data Source Tests', () => { it('should use data source', async () => { const mockDataSource = { getUser: jest.fn().mockResolvedValue({ id: '1', name: 'John' }) }; const context = { dataSources: { userAPI: mockDataSource } }; await resolvers.Query.user(null, { id: '1' }, context); expect(mockDataSource.getUser).toHaveBeenCalledWith('1'); });});5. 测试错误处理测试验证错误describe('Validation Tests', () => { it('should validate email format', async () => { const input = { name: 'John', email: 'invalid-email' }; await expect( resolvers.Mutation.createUser(null, { input }) ).rejects.toThrow('Invalid email format'); }); it('should require required fields', async () => { const input = { name: 'John' // email is missing }; await expect( resolvers.Mutation.createUser(null, { input }) ).rejects.toThrow('Email is required'); });});测试 GraphQL 错误describe('Error Handling Tests', () => { it('should return GraphQL error for not found', async () => { User.findById = jest.fn().mockResolvedValue(null); const { data, errors } = await client.query({ query: 'query { user(id: "999") { id name } }' }); expect(data.user).toBeNull(); expect(errors).toBeDefined(); expect(errors[0].message).toContain('not found'); });});6. 测试订阅测试订阅 Resolverdescribe('Subscription Tests', () => { it('should publish post created event', async () => { const mockPost = { id: '1', title: 'New Post' }; const mockIterator = { [Symbol.asyncIterator]: jest.fn().mockReturnValue( (async function* () { yield { postCreated: mockPost }; })() ) }; pubsub.asyncIterator = jest.fn().mockReturnValue(mockIterator); const iterator = resolvers.Subscription.postCreated.subscribe(); const result = await iterator.next(); expect(result.value.postCreated).toEqual(mockPost); });});测试订阅过滤describe('Subscription Filtering Tests', () => { it('should filter subscriptions by userId', async () => { const mockIterator = { [Symbol.asyncIterator]: jest.fn().mockReturnValue( (async function* () { yield { notification: { userId: '1', message: 'Hello' } }; yield { notification: { userId: '2', message: 'Hi' } }; })() ) }; pubsub.asyncIterator = jest.fn().mockReturnValue(mockIterator); const iterator = resolvers.Subscription.notification.subscribe( null, { userId: '1' } ); const results = []; for await (const event of iterator) { results.push(event); if (results.length >= 2) break; } expect(results).toHaveLength(1); expect(results[0].notification.userId).toBe('1'); });});7. 性能测试测试查询性能describe('Performance Tests', () => { it('should handle large datasets efficiently', async () => { const largeDataset = generateMockUsers(10000); User.findAll = jest.fn().mockResolvedValue(largeDataset); const startTime = Date.now(); const result = await resolvers.Query.users(); const duration = Date.now() - startTime; expect(result).toHaveLength(10000); expect(duration).toBeLessThan(1000); // 应该在 1 秒内完成 });});测试 N+1 查询describe('N+1 Query Tests', () => { it('should not have N+1 query problem', async () => { const posts = generateMockPosts(10); const users = generateMockUsers(10); Post.findAll = jest.fn().mockResolvedValue(posts); User.findById = jest.fn() .mockImplementation((id) => Promise.resolve(users.find(u => u.id === id)) ); const result = await resolvers.Query.posts(); // 应该只调用一次批量查询,而不是 10 次单独查询 expect(User.findById).not.toHaveBeenCalledTimes(10); });});8. 测试覆盖率配置覆盖率// package.json{ "scripts": { "test": "jest", "test:coverage": "jest --coverage", "test:watch": "jest --watch" }}生成覆盖率报告npm run test:coverage9. 测试最佳实践| 实践 | 说明 ||------|------|| 测试所有 Resolvers | 确保每个 Resolver 都有测试 || 使用 Mock 数据 | 隔离测试,避免依赖外部服务 || 测试错误场景 | 验证错误处理逻辑 || 测试边界条件 | 测试空值、极大值等 || 保持测试独立 | 每个测试应该独立运行 || 使用描述性名称 | 测试名称应该清楚描述测试内容 || 测试 Context | 验证认证、授权等 Context 功能 || 测试订阅 | 确保订阅功能正常工作 || 性能测试 | 确保查询性能符合预期 || 监控覆盖率 | 保持高测试覆盖率 |10. 常见测试问题及解决方案| 问题 | 原因 | 解决方案 ||------|------|----------|| 测试不稳定 | 异步操作未正确处理 | 使用 async/await、适当的等待 || Mock 失败 | Mock 配置不正确 | 检查 Mock 的配置和调用 || 测试慢 | 测试数据量过大 | 使用较小的测试数据集 || 覆盖率低 | 未测试所有代码路径 | 增加测试用例,覆盖所有分支 || 测试难以维护 | 测试代码复杂 | 重构测试代码,使用测试工具 |
阅读 0·2月21日 17:00

GraphQL 查询、变更和订阅有什么区别

GraphQL 查询(Query)、变更(Mutation)和订阅(Subscription)的区别GraphQL 提供了三种主要的操作类型,每种类型都有特定的用途和语义。1. 查询(Query)定义: 用于获取数据,类似于 REST 中的 GET 请求。特点:只读操作,不会修改服务器上的数据可以并行执行多个查询可以嵌套查询以获取相关数据可以使用参数来过滤或定制结果示例:query GetUser($userId: ID!) { user(id: $userId) { id name email posts { id title createdAt } }}使用场景:获取用户信息列表查询数据详情展示报表生成2. 变更(Mutation)定义: 用于修改服务器上的数据,类似于 REST 中的 POST、PUT、DELETE 请求。特点:会修改服务器上的数据按顺序执行(串行),确保数据一致性通常返回修改后的数据或操作结果可以包含输入对象和参数示例:mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email createdAt }}mutation UpdateUser($userId: ID!, $input: UpdateUserInput!) { updateUser(id: $userId, input: $input) { id name email updatedAt }}mutation DeleteUser($userId: ID!) { deleteUser(id: $userId) { success message }}使用场景:创建新资源更新现有资源删除资源批量操作3. 订阅(Subscription)定义: 用于实时数据推送,当服务器端数据发生变化时,客户端会自动收到更新。特点:建立持久连接(通常使用 WebSocket)服务器主动推送数据到客户端适用于实时性要求高的场景可以包含过滤条件示例:subscription OnUserCreated { userCreated { id name email createdAt }}subscription OnPostUpdated($postId: ID!) { postUpdated(postId: $postId) { id title content updatedAt }}使用场景:聊天应用实时通知实时数据监控协作编辑核心区别对比| 特性 | Query | Mutation | Subscription ||------|-------|----------|--------------|| 数据修改 | 只读 | 修改数据 | 只读 || 执行方式 | 并行 | 串行 | 持久连接 || 实时性 | 按需请求 | 按需请求 | 实时推送 || 网络协议 | HTTP | HTTP | WebSocket || 缓存 | 可缓存 | 不可缓存 | 不可缓存 || 幂等性 | 幂等 | 非幂等 | 非幂等 |最佳实践Query 最佳实践避免过度获取,只请求需要的字段使用参数进行数据过滤合理使用分页利用 GraphQL 的类型系统进行数据验证Mutation 最佳实践使用输入对象(Input Types)封装参数返回修改后的完整数据提供清晰的成功/失败响应实现适当的错误处理和验证Subscription 最佳实践提供过滤条件减少不必要的数据推送实现连接管理和重连机制考虑使用负载均衡处理大量订阅设置合理的超时和心跳机制性能考虑Query: 使用 DataLoader 解决 N+1 查询问题Mutation: 使用事务确保数据一致性Subscription: 使用消息队列处理高并发订阅
阅读 0·2月21日 17:00

GraphQL 性能优化有哪些策略

GraphQL 性能优化策略GraphQL 的灵活性虽然强大,但也可能带来性能挑战。以下是优化 GraphQL API 性能的关键策略。1. 解决 N+1 查询问题问题描述当查询嵌套关系时,每个父对象都会触发一次子对象的查询,导致大量数据库查询。解决方案:DataLoaderconst DataLoader = require('dataloader');// 创建 User 的 DataLoaderconst userLoader = new DataLoader(async (userIds) => { const users = await User.findAll({ where: { id: userIds } }); // 按照请求的顺序返回结果 return userIds.map(id => users.find(user => user.id === id));});// 在 Resolver 中使用const resolvers = { Post: { author: (post) => userLoader.load(post.authorId) }};优势:批量查询,减少数据库往返自动去重和缓存保持查询顺序2. 查询复杂度分析限制查询深度const depthLimit = require('graphql-depth-limit');const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ depthLimit(7) // 限制查询深度为 7 层 ]});限制查询复杂度const { createComplexityLimitRule } = require('graphql-validation-complexity');const complexityLimitRule = createComplexityLimitRule(1000, { onCost: (cost) => console.log(`Query cost: ${cost}`)});const server = new ApolloServer({ typeDefs, resolvers, validationRules: [complexityLimitRule]});3. 字段级缓存使用 Redis 缓存const Redis = require('ioredis');const redis = new Redis();async function cachedResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; // 尝试从缓存获取 const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // 执行实际查询 const result = await fetchData(args); // 缓存结果(5 分钟过期) await redis.setex(cacheKey, 300, JSON.stringify(result)); return result;}使用 Apollo Client 缓存const client = new ApolloClient({ cache: new InMemoryCache({ typePolicies: { Query: { fields: { posts: { keyArgs: ['filter'], merge(existing, incoming) { return incoming; } } } } } })});4. 查询持久化使用 persisted queriesconst { PersistedQueryLink } = require('@apollo/client/link/persisted-queries');const { createPersistedQueryLink } = require('@apollo/client/link/persisted-queries');const { sha256 } = require('crypto-hash');const link = createPersistedQueryLink({ sha256, useGETForHashedQueries: true });const client = new ApolloClient({ link: link.concat(httpLink), cache: new InMemoryCache()});优势:减少网络传输提高安全性降低服务器负载5. 数据库优化使用索引// 为常用查询字段添加索引User.addIndex('email');Post.addIndex(['authorId', 'createdAt']);优化关联查询// 使用 JOIN 而不是多次查询const postsWithAuthors = await Post.findAll({ include: [{ model: User, as: 'author', attributes: ['id', 'name', 'email'] }]});6. 分页优化使用游标分页type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String}type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! totalCount: Int!}type PostEdge { node: Post! cursor: String!}type Query { posts(after: String, first: Int, before: String, last: Int): PostConnection!}优势:性能稳定,不受数据量影响支持实时数据更新更好的用户体验7. 批量操作批量查询# 不好的做法 - 多次查询query { user1: user(id: "1") { name } user2: user(id: "2") { name } user3: user(id: "3") { name }}# 好的做法 - 批量查询query { users(ids: ["1", "2", "3"]) { id name }}批量变更mutation { createPosts(input: [ { title: "Post 1", content: "Content 1" }, { title: "Post 2", content: "Content 2" }, { title: "Post 3", content: "Content 3" } ]) { id title }}8. 懒加载使用 @defer 指令query GetUser($userId: ID!) { user(id: $userId) { id name email ... @defer { posts { id title } } }}优势:优先加载关键数据提高首屏渲染速度改善用户体验9. 订阅优化使用消息队列const { PubSub } = require('graphql-subscriptions');const RedisPubSub = require('graphql-redis-subscriptions').RedisPubSub;const pubsub = new RedisPubSub({ connection: { host: 'localhost', port: 6379 }});const POST_UPDATED = 'POST_UPDATED';const resolvers = { Mutation: { updatePost: (_, { id, input }) => { const updatedPost = updatePost(id, input); pubsub.publish(POST_UPDATED, { postUpdated: updatedPost }); return updatedPost; } }, Subscription: { postUpdated: { subscribe: () => pubsub.asyncIterator([POST_UPDATED]) } }};10. 监控和分析使用 Apollo Studioconst { ApolloServerPluginUsageReporting } = require('apollo-server-core');const server = new ApolloServer({ typeDefs, resolvers, plugins: [ ApolloServerPluginUsageReporting({ apiKey: process.env.APOLLO_KEY, graphRef: 'my-graph@current' }) ]});自定义监控const resolvers = { Query: { user: async (_, { id }, context) => { const startTime = Date.now(); try { const user = await User.findById(id); const duration = Date.now() - startTime; // 记录查询性能 context.metrics.recordQuery('user', duration); return user; } catch (error) { context.metrics.recordError('user', error); throw error; } } }};11. 性能优化检查清单[ ] 使用 DataLoader 解决 N+1 查询问题[ ] 实施查询深度和复杂度限制[ ] 配置适当的缓存策略[ ] 使用查询持久化[ ] 优化数据库查询和索引[ ] 实现高效的分页[ ] 支持批量操作[ ] 使用懒加载指令[ ] 优化订阅性能[ ] 设置监控和分析工具[ ] 定期进行性能测试[ ] 优化网络传输(压缩、HTTP/2)12. 常见性能问题及解决方案| 问题 | 原因 | 解决方案 ||------|------|----------|| 查询响应慢 | N+1 查询 | 使用 DataLoader || 数据库负载高 | 过度获取数据 | 限制查询字段,使用分页 || 内存占用高 | 缓存策略不当 | 设置合理的缓存过期时间 || 网络传输慢 | 查询过大 | 使用查询持久化,启用压缩 || 订阅延迟高 | 消息队列性能差 | 使用高性能消息队列(Redis) |
阅读 0·2月21日 17:00

GraphQL 有哪些重要的工具和生态系统

GraphQL 工具和生态系统详解GraphQL 拥有丰富的工具和生态系统,这些工具可以大大提高开发效率。以下是 GraphQL 生态系统中最重要的工具和库。1. 服务器框架Apollo Serverconst { ApolloServer } = require('apollo-server');const typeDefs = ` type Query { hello: String! }`;const resolvers = { Query: { hello: () => 'Hello World!' }};const server = new ApolloServer({ typeDefs, resolvers });server.listen().then(({ url }) => { console.log(`Server ready at ${url}`);});特性:开箱即用的 GraphQL 服务器内置 GraphQL Playground支持订阅、文件上传等丰富的插件系统集成 Apollo StudioGraphQL Yogaimport { createServer } from 'graphql-yoga';const server = createServer({ schema: { typeDefs: ` type Query { hello: String! } `, resolvers: { Query: { hello: () => 'Hello World!' } } }});server.start().then(() => { console.log('Server is running on http://localhost:4000');});特性:轻量级、高性能支持 Express、Fastify 等内置 WebSocket 支持支持 GraphQL 文件上传兼容 Apollo Server2. 客户端库Apollo Clientimport { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';const client = new ApolloClient({ link: new HttpLink({ uri: 'https://api.example.com/graphql' }), cache: new InMemoryCache()});特性:功能强大的缓存系统支持查询、变更、订阅乐观更新分页支持开发工具集成Relayimport { RelayEnvironment, RecordSource, Store, Network } from 'relay-runtime';const network = Network.create((operation, variables) => { return fetch('https://api.example.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: operation.text, variables }) }).then(response => response.json());});const environment = new RelayEnvironment({ network, store: new Store(new RecordSource())});特性:Facebook 开发高度优化的数据获取自动数据规范化强类型查询适合大型应用URQLimport { createClient, Provider } from 'urql';import { cacheExchange, fetchExchange } from '@urql/core';const client = createClient({ url: 'https://api.example.com/graphql', exchanges: [cacheExchange, fetchExchange]});特性:轻量级(7KB)简单的 API可扩展的交换系统良好的 TypeScript 支持活跃的社区3. 开发工具GraphQL Code Generator# 安装npm install @graphql-codegen/cli# 配置文件 codegen.ymlschema: https://api.example.com/graphqldocuments: ./src/**/*.graphqlgenerates: ./src/generated/graphql.ts: plugins: - typescript - typescript-operations - typescript-react-apollo config: withHooks: true功能:从 Schema 生成 TypeScript 类型生成 React Hooks生成 Resolvers 类型支持多种框架自动保持类型同步GraphQL Playgroundconst { ApolloServer } = require('apollo-server');const server = new ApolloServer({ typeDefs, resolvers, playground: true, // 启用 Playground playgroundOptions: { endpoint: '/graphql', settings: { 'editor.theme': 'dark' } }});功能:交互式 GraphQL IDE实时查询执行自动完成和文档查询历史支持多个端点Apollo Studioconst { ApolloServerPluginUsageReporting } = require('apollo-server-core');const server = new ApolloServer({ typeDefs, resolvers, plugins: [ ApolloServerPluginUsageReporting({ apiKey: process.env.APOLLO_KEY, graphRef: 'my-graph@current' }) ]});功能:追踪查询性能分析 Schema 使用情况监控错误率实时告警Schema 变更管理4. 测试工具GraphQL Testing Libraryimport { render, screen } from '@testing-library/react';import { MockedProvider } from '@apollo/client/testing';import { GET_USERS } from './queries';import UserList from './UserList';const mocks = [ { request: { query: GET_USERS }, result: { data: { users: [ { id: '1', name: 'John' }, { id: '2', name: 'Jane' } ] } } }];test('renders user list', () => { render( <MockedProvider mocks={mocks}> <UserList /> </MockedProvider> ); expect(screen.getByText('John')).toBeInTheDocument(); expect(screen.getByText('Jane')).toBeInTheDocument();});Apollo Server Testingimport { ApolloServer } from 'apollo-server';import { createTestClient } from 'apollo-server-testing';const server = new ApolloServer({ typeDefs, resolvers });const { query, mutate } = createTestClient(server);test('should return users', async () => { const { data, errors } = await query({ query: 'query { users { id name } }' }); expect(errors).toBeUndefined(); expect(data.users).toBeDefined();});5. 性能优化工具DataLoaderconst DataLoader = require('dataloader');const userLoader = new DataLoader(async (userIds) => { const users = await User.findAll({ where: { id: userIds } }); return userIds.map(id => users.find(user => user.id === id));});const resolvers = { Post: { author: (post) => userLoader.load(post.authorId) }};功能:批量查询自动去重缓存结果解决 N+1 查询问题GraphQL Cache Controltype Query { user(id: ID!): User @cacheControl(maxAge: 300) posts: [Post!]! @cacheControl(maxAge: 60)}功能:声明式缓存控制CDN 友好减少服务器负载提高响应速度6. 安全工具GraphQL Shieldconst { shield, rule } = require('graphql-shield');const isAuthenticated = rule()((parent, args, context) => { return !!context.user;});const isAdmin = rule()((parent, args, context) => { return context.user?.role === 'ADMIN';});const permissions = shield({ Query: { me: isAuthenticated, users: isAdmin }, Mutation: { createUser: isAuthenticated, deleteUser: isAdmin }});const server = new ApolloServer({ typeDefs, resolvers, middleware: [permissions]});功能:声明式权限控制字段级授权类型安全易于维护Envelopimport { envelop } from '@envelop/core';import { useAuth } from '@envelop/auth';import { useRateLimiter } from '@envelop/rate-limiter';const getEnveloped = envelop({ plugins: [ useAuth({ resolveUserFn: (context) => context.user }), useRateLimiter({ tokenLimit: 100, windowSize: 10000 }) ]});功能:可插拔的插件系统认证和授权速率限制查询复杂度限制7. 文档工具GraphQL Docs# 生成文档npx @graphql-docs/cli generate \ --schema ./schema.graphql \ --output ./docs.md功能:从 Schema 生成文档支持多种格式(Markdown、HTML)自定义模板集成到 CI/CDSpectaQL# 生成交互式文档npx spectaql \ --schema ./schema.graphql \ --output-dir ./docs功能:生成交互式文档网站支持查询测试自定义主题多语言支持8. 微服务工具Apollo Federationconst { ApolloServer } = require('@apollo/server');const { buildSubgraphSchema } = require('@apollo/subgraph');const server = new ApolloServer({ schema: buildSubgraphSchema({ typeDefs, resolvers })});功能:构建联合图分布式 Schema独立部署类型安全的跨服务查询GraphQL Meshimport { createMeshHTTPHandler } from '@graphql-mesh/http';import { loadGraphQLConfig } from '@graphql-mesh/config';const config = await loadGraphQLConfig();const handler = createMeshHTTPHandler(config);// 使用 handlerapp.use('/graphql', handler);功能:整合多个 GraphQL 和 REST API自动生成联合 Schema支持多种数据源无需修改现有 API9. 监控和调试Apollo Tracingconst { ApolloServerPluginUsageReporting } = require('apollo-server-core');const server = new ApolloServer({ typeDefs, resolvers, plugins: [ ApolloServerPluginUsageReporting({ includeRequestContext: true, includeResponseContext: true }) ]});GraphQL Inspector# 安装 Chrome 扩展# 或使用 CLInpx graphql-inspector diff \ --schema ./schema.graphql \ --endpoint https://api.example.com/graphql功能:Schema 变更检测查询分析性能分析安全审计10. 工具选择指南| 场景 | 推荐工具 ||------|----------|| 服务器开发 | Apollo Server, GraphQL Yoga || 客户端开发 | Apollo Client, URQL || 大型应用 | Relay || 类型生成 | GraphQL Code Generator || 测试 | GraphQL Testing Library || 性能优化 | DataLoader || 安全 | GraphQL Shield, Envelop || 文档 | GraphQL Docs, SpectaQL || 微服务 | Apollo Federation, GraphQL Mesh || 监控 | Apollo Studio, GraphQL Inspector |11. 生态系统最佳实践| 实践 | 说明 ||------|------|| 选择合适的工具 | 根据项目需求选择工具 || 保持工具更新 | 定期更新依赖 || 使用类型生成 | 利用代码生成提高开发效率 || 实施监控 | 使用监控工具追踪性能 || 编写测试 | 使用测试工具确保质量 || 优化性能 | 使用性能优化工具 || 重视安全 | 使用安全工具保护 API || 生成文档 | 使用文档工具自动生成文档 || 集成 CI/CD | 将工具集成到开发流程 || 持续学习 | 关注 GraphQL 生态系统的发展 |12. 常见问题及解决方案| 问题 | 解决方案 ||------|----------|| 工具选择困难 | 根据项目规模和团队经验选择 || 工具版本冲突 | 使用包管理器解决依赖冲突 || 学习曲线陡峭 | 从简单工具开始,逐步学习 || 性能问题 | 使用性能工具分析和优化 || 安全漏洞 | 定期更新工具,使用安全工具 |
阅读 0·2月21日 17:00

GraphQL 客户端开发有哪些关键要点

GraphQL 客户端开发指南GraphQL 客户端开发是构建现代前端应用的关键部分。以下是使用 Apollo Client 和其他 GraphQL 客户端的全面指南。1. Apollo Client 配置基本配置import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';const httpLink = new HttpLink({ uri: 'https://api.example.com/graphql', credentials: 'include'});const client = new ApolloClient({ link: httpLink, cache: new InMemoryCache(), defaultOptions: { watchQuery: { fetchPolicy: 'cache-and-network', errorPolicy: 'all' }, query: { fetchPolicy: 'network-only', errorPolicy: 'all' }, mutate: { errorPolicy: 'all' } }});带认证的配置import { ApolloClient, InMemoryCache, createHttpLink, ApolloLink } from '@apollo/client';import { setContext } from '@apollo/client/link/context';const httpLink = createHttpLink({ uri: 'https://api.example.com/graphql'});const authLink = setContext((_, { headers }) => { const token = localStorage.getItem('token'); return { headers: { ...headers, authorization: token ? `Bearer ${token}` : '' } };});const client = new ApolloClient({ link: authLink.concat(httpLink), cache: new InMemoryCache()});2. 查询数据使用 useQuery Hookimport { useQuery, gql } from '@apollo/client';const GET_USERS = gql` query GetUsers($limit: Int, $offset: Int) { users(limit: $limit, offset: $offset) { id name email createdAt } }`;function UserList() { const { loading, error, data, fetchMore } = useQuery(GET_USERS, { variables: { limit: 10, offset: 0 }, notifyOnNetworkStatusChange: true }); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> {data.users.map(user => ( <div key={user.id}> <h3>{user.name}</h3> <p>{user.email}</p> </div> ))} <button onClick={() => fetchMore({ variables: { offset: data.users.length }, updateQuery: (prev, { fetchMoreResult }) => { if (!fetchMoreResult) return prev; return { users: [...prev.users, ...fetchMoreResult.users] }; } })} > Load More </button> </div> );}使用 lazy queryimport { useLazyQuery, gql } from '@apollo/client';const GET_USER = gql` query GetUser($id: ID!) { user(id: $id) { id name email } }`;function UserSearch() { const [getUser, { loading, error, data }] = useLazyQuery(GET_USER); const [userId, setUserId] = useState(''); return ( <div> <input value={userId} onChange={(e) => setUserId(e.target.value)} placeholder="Enter user ID" /> <button onClick={() => getUser({ variables: { id: userId } })}> Search </button> {loading && <div>Loading...</div>} {error && <div>Error: {error.message}</div>} {data && ( <div> <h3>{data.user.name}</h3> <p>{data.user.email}</p> </div> )} </div> );}3. 修改数据使用 useMutation Hookimport { useMutation, gql } from '@apollo/client';const CREATE_USER = gql` mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email } }`;function CreateUserForm() { const [createUser, { loading, error }] = useMutation(CREATE_USER, { update(cache, { data: { createUser } }) { cache.modify({ fields: { users(existingUsers = []) { const newUserRef = cache.writeFragment({ data: createUser, fragment: gql` fragment NewUser on User { id name email } ` }); return [...existingUsers, newUserRef]; } } }); } }); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const handleSubmit = (e) => { e.preventDefault(); createUser({ variables: { input: { name, email } } }); }; return ( <form onSubmit={handleSubmit}> <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" /> <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" /> <button type="submit" disabled={loading}> {loading ? 'Creating...' : 'Create User'} </button> {error && <div>Error: {error.message}</div>} </form> );}乐观更新const [updateUser, { loading }] = useMutation(UPDATE_USER, { optimisticResponse: (variables) => ({ updateUser: { __typename: 'User', id: variables.id, name: variables.input.name, email: variables.input.email } }), update(cache, { data: { updateUser } }) { cache.writeFragment({ id: `User:${updateUser.id}`, fragment: gql` fragment UpdateUser on User { name email } `, data: updateUser }); }});4. 缓存管理配置缓存策略const cache = new InMemoryCache({ typePolicies: { Query: { fields: { users: { keyArgs: ['filter', 'sort'], merge(existing = [], incoming) { return [...existing, ...incoming]; } }, read(existing, { args: { offset, limit } }) { return existing && existing.slice(offset, offset + limit); } }, user: { read(_, { args, toReference }) { return toReference({ __typename: 'User', id: args.id }); } } } }, User: { keyFields: ['id', 'email'] } }});手动更新缓存import { useApolloClient } from '@apollo/client';function UpdateUserButton({ userId, newData }) { const client = useApolloClient(); const handleClick = () => { client.writeFragment({ id: `User:${userId}`, fragment: gql` fragment UserFragment on User { name email } `, data: newData }); }; return <button onClick={handleClick}>Update User</button>;}清除缓存function ClearCacheButton() { const client = useApolloClient(); const handleClick = () => { client.clearStore(); }; return <button onClick={handleClick}>Clear Cache</button>;}5. 分页基于偏移的分页const GET_POSTS = gql` query GetPosts($offset: Int, $limit: Int) { posts(offset: $offset, limit: $limit) { id title content author { name } } }`;function PostList() { const { loading, data, fetchMore } = useQuery(GET_POSTS, { variables: { offset: 0, limit: 10 } }); return ( <div> {data?.posts.map(post => ( <div key={post.id}> <h3>{post.title}</h3> <p>{post.content}</p> </div> ))} <button onClick={() => fetchMore({ variables: { offset: data.posts.length }, updateQuery: (prev, { fetchMoreResult }) => { if (!fetchMoreResult) return prev; return { posts: [...prev.posts, ...fetchMoreResult.posts] }; } })} > Load More </button> </div> );}基于游标的分页const GET_POSTS = gql` query GetPosts($after: String, $first: Int) { posts(after: $after, first: $first) { edges { node { id title content } cursor } pageInfo { hasNextPage endCursor } } }`;function PostList() { const { loading, data, fetchMore } = useQuery(GET_POSTS, { variables: { first: 10 } }); return ( <div> {data?.posts.edges.map(({ node }) => ( <div key={node.id}> <h3>{node.title}</h3> <p>{node.content}</p> </div> ))} {data?.posts.pageInfo.hasNextPage && ( <button onClick={() => fetchMore({ variables: { after: data.posts.pageInfo.endCursor }, updateQuery: (prev, { fetchMoreResult }) => { if (!fetchMoreResult) return prev; return { posts: { ...fetchMoreResult.posts, edges: [ ...prev.posts.edges, ...fetchMoreResult.posts.edges ] } }; } })} > Load More </button> )} </div> );}6. 错误处理处理 GraphQL 错误function UserList() { const { loading, error, data } = useQuery(GET_USERS); if (loading) return <div>Loading...</div>; if (error) { if (error.graphQLErrors) { return ( <div> GraphQL Errors: {error.graphQLErrors.map((err, i) => ( <div key={i}>{err.message}</div> ))} </div> ); } if (error.networkError) { return <div>Network Error: {error.networkError.message}</div>; } return <div>Error: {error.message}</div>; } return <div>{/* render data */}</div>;}全局错误处理import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client';import { onError } from '@apollo/client/link/error';const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => { if (graphQLErrors) { graphQLErrors.forEach(({ message, locations, path }) => { console.error( `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}` ); }); } if (networkError) { console.error(`[Network error]: ${networkError}`); }});const httpLink = new HttpLink({ uri: 'https://api.example.com/graphql'});const client = new ApolloClient({ link: errorLink.concat(httpLink), cache: new InMemoryCache()});7. 订阅使用 useSubscription Hookimport { useSubscription, gql } from '@apollo/client';const MESSAGE_ADDED = gql` subscription OnMessageAdded($roomId: ID!) { messageAdded(roomId: $roomId) { id text author { name } createdAt } }`;function ChatRoom({ roomId }) { const { data, loading, error } = useSubscription(MESSAGE_ADDED, { variables: { roomId } }); if (loading) return <div>Connecting...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> <h3>New Message:</h3> <p>{data.messageAdded.text}</p> <small>By {data.messageAdded.author.name}</small> </div> );}8. 性能优化使用 persisted queriesimport { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';import { sha256 } from 'crypto-hash';const persistedQueryLink = createPersistedQueryLink({ sha256, useGETForHashedQueries: true});const client = new ApolloClient({ link: persistedQueryLink.concat(httpLink), cache: new InMemoryCache()});批量查询import { useQuery, gql } from '@apollo/client';const GET_MULTIPLE_USERS = gql` query GetMultipleUsers($ids: [ID!]!) { users(ids: $ids) { id name email } }`;function UserList({ userIds }) { const { loading, data } = useQuery(GET_MULTIPLE_USERS, { variables: { ids: userIds } }); if (loading) return <div>Loading...</div>; return ( <div> {data.users.map(user => ( <div key={user.id}>{user.name}</div> ))} </div> );}9. 客户端开发最佳实践| 实践 | 说明 ||------|------|| 使用缓存 | 减少网络请求,提高性能 || 实现乐观更新 | 提升用户体验 || 处理错误 | 提供友好的错误提示 || 使用分页 | 处理大量数据 || 实现加载状态 | 改善用户体验 || 使用订阅 | 实现实时更新 || 优化查询 | 只请求需要的字段 || 使用 persisted queries | 减少网络传输 || 实现重试机制 | 提高可靠性 || 监控性能 | 及时发现性能问题 |10. 常见问题及解决方案| 问题 | 原因 | 解决方案 ||------|------|----------|| 缓存未更新 | 缓存策略配置不当 | 调整缓存策略,手动更新缓存 || 查询慢 | 请求过多数据 | 优化查询,使用字段选择 || 内存占用高 | 缓存数据过多 | 定期清理缓存,限制缓存大小 || 订阅断开 | 网络不稳定 | 实现自动重连机制 || 错误处理不当 | 未正确处理错误 | 实现全局错误处理 |
阅读 0·2月21日 17:00

Kafka 与 RabbitMQ、RocketMQ 有什么区别?

Kafka 与其他消息队列的对比Kafka 作为分布式流处理平台,与传统消息队列(如 RabbitMQ、RocketMQ、ActiveMQ)相比,在设计理念、性能特性和应用场景上都有显著差异。理解这些差异对于技术选型和系统架构设计非常重要。Kafka vs RabbitMQ架构设计Kafka:分布式架构,支持水平扩展基于日志存储,消息持久化到磁盘采用 Pull 模式,Consumer 主动拉取消息无状态 Broker,消息存储在文件系统RabbitMQ:集中式架构,支持集群模式基于内存存储,消息存储在内存或磁盘采用 Push 模式,Broker 主动推送消息有状态 Broker,消息存储在内部数据库性能特性Kafka:高吞吐量:单机可达百万级 TPS低延迟:毫秒级延迟高并发:支持大量并发连接顺序读写:利用磁盘顺序读写优势RabbitMQ:中等吞吐量:单机万级 TPS低延迟:微秒级延迟中等并发:并发连接数有限随机读写:内存访问速度快消息可靠性Kafka:消息持久化到磁盘支持副本机制保证数据不丢失支持消息回溯消息保留时间可配置RabbitMQ:消息可持久化到磁盘支持消息确认机制支持死信队列消息默认不持久化功能特性Kafka:支持消息回溯支持消息压缩支持事务消息支持流处理(Kafka Streams)RabbitMQ:支持消息路由(Exchange、Binding)支持消息优先级支持延迟消息支持消息 TTLKafka vs RocketMQ架构设计Kafka:纯分布式架构无中心化设计基于 ZooKeeper 协调简单的存储模型RocketMQ:分布式架构支持 NameServer 协调支持主从架构复杂的存储模型性能特性Kafka:吞吐量更高延迟稍高批量处理能力强零拷贝技术优化RocketMQ:吞吐量较高延迟较低单条消息处理快事务消息性能好消息可靠性Kafka:副本机制保证可靠性支持同步和异步复制数据持久化到磁盘支持消息回溯RocketMQ:主从同步保证可靠性支持同步双写和异步复制支持消息刷盘策略支持消息重试功能特性Kafka:流处理能力强生态丰富(Kafka Connect、Kafka Streams)社区活跃文档完善RocketMQ:事务消息支持完善支持消息过滤支持定时消息支持消息轨迹Kafka vs ActiveMQ架构设计Kafka:现代分布式架构无状态设计水平扩展能力强存储与计算分离ActiveMQ:传统消息队列架构有状态设计垂直扩展为主存储与计算耦合性能特性Kafka:高吞吐量低延迟高并发顺序读写优化ActiveMQ:中等吞吐量中等延迟低并发传统数据库存储消息可靠性Kafka:副本机制持久化存储消息回溯高可用性ActiveMQ:持久化存储消息确认事务支持主从复制技术选型建议选择 Kafka 的场景大数据场景日志收集实时数据分析流式处理数据管道高吞吐量场景每秒百万级消息批量数据处理大规模数据传输消息回溯需求需要重新消费历史消息需要多订阅者消费需要消息持久化流处理场景实时计算事件驱动架构复杂事件处理选择 RabbitMQ 的场景复杂路由场景需要灵活的消息路由需要消息过滤需要多条件匹配低延迟场景微秒级延迟要求实时性要求高消息量适中企业应用场景企业级消息中间件传统的消息队列需求需要丰富的管理功能选择 RocketMQ 的场景金融场景事务消息需求高可靠性要求消息顺序要求电商场景订单处理库存同步消息轨迹追踪阿里生态使用阿里云服务需要 Spring Cloud 集成需要完善的技术支持选择 ActiveMQ 的场景传统应用JMS 规范要求传统企业应用简单消息队列需求小规模应用消息量不大部署简单维护成本低性能对比总结| 特性 | Kafka | RabbitMQ | RocketMQ | ActiveMQ ||------|-------|----------|----------|----------|| 吞吐量 | 极高 | 中等 | 高 | 低 || 延迟 | 毫秒级 | 微秒级 | 毫秒级 | 中等 || 可靠性 | 高 | 高 | 高 | 中等 || 扩展性 | 极强 | 中等 | 强 | 弱 || 复杂度 | 中等 | 高 | 高 | 中等 || 生态 | 丰富 | 丰富 | 一般 | 一般 |最佳实践根据业务场景选择大数据场景优先选择 Kafka复杂路由场景优先选择 RabbitMQ金融场景优先选择 RocketMQ考虑团队能力选择团队熟悉的技术栈考虑学习和维护成本评估技术支持能力评估长期规划考虑业务增长需求评估技术发展趋势规划技术演进路线通过对比不同消息队列的特性和适用场景,可以做出更合理的技术选型决策。
阅读 0·2月21日 17:00