GraphQL
GraphQL 是一种 API 技术,旨在描述现代 Web 应用程序复杂的嵌套数据依赖关系。它通常被认为是 SOAP 或 REST 的替代品

查看更多相关内容
GraphQL 安全有哪些最佳实践## GraphQL 安全最佳实践
GraphQL 的灵活性虽然强大,但也带来了独特的安全挑战。以下是保护 GraphQL API 的关键安全措施。
### 1. 认证与授权
#### 认证机制
```javascript
const { ApolloServer } = require('apollo-server');
const jwt = require('jsonwebtoken');
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// 从请求头获取 token
const token = req.headers.authorization || '';
try {
// 验证 token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
return { user: decoded };
} catch (error) {
return { user: null };
}
}
});
```
#### 授权中间件
```javascript
const { AuthenticationError, ForbiddenError } = require('apollo-server-express');
const resolvers = {
Query: {
me: (parent, args, context) => {
if (!context.user) {
throw new AuthenticationError('未认证');
}
return context.user;
},
users: (parent, args, context) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new ForbiddenError('需要管理员权限');
}
return User.findAll();
}
}
};
```
### 2. 查询深度限制
#### 防止深度嵌套攻击
```javascript
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7, {
ignore: ['ignoredField'] // 忽略特定字段
})
]
});
```
**为什么需要**:
- 防止恶意客户端发送深度嵌套查询
- 避免服务器资源耗尽
- 防止 DoS 攻击
### 3. 查询复杂度限制
#### 限制查询复杂度
```javascript
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const complexityLimitRule = createComplexityLimitRule(1000, {
onCost: (cost) => console.log(`查询复杂度: ${cost}`),
createError: (max, actual) => {
return new Error(`查询复杂度 ${actual} 超过最大限制 ${max}`);
}
});
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [complexityLimitRule]
});
```
#### 自定义复杂度计算
```javascript
const typeDefs = `
type Query {
user(id: ID!): User @cost(complexity: 1)
users(limit: Int!): [User!]! @cost(complexity: 5, multipliers: ["limit"])
posts(first: Int!): [Post!]! @cost(complexity: 10, multipliers: ["first"])
}
`;
```
### 4. 速率限制
#### 使用 express-rate-limit
```javascript
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 限制每个 IP 100 个请求
message: '请求过于频繁,请稍后再试'
});
app.use('/graphql', limiter);
```
#### GraphQL 特定的速率限制
```javascript
const { GraphQLComplexityLimit } = require('graphql-complexity-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
new GraphQLComplexityLimit({
maxComplexity: 1000,
onExceed: (complexity) => {
throw new Error(`查询复杂度 ${complexity} 超过限制`);
}
})
]
});
```
### 5. 输入验证
#### 使用 Yup 验证
```javascript
const yup = require('yup');
const createUserSchema = yup.object().shape({
name: yup.string().required().min(2).max(100),
email: yup.string().email().required(),
age: yup.number().min(0).max(150).optional()
});
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
// 验证输入
await createUserSchema.validate(input);
// 创建用户
return User.create(input);
}
}
};
```
#### GraphQL Schema 验证
```graphql
input CreateUserInput {
name: String! @constraint(minLength: 2, maxLength: 100)
email: String! @constraint(format: "email")
age: Int @constraint(min: 0, max: 150)
}
```
### 6. 字段级权限控制
#### 使用指令
```graphql
directive @auth(requires: Role) on FIELD_DEFINITION
enum Role {
USER
ADMIN
}
type User {
id: ID!
name: String!
email: String! @auth(requires: ADMIN)
salary: Float @auth(requires: ADMIN)
}
```
```javascript
const resolvers = {
User: {
email: (user, args, context) => {
if (context.user?.role !== 'ADMIN') {
throw new ForbiddenError('无权访问该字段');
}
return user.email;
},
salary: (user, args, context) => {
if (context.user?.role !== 'ADMIN') {
throw new ForbiddenError('无权访问该字段');
}
return user.salary;
}
}
};
```
### 7. 防止查询注入
#### 参数化查询
```javascript
// 不好的做法 - 字符串拼接
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// 好的做法 - 参数化查询
const query = 'SELECT * FROM users WHERE id = ?';
const result = await db.query(query, [userId]);
```
#### 使用 ORM
```javascript
// 使用 Sequelize ORM
const user = await User.findOne({
where: { id: userId }
});
```
### 8. CORS 配置
```javascript
const server = new ApolloServer({
typeDefs,
resolvers,
cors: {
origin: ['https://yourdomain.com'], // 只允许特定域名
credentials: true,
methods: ['POST']
}
});
```
### 9. 错误处理
#### 不暴露敏感信息
```javascript
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (error) => {
// 生产环境不暴露详细错误信息
if (process.env.NODE_ENV === 'production') {
return new Error('服务器内部错误');
}
// 开发环境返回完整错误信息
return error;
}
});
```
#### 自定义错误类型
```graphql
type Error {
code: String!
message: String!
field: String
}
type UserResult {
user: User
errors: [Error!]!
}
type Mutation {
createUser(input: CreateUserInput!): UserResult!
}
```
### 10. 日志与监控
#### 记录查询日志
```javascript
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart: () => ({
didResolveOperation: (context) => {
console.log('查询:', context.request.operationName);
console.log('变量:', JSON.stringify(context.request.variables));
}
})
}
]
});
```
#### 监控异常查询
```javascript
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart: () => ({
didEncounterErrors: (context) => {
context.errors.forEach(error => {
console.error('查询错误:', error.message);
// 发送到监控系统
monitoringService.logError(error);
});
}
})
}
]
});
```
### 11. 查询白名单
#### 使用 persisted queries
```javascript
const { createPersistedQueryLink } = require('@apollo/client/link/persisted-queries');
const { sha256 } = require('crypto-hash');
const link = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true
});
// 只允许预定义的查询
const allowedQueries = new Set([
'hash1',
'hash2'
});
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: {
cache: new Map(),
ttl: 3600
}
});
```
### 12. 安全检查清单
- [ ] 实施认证机制(JWT、OAuth)
- [ ] 实施授权机制(基于角色的访问控制)
- [ ] 限制查询深度
- [ ] 限制查询复杂度
- [ ] 实施速率限制
- [ ] 验证所有输入
- [ ] 实施字段级权限控制
- [ ] 防止查询注入
- [ ] 配置 CORS
- [ ] 正确处理错误
- [ ] 记录查询日志
- [ ] 监控异常查询
- [ ] 使用查询白名单
- [ ] 定期安全审计
- [ ] 保持依赖更新
### 13. 常见安全威胁及防护
| 威胁 | 描述 | 防护措施 |
|------|------|----------|
| 深度嵌套攻击 | 恶意客户端发送深度嵌套查询 | 限制查询深度 |
| 复杂度攻击 | 发送高复杂度查询消耗资源 | 限制查询复杂度 |
| DoS 攻击 | 大量请求导致服务不可用 | 速率限制、查询白名单 |
| 注入攻击 | 恶意输入导致 SQL 注入 | 参数化查询、输入验证 |
| 未授权访问 | 访问未授权的数据 | 认证、授权、字段级权限控制 |
| 信息泄露 | 错误信息暴露敏感数据 | 正确的错误处理 |
服务端 · 2月21日 17:01
GraphQL 与 REST API 的核心区别是什么## GraphQL 与 REST API 的核心区别
GraphQL 是一种用于 API 的查询语言和运行时环境,它与 REST API 有以下核心区别:
### 1. 数据获取方式
- **REST**: 客户端需要访问多个端点来获取相关数据,可能导致过度获取或获取不足
- **GraphQL**: 客户端可以在单个请求中精确指定所需的数据字段,避免过度获取和获取不足
### 2. 端点设计
- **REST**: 基于资源的端点设计,每个资源有独立的 URL
- **GraphQL**: 单一端点,所有请求都发送到同一个 URL
### 3. 版本控制
- **REST**: 通常需要版本控制(如 /api/v1/users)
- **GraphQL**: 通过 Schema 演进,无需版本控制,可以废弃字段而不破坏现有客户端
### 4. 请求方法
- **REST**: 使用标准 HTTP 方法(GET、POST、PUT、DELETE)
- **GraphQL**: 使用 POST 方法发送查询、变更和订阅
### 5. 响应格式
- **REST**: 响应格式由服务器决定
- **GraphQL**: 响应格式由客户端查询决定
### 6. 缓存策略
- **REST**: 可以利用 HTTP 缓存机制
- **GraphQL**: 需要实现自定义缓存策略(如 Apollo Client 缓存)
### 7. 实时数据
- **REST**: 通常需要轮询或使用 WebSocket
- **GraphQL**: 原生支持订阅(Subscriptions)实现实时数据更新
### 8. 错误处理
- **REST**: 使用 HTTP 状态码表示错误
- **GraphQL**: 在响应体中包含 errors 字段,HTTP 状态码通常为 200
### 9. 类型系统
- **REST**: 通常使用 OpenAPI/Swagger 进行文档化,但不是强类型
- **GraphQL**: 内置强类型系统,Schema 定义了所有可用的类型和操作
### 10. 文档
- **REST**: 需要手动维护 API 文档
- **GraphQL**: Schema 本身就是文档,可以通过工具自动生成
## 适用场景
**GraphQL 适合**:
- 需要从多个数据源获取复杂嵌套数据的应用
- 移动应用,需要减少网络请求和数据传输
- 需要灵活数据需求的多个客户端
- 快速迭代的产品,需要频繁调整 API
**REST 适合**:
- 简单的 CRUD 操作
- 需要利用 HTTP 缓存的场景
- 公共 API,需要简单易用
- 团队对 GraphQL 不熟悉的场景
服务端 · 2月21日 17:01
GraphQL Schema 设计有哪些最佳实践## GraphQL Schema 设计最佳实践
GraphQL Schema 是 API 的核心,良好的 Schema 设计能够提高开发效率、减少维护成本,并提供更好的开发者体验。
### 1. Schema 基础结构
#### 类型定义
```graphql
type User {
id: ID!
name: String!
email: String!
age: Int
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
createdAt: DateTime!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
createdAt: DateTime!
}
```
#### 输入类型
```graphql
input CreateUserInput {
name: String!
email: String!
age: Int
}
input CreatePostInput {
title: String!
content: String!
authorId: ID!
}
```
#### 枚举类型
```graphql
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
```
#### 联合类型
```graphql
union SearchResult = User | Post | Comment
```
#### 接口类型
```graphql
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
email: String!
}
type Post implements Node {
id: ID!
title: String!
content: String!
}
```
### 2. 命名规范
#### 类型命名
- 使用 PascalCase(首字母大写)
- 使用描述性名称
- 避免缩写
- 使用单数形式
**示例**:
```graphql
# 好的命名
type UserProfile { }
type Article { }
type ShoppingCart { }
# 不好的命名
type user { }
type Art { }
type ShopCart { }
```
#### 字段命名
- 使用 camelCase(首字母小写)
- 使用动词或名词
- 避免使用保留字
**示例**:
```graphql
# 好的命名
type User {
firstName: String!
lastName: String!
fullName: String!
isActive: Boolean!
}
# 不好的命名
type User {
first_name: String!
LastName: String!
Full_Name: String!
active: Boolean!
}
```
#### 参数命名
- 使用 camelCase
- 描述性名称
- 包含单位或类型信息
**示例**:
```graphql
# 好的命名
query GetUsers($limit: Int, $offset: Int) { }
query GetPosts($after: String, $first: Int) { }
# 不好的命名
query GetUsers($l: Int, $o: Int) { }
query GetPosts($a: String, $f: Int) { }
```
### 3. 类型设计原则
#### 避免过度嵌套
```graphql
# 不好的设计 - 过度嵌套
type User {
posts {
comments {
author {
posts {
comments {
# 无限嵌套
}
}
}
}
}
}
# 好的设计 - 合理嵌套
type User {
posts(limit: Int): [Post!]!
}
type Post {
comments(limit: Int): [Comment!]!
}
```
#### 使用输入类型封装参数
```graphql
# 不好的设计 - 参数过多
mutation CreateUser($name: String!, $email: String!, $age: Int, $address: String, $phone: String) { }
# 好的设计 - 使用输入类型
input CreateUserInput {
name: String!
email: String!
age: Int
address: String
phone: String
}
mutation CreateUser($input: CreateUserInput!) { }
```
#### 实现分页
```graphql
# 基于游标的分页(推荐)
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): PostConnection!
}
# 基于偏移的分页
type PostResult {
posts: [Post!]!
total: Int!
page: Int!
pageSize: Int!
}
type Query {
posts(offset: Int, limit: Int): PostResult!
}
```
### 4. 错误处理
#### 自定义错误类型
```graphql
type Error {
code: String!
message: String!
field: String
}
type UserResult {
user: User
errors: [Error!]!
}
type Mutation {
createUser(input: CreateUserInput!): UserResult!
}
```
#### 使用可空字段
```graphql
type Mutation {
# 返回可空类型表示可能失败
createUser(input: CreateUserInput!): User
}
```
### 5. 版本控制与演进
#### 废弃字段
```graphql
type User {
id: ID!
name: String!
# 废弃字段,提供替代方案
fullName: String @deprecated(reason: "Use 'name' instead")
email: String!
}
```
#### 添加新字段
```graphql
type User {
id: ID!
name: String!
email: String!
# 新增字段,不影响现有客户端
phoneNumber: String
}
```
### 6. 性能优化
#### 使用 DataLoader
```javascript
const userLoader = new DataLoader(async (userIds) => {
const users = await User.findAll({ where: { id: userIds } });
return userIds.map(id => users.find(user => user.id === id));
});
```
#### 字段级权限控制
```graphql
type User {
id: ID!
name: String!
# 敏感字段需要权限
email: String! @auth(requires: ADMIN)
salary: Float @auth(requires: ADMIN)
}
```
### 7. 文档与描述
#### 添加描述
```graphql
"""
用户类型,包含用户的基本信息
"""
type User {
"""
用户唯一标识符
"""
id: ID!
"""
用户姓名,最大长度 100 字符
"""
name: String!
"""
用户邮箱地址,必须是有效的邮箱格式
"""
email: String!
}
```
### 8. 最佳实践总结
1. **保持简洁**: 避免过度设计,保持 Schema 简洁明了
2. **一致性**: 在整个项目中保持命名和结构的一致性
3. **可扩展性**: 设计时考虑未来的扩展需求
4. **向后兼容**: 添加新功能时保持向后兼容
5. **文档化**: 为所有类型、字段和参数添加描述
6. **性能考虑**: 避免过度嵌套,合理使用分页
7. **安全性**: 实现适当的权限控制和验证
8. **测试**: 为 Resolver 编写单元测试和集成测试
服务端 · 2月21日 17:01
GraphQL 有哪些高级概念和架构设计模式## GraphQL 高级概念与架构设计
GraphQL 不仅仅是查询语言,它还包含许多高级概念和架构设计模式,掌握这些对于构建大规模 GraphQL 应用至关重要。
### 1. 联合类型(Union Types)
#### 定义联合类型
```graphql
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
```
#### 实现联合类型 Resolver
```javascript
const resolvers = {
SearchResult: {
__resolveType: (obj) => {
if (obj.email) {
return 'User';
}
if (obj.title) {
return 'Post';
}
if (obj.text) {
return 'Comment';
}
return null;
}
},
Query: {
search: async (_, { query }) => {
const users = await User.search(query);
const posts = await Post.search(query);
const comments = await Comment.search(query);
return [...users, ...posts, ...comments];
}
}
};
```
#### 使用联合类型
```graphql
query SearchResults($query: String!) {
search(query: $query) {
... on User {
id
name
email
}
... on Post {
id
title
author {
name
}
}
... on Comment {
id
text
author {
name
}
}
}
}
```
### 2. 接口类型(Interface Types)
#### 定义接口
```graphql
interface Node {
id: ID!
createdAt: DateTime!
}
type User implements Node {
id: ID!
createdAt: DateTime!
name: String!
email: String!
}
type Post implements Node {
id: ID!
createdAt: DateTime!
title: String!
content: String!
}
type Query {
node(id: ID!): Node
}
```
#### 实现接口 Resolver
```javascript
const resolvers = {
Node: {
__resolveType: (obj) => {
if (obj.email) {
return 'User';
}
if (obj.title) {
return 'Post';
}
return null;
}
},
Query: {
node: async (_, { id }) => {
// 尝试从不同数据源获取
const user = await User.findById(id);
if (user) return user;
const post = await Post.findById(id);
if (post) return post;
throw new Error('Node not found');
}
}
};
```
### 3. 自定义指令(Custom Directives)
#### 创建自定义指令
```graphql
directive @auth(requires: Role) on FIELD_DEFINITION
directive @cache(ttl: Int) on FIELD_DEFINITION
directive @transform(type: String) on FIELD_DEFINITION
enum Role {
USER
ADMIN
}
```
#### 实现指令
```javascript
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { defaultFieldResolver } = require('graphql');
const authDirective = (schema) => {
return makeExecutableSchema({
typeDefs: schema,
resolvers: {
Query: {
// ... existing resolvers
}
},
directiveResolvers: {
auth: (next, source, args, context) => {
const { requires } = args;
if (!context.user || context.user.role !== requires) {
throw new Error('Unauthorized');
}
return next();
},
cache: async (next, source, args, context) => {
const { ttl } = args;
const cacheKey = `cache:${JSON.stringify(source)}`;
// 尝试从缓存获取
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 执行 resolver
const result = await next();
// 缓存结果
await redis.setex(cacheKey, ttl, JSON.stringify(result));
return result;
}
}
});
};
```
### 4. 订阅(Subscriptions)
#### 定义订阅
```graphql
type Subscription {
postCreated: Post!
postUpdated(postId: ID!): Post!
commentAdded(postId: ID!): Comment!
}
```
#### 实现订阅
```javascript
const { PubSub } = require('graphql-subscriptions');
const RedisPubSub = require('graphql-redis-subscriptions').RedisPubSub;
const pubsub = new RedisPubSub({
connection: {
host: 'localhost',
port: 6379
}
});
const POST_CREATED = 'POST_CREATED';
const POST_UPDATED = 'POST_UPDATED';
const COMMENT_ADDED = 'COMMENT_ADDED';
const resolvers = {
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator([POST_CREATED])
},
postUpdated: {
subscribe: (_, { postId }) => {
const filteredIterator = pubsub.asyncIterator([POST_UPDATED]);
return {
[Symbol.asyncIterator]() {
return (async function* () {
for await (const event of filteredIterator) {
if (event.postUpdated.id === postId) {
yield event;
}
}
})();
}
};
}
},
commentAdded: {
subscribe: (_, { postId }) => {
const filteredIterator = pubsub.asyncIterator([COMMENT_ADDED]);
return {
[Symbol.asyncIterator]() {
return (async function* () {
for await (const event of filteredIterator) {
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;
}
}
};
```
### 5. 数据加载器模式(DataLoader Pattern)
#### 批量加载
```javascript
const DataLoader = require('dataloader');
class DataLoaderContext {
constructor() {
this.userLoader = new DataLoader(this.batchGetUsers);
this.postLoader = new DataLoader(this.batchGetPosts);
}
async batchGetUsers(userIds) {
const users = await User.findAll({
where: { id: userIds }
});
return userIds.map(id => users.find(user => user.id === id));
}
async batchGetPosts(postIds) {
const posts = await Post.findAll({
where: { id: postIds }
});
return postIds.map(id => posts.find(post => post.id === id));
}
}
```
#### 在 Resolver 中使用
```javascript
const resolvers = {
Post: {
author: (post, _, context) => {
return context.dataLoaders.userLoader.load(post.authorId);
},
comments: (post, _, context) => {
return context.dataLoaders.commentLoader.load(post.id);
}
},
Query: {
posts: async (_, __, context) => {
const posts = await Post.findAll();
return posts;
}
}
};
```
### 6. Schema 拆分与组合
#### Schema 拆分
```graphql
# user.graphql
type User {
id: ID!
name: String!
email: String!
}
extend type Query {
user(id: ID!): User
users: [User!]!
}
extend type Mutation {
createUser(input: CreateUserInput!): User
updateUser(id: ID!, input: UpdateUserInput!): User
deleteUser(id: ID!): Boolean
}
```
```graphql
# post.graphql
type Post {
id: ID!
title: String!
content: String!
author: User!
}
extend type Query {
post(id: ID!): Post
posts: [Post!]!
}
extend type Mutation {
createPost(input: CreatePostInput!): Post
updatePost(id: ID!, input: UpdatePostInput!): Post
deletePost(id: ID!): Boolean
}
```
#### Schema 组合
```javascript
const { mergeTypeDefs } = require('@graphql-tools/merge');
const { loadFilesSync } = require('@graphql-tools/load-files');
const typeDefsArray = loadFilesSync(path.join(__dirname, './schemas'));
const typeDefs = mergeTypeDefs(typeDefsArray);
```
### 7. 微服务架构中的 GraphQL
#### 网关模式
```javascript
const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('@apollo/gateway');
const { readFileSync } = require('fs');
const gateway = new ApolloGateway({
supergraphSdl: readFileSync('./supergraph.graphql').toString(),
serviceList: [
{ name: 'users', url: 'http://localhost:4001/graphql' },
{ name: 'posts', url: 'http://localhost:4002/graphql' },
{ name: 'comments', url: 'http://localhost:4003/graphql' }
]
});
const server = new ApolloServer({
gateway,
subscriptions: false
});
server.listen().then(({ url }) => {
console.log(`Gateway ready at ${url}`);
});
```
#### 联邦模式
```graphql
# users service
extend type Post @key(fields: "id") {
id: ID! @external
author: User
}
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
```
```graphql
# posts service
type Post @key(fields: "id") {
id: ID!
title: String!
content: String!
authorId: ID!
author: User
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}
```
### 8. 错误处理与重试
#### 自定义错误类
```javascript
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 AuthenticationError extends GraphQLError {
constructor(message = 'Authentication required') {
super(message, 'AUTHENTICATION_ERROR');
}
}
class AuthorizationError extends GraphQLError {
constructor(message = 'Not authorized') {
super(message, 'AUTHORIZATION_ERROR');
}
}
```
#### 错误处理中间件
```javascript
const formatError = (error) => {
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;
};
```
### 9. 测试策略
#### Resolver 单元测试
```javascript
const { resolvers } = require('./resolvers');
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 resolvers.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(
resolvers.Query.user(null, { id: '1' })
).rejects.toThrow('User not found');
});
});
});
```
#### 集成测试
```javascript
const { ApolloServer } = require('apollo-server');
const { createTestClient } = require('apollo-server-testing');
const { typeDefs, resolvers } = require('./schema');
describe('GraphQL API', () => {
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');
});
});
});
```
### 10. 高级架构模式
#### CQRS 模式
```javascript
const resolvers = {
Query: {
// 读操作 - 使用优化的查询
user: async (_, { id }, { readDb }) => {
return readDb.User.findById(id);
}
},
Mutation: {
// 写操作 - 使用事件溯源
createUser: async (_, { input }, { writeDb, eventBus }) => {
const user = await writeDb.User.create(input);
await eventBus.publish('USER_CREATED', { user });
return user;
}
}
};
```
#### 事件溯源模式
```javascript
class EventStore {
async saveEvent(aggregateId, eventType, payload) {
await Event.create({
aggregateId,
eventType,
payload: JSON.stringify(payload),
timestamp: new Date()
});
}
async getEvents(aggregateId) {
const events = await Event.findAll({
where: { aggregateId },
order: [['timestamp', 'ASC']]
});
return events.map(event => ({
...event,
payload: JSON.parse(event.payload)
}));
}
}
```
### 11. 高级概念总结
| 概念 | 用途 | 优势 |
|------|------|------|
| 联合类型 | 处理多种类型的返回结果 | 灵活的数据结构 |
| 接口类型 | 定义共享字段 | 代码复用、类型安全 |
| 自定义指令 | 声明式功能 | 可重用、可组合 |
| 订阅 | 实时数据推送 | 实时性、低延迟 |
| DataLoader | 批量加载 | 性能优化、减少查询 |
| Schema 组合 | 模块化设计 | 可维护性、团队协作 |
| 微服务 | 分布式架构 | 可扩展性、独立部署 |
| 错误处理 | 统一错误管理 | 一致性、可调试性 |
| 测试策略 | 质量保证 | 可靠性、可维护性 |
| 架构模式 | 解决复杂问题 | 可扩展性、灵活性 |
服务端 · 2月21日 17:00
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
```graphql
# types/user.graphql
type User {
id: ID!
name: String!
email: String!
createdAt: DateTime!
updatedAt: DateTime!
}
input CreateUserInput {
name: String!
email: String!
}
input UpdateUserInput {
name: String
email: String
}
```
```graphql
# queries/index.graphql
extend type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
}
```
```graphql
# mutations/index.graphql
extend type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
```
### 2. 代码规范
#### 命名约定
```typescript
// 类型定义使用 PascalCase
type User {
id: ID!
name: String!
}
// 字段使用 camelCase
type User {
firstName: String!
lastName: String!
}
// 输入类型以 Input 结尾
input CreateUserInput {
name: String!
email: String!
}
// 枚举使用 PascalCase
enum UserRole {
ADMIN
USER
GUEST
}
```
#### Resolver 命名
```typescript
// Resolver 文件命名: *.resolver.ts
// user.resolver.ts
export const userResolvers = {
Query: {
user: () => {},
users: () => {}
},
Mutation: {
createUser: () => {},
updateUser: () => {},
deleteUser: () => {}
},
User: {
posts: () => {},
comments: () => {}
}
};
```
### 3. 错误处理
#### 统一错误格式
```typescript
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');
}
}
```
#### 错误处理中间件
```typescript
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. 日志记录
#### 结构化日志
```typescript
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. 测试策略
#### 单元测试
```typescript
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');
});
});
});
```
#### 集成测试
```typescript
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
```typescript
// codegen.yml
schema: ./src/graphql/schema/**/*.graphql
documents: ./src/graphql/documents/**/*.graphql
generates:
./src/generated/graphql.ts:
plugins:
- typescript
- typescript-resolvers
config:
contextType: ./context#Context
scalars:
DateTime: Date
```
#### 自动生成文档
```typescript
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 Studio
```typescript
import { ApolloServerPluginUsageReporting } from 'apollo-server-core';
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginUsageReporting({
apiKey: process.env.APOLLO_KEY,
graphRef: 'my-graph@current'
})
]
});
```
#### 自定义监控
```typescript
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 化
```dockerfile
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 4000
CMD ["npm", "start"]
```
```yaml
# docker-compose.yml
version: '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:7
```
#### CI/CD 流程
```yaml
# .github/workflows/ci.yml
name: CI
on: [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 }}:latest
```
### 9. 版本控制
#### Schema 演进策略
```graphql
# 添加新字段 - 向后兼容
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 评审流程
1. **设计阶段**: 团队讨论 Schema 设计
2. **文档阶段**: 编写详细的 Schema 文档
3. **评审阶段**: 团队成员评审 Schema
4. **实现阶段**: 实现 Resolver 和业务逻辑
5. **测试阶段**: 编写测试用例
6. **部署阶段**: 部署到测试环境
7. **监控阶段**: 监控 API 性能和错误
#### 代码审查清单
- [ ] Schema 设计合理
- [ ] 命名符合规范
- [ ] 错误处理完善
- [ ] 性能优化到位
- [ ] 安全措施完备
- [ ] 测试覆盖充分
- [ ] 文档完整准确
- [ ] 日志记录合理
### 11. 最佳实践总结
| 方面 | 最佳实践 |
|------|----------|
| 项目结构 | 模块化、分层清晰 |
| 代码规范 | 统一命名约定、代码风格 |
| 错误处理 | 统一错误格式、详细错误信息 |
| 日志记录 | 结构化日志、关键操作记录 |
| 测试策略 | 单元测试、集成测试、E2E 测试 |
| 文档生成 | 自动生成、保持更新 |
| 性能监控 | 实时监控、性能分析 |
| 部署策略 | 容器化、CI/CD 自动化 |
| 版本控制 | 向后兼容、渐进式演进 |
| 团队协作 | 代码审查、知识共享 |
服务端 · 2月21日 17:00
GraphQL 错误处理有哪些最佳实践## GraphQL 错误处理最佳实践
GraphQL 提供了灵活的错误处理机制,但正确实现错误处理对于构建健壮的 API 至关重要。以下是 GraphQL 错误处理的关键策略和最佳实践。
### 1. GraphQL 错误结构
#### 基本错误响应格式
```json
{
"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. 自定义错误类
#### 创建错误类层次结构
```javascript
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 中抛出错误
#### 基本错误抛出
```javascript
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);
}
}
};
```
#### 部分错误处理
```javascript
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. 错误格式化
#### 自定义错误格式化器
```javascript
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. 错误日志记录
#### 结构化错误日志
```javascript
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. 错误恢复策略
#### 降级处理
```javascript
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
};
}
}
}
};
```
#### 重试机制
```javascript
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. 错误类型设计
#### 错误结果类型
```graphql
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!
}
```
#### 实现
```javascript
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. 错误监控和告警
#### 错误监控集成
```javascript
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
}
});
});
}
})
}
]
});
```
#### 错误告警
```javascript
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 | 使用缓存或降级 |
服务端 · 2月21日 17:00
GraphQL 订阅(Subscriptions)如何实现## GraphQL 订阅(Subscriptions)实现详解
GraphQL 订阅允许客户端实时接收服务器推送的数据更新,是构建实时应用的关键功能。以下是 GraphQL 订阅的详细实现方案。
### 1. 订阅基础概念
#### 订阅的工作原理
1. 客户端通过 WebSocket 建立持久连接
2. 客户端发送订阅查询
3. 服务器保持连接并监听事件
4. 当事件发生时,服务器推送数据到客户端
5. 客户端接收并处理更新
#### 订阅 vs 轮询
| 特性 | 订阅 | 轮询 |
|------|------|------|
| 实时性 | 高 | 低 |
| 服务器负载 | 低(事件驱动) | 高(持续查询) |
| 网络开销 | 低(按需推送) | 高(定期请求) |
| 实现复杂度 | 高 | 低 |
| 适用场景 | 实时更新 | 定期检查 |
### 2. 服务器端实现
#### 使用 graphql-subscriptions
```javascript
const { 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 PubSub
```javascript
const { 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 Server
```javascript
const { 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 订阅
```javascript
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 订阅
```javascript
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. 订阅过滤
#### 基于参数的过滤
```javascript
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;
}
})();
}
};
}
}
}
};
```
#### 基于权限的过滤
```javascript
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. 订阅错误处理
#### 连接错误处理
```javascript
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);
// 清理资源
}
}
}
});
```
#### 订阅错误处理
```javascript
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. 订阅性能优化
#### 批量发布
```javascript
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: {} });
```
#### 订阅节流
```javascript
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. 订阅监控
#### 连接监控
```javascript
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
);
```
#### 订阅指标
```javascript
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. 常见问题及解决方案
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| 连接频繁断开 | 网络不稳定、超时 | 实现自动重连、增加超时时间 |
| 订阅延迟高 | 服务器负载高、处理慢 | 优化性能、使用批量发布 |
| 内存泄漏 | 未正确清理订阅 | 确保取消订阅、清理资源 |
| 数据不一致 | 缓存未更新 | 实现缓存失效机制 |
| 安全问题 | 未验证连接 | 实现连接认证和授权 |
服务端 · 2月21日 17:00
GraphQL 缓存策略有哪些实现方式## GraphQL 缓存策略与实现
GraphQL 的缓存机制对于提高性能、减少服务器负载和改善用户体验至关重要。以下是 GraphQL 缓存的各种策略和实现方法。
### 1. 客户端缓存
#### Apollo Client 缓存
```javascript
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']
}
}
})
});
```
#### 缓存策略配置
```javascript
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 缓存实现
```javascript
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 缓存
```javascript
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. 缓存失效策略
#### 基于时间的失效
```javascript
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;
}
```
#### 基于事件的失效
```javascript
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. 缓存预热
#### 预加载热门数据
```javascript
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. 缓存穿透保护
#### 布隆过滤器
```javascript
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. 缓存雪崩保护
#### 随机过期时间
```javascript
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;
}
```
#### 互斥锁
```javascript
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 queries
```javascript
import { 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()
});
```
#### 配置 CDN
```javascript
const server = new ApolloServer({
typeDefs,
resolvers,
cacheControl: {
defaultMaxAge: 60,
stripFormattedExtensions: false,
calculateHttpHeaders: true
},
plugins: [
require('apollo-cache-control')({
defaultMaxAge: 60
})
]
});
```
### 8. 缓存监控
#### 缓存命中率监控
```javascript
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 加速静态数据
- [ ] 定期清理无效缓存
- [ ] 实现缓存降级机制
- [ ] 记录缓存操作日志
服务端 · 2月21日 17:00
GraphQL 测试有哪些策略和最佳实践## GraphQL 测试策略与最佳实践
GraphQL API 的测试对于确保代码质量和系统稳定性至关重要。以下是 GraphQL 测试的全面策略和最佳实践。
### 1. 测试类型
#### 单元测试
测试单个 Resolver 函数的逻辑。
```javascript
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);
});
});
});
```
#### 集成测试
测试多个组件协同工作。
```javascript
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 测试
测试完整的用户流程。
```javascript
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
```javascript
// jest.config.js
module.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 + Chai
```javascript
const { 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 数据库
```javascript
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 生成测试数据
```javascript
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
#### 测试认证 Context
```javascript
describe('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');
});
});
```
#### 测试数据源 Context
```javascript
describe('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. 测试错误处理
#### 测试验证错误
```javascript
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 错误
```javascript
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. 测试订阅
#### 测试订阅 Resolver
```javascript
describe('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);
});
});
```
#### 测试订阅过滤
```javascript
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. 性能测试
#### 测试查询性能
```javascript
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 查询
```javascript
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. 测试覆盖率
#### 配置覆盖率
```javascript
// package.json
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch"
}
}
```
#### 生成覆盖率报告
```bash
npm run test:coverage
```
### 9. 测试最佳实践
| 实践 | 说明 |
|------|------|
| 测试所有 Resolvers | 确保每个 Resolver 都有测试 |
| 使用 Mock 数据 | 隔离测试,避免依赖外部服务 |
| 测试错误场景 | 验证错误处理逻辑 |
| 测试边界条件 | 测试空值、极大值等 |
| 保持测试独立 | 每个测试应该独立运行 |
| 使用描述性名称 | 测试名称应该清楚描述测试内容 |
| 测试 Context | 验证认证、授权等 Context 功能 |
| 测试订阅 | 确保订阅功能正常工作 |
| 性能测试 | 确保查询性能符合预期 |
| 监控覆盖率 | 保持高测试覆盖率 |
### 10. 常见测试问题及解决方案
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| 测试不稳定 | 异步操作未正确处理 | 使用 async/await、适当的等待 |
| Mock 失败 | Mock 配置不正确 | 检查 Mock 的配置和调用 |
| 测试慢 | 测试数据量过大 | 使用较小的测试数据集 |
| 覆盖率低 | 未测试所有代码路径 | 增加测试用例,覆盖所有分支 |
| 测试难以维护 | 测试代码复杂 | 重构测试代码,使用测试工具 |
服务端 · 2月21日 17:00
GraphQL 查询、变更和订阅有什么区别## GraphQL 查询(Query)、变更(Mutation)和订阅(Subscription)的区别
GraphQL 提供了三种主要的操作类型,每种类型都有特定的用途和语义。
### 1. 查询(Query)
**定义**: 用于获取数据,类似于 REST 中的 GET 请求。
**特点**:
- 只读操作,不会修改服务器上的数据
- 可以并行执行多个查询
- 可以嵌套查询以获取相关数据
- 可以使用参数来过滤或定制结果
**示例**:
```graphql
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
posts {
id
title
createdAt
}
}
}
```
**使用场景**:
- 获取用户信息
- 列表查询
- 数据详情展示
- 报表生成
### 2. 变更(Mutation)
**定义**: 用于修改服务器上的数据,类似于 REST 中的 POST、PUT、DELETE 请求。
**特点**:
- 会修改服务器上的数据
- 按顺序执行(串行),确保数据一致性
- 通常返回修改后的数据或操作结果
- 可以包含输入对象和参数
**示例**:
```graphql
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)
- 服务器主动推送数据到客户端
- 适用于实时性要求高的场景
- 可以包含过滤条件
**示例**:
```graphql
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**: 使用消息队列处理高并发订阅
服务端 · 2月21日 17:00