Skip to content

Node.js GraphQL 开发指南

更新: 8/8/2025 字数: 0 字 时长: 0 分钟

本章将介绍如何在 Node.js 中使用 GraphQL 构建现代化的 API,包括 Schema 设计、Resolver 实现、查询优化、安全性等核心概念。

GraphQL 基础

1. GraphQL 概述

GraphQL 是一种用于 API 的查询语言和运行时,它提供了一种更高效、强大和灵活的替代 REST 的方案。

优势:

  • 精确获取所需数据
  • 强类型系统
  • 单一端点
  • 实时订阅
  • 自文档化

核心概念:

  • Schema:定义 API 的结构
  • Query:读取数据
  • Mutation:修改数据
  • Subscription:实时数据
  • Resolver:数据获取逻辑

2. 环境搭建

bash
# 安装依赖
npm install apollo-server-express graphql
npm install @graphql-tools/schema @graphql-tools/load-files
npm install graphql-subscriptions
npm install dataloader
npm install graphql-depth-limit graphql-query-complexity
javascript
// server.js - 基础服务器设置
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { loadFilesSync } = require('@graphql-tools/load-files');
const path = require('path');

// 加载类型定义和解析器
const typeDefs = loadFilesSync(path.join(__dirname, './schema/**/*.graphql'));
const resolvers = loadFilesSync(path.join(__dirname, './resolvers/**/*.js'));

// 创建可执行的 Schema
const schema = makeExecutableSchema({
  typeDefs,
  resolvers
});

// 创建 Apollo Server
const server = new ApolloServer({
  schema,
  context: ({ req, connection }) => {
    // HTTP 请求上下文
    if (req) {
      return {
        user: req.user,
        dataSources: {
          userAPI: new UserAPI(),
          postAPI: new PostAPI()
        }
      };
    }
    
    // WebSocket 连接上下文(用于订阅)
    if (connection) {
      return connection.context;
    }
  },
  subscriptions: {
    path: '/graphql',
    onConnect: (connectionParams, webSocket) => {
      console.log('Client connected for subscriptions');
      return {
        user: connectionParams.user
      };
    },
    onDisconnect: () => {
      console.log('Client disconnected from subscriptions');
    }
  },
  playground: process.env.NODE_ENV === 'development',
  introspection: true
});

// 创建 Express 应用
const app = express();

// 应用 Apollo GraphQL 中间件
server.applyMiddleware({ app, path: '/graphql' });

// 启动服务器
const PORT = process.env.PORT || 4000;

const httpServer = app.listen(PORT, () => {
  console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`);
  console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}${server.subscriptionsPath}`);
});

// 安装订阅处理器
server.installSubscriptionHandlers(httpServer);

module.exports = { app, server };

Schema 设计

1. 类型定义

graphql
# schema/user.graphql
type User {
  id: ID!
  username: String!
  email: String!
  profile: UserProfile
  posts: [Post!]!
  followers: [User!]!
  following: [User!]!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type UserProfile {
  firstName: String
  lastName: String
  bio: String
  avatar: String
  website: String
  location: String
}

input CreateUserInput {
  username: String!
  email: String!
  password: String!
  profile: UserProfileInput
}

input UpdateUserInput {
  username: String
  email: String
  profile: UserProfileInput
}

input UserProfileInput {
  firstName: String
  lastName: String
  bio: String
  avatar: String
  website: String
  location: String
}

type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  node: User!
  cursor: String!
}
graphql
# schema/post.graphql
type Post {
  id: ID!
  title: String!
  content: String!
  excerpt: String
  author: User!
  tags: [Tag!]!
  comments: [Comment!]!
  likes: [Like!]!
  likesCount: Int!
  commentsCount: Int!
  published: Boolean!
  publishedAt: DateTime
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Comment {
  id: ID!
  content: String!
  author: User!
  post: Post!
  parent: Comment
  replies: [Comment!]!
  likes: [Like!]!
  likesCount: Int!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Like {
  id: ID!
  user: User!
  post: Post
  comment: Comment
  createdAt: DateTime!
}

type Tag {
  id: ID!
  name: String!
  slug: String!
  posts: [Post!]!
  postsCount: Int!
}

input CreatePostInput {
  title: String!
  content: String!
  excerpt: String
  tags: [String!]
  published: Boolean = false
}

input UpdatePostInput {
  title: String
  content: String
  excerpt: String
  tags: [String!]
  published: Boolean
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type PostEdge {
  node: Post!
  cursor: String!
}
graphql
# schema/common.graphql
scalar DateTime
scalar JSON

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

enum SortOrder {
  ASC
  DESC
}

input PaginationInput {
  first: Int
  after: String
  last: Int
  before: String
}

input SortInput {
  field: String!
  order: SortOrder = ASC
}

type Query {
  # 用户查询
  user(id: ID!): User
  users(
    pagination: PaginationInput
    sort: SortInput
    search: String
  ): UserConnection!
  me: User
  
  # 文章查询
  post(id: ID!): Post
  posts(
    pagination: PaginationInput
    sort: SortInput
    authorId: ID
    tag: String
    published: Boolean
    search: String
  ): PostConnection!
  
  # 标签查询
  tags: [Tag!]!
  tag(slug: String!): Tag
}

type Mutation {
  # 用户操作
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
  followUser(userId: ID!): User!
  unfollowUser(userId: ID!): User!
  
  # 文章操作
  createPost(input: CreatePostInput!): Post!
  updatePost(id: ID!, input: UpdatePostInput!): Post!
  deletePost(id: ID!): Boolean!
  publishPost(id: ID!): Post!
  unpublishPost(id: ID!): Post!
  
  # 互动操作
  likePost(postId: ID!): Like!
  unlikePost(postId: ID!): Boolean!
  createComment(postId: ID!, content: String!, parentId: ID): Comment!
  updateComment(id: ID!, content: String!): Comment!
  deleteComment(id: ID!): Boolean!
  likeComment(commentId: ID!): Like!
  unlikeComment(commentId: ID!): Boolean!
}

type Subscription {
  # 实时通知
  postCreated: Post!
  postUpdated(id: ID!): Post!
  commentAdded(postId: ID!): Comment!
  userFollowed(userId: ID!): User!
  
  # 实时统计
  postLikesCount(postId: ID!): Int!
  userFollowersCount(userId: ID!): Int!
}

2. 自定义标量类型

javascript
// scalars/DateTime.js
const { GraphQLScalarType, GraphQLError } = require('graphql');
const { Kind } = require('graphql/language');

const DateTimeType = new GraphQLScalarType({
  name: 'DateTime',
  description: 'DateTime custom scalar type',
  
  serialize(value) {
    // 发送给客户端时的序列化
    if (value instanceof Date) {
      return value.toISOString();
    }
    
    if (typeof value === 'string') {
      return new Date(value).toISOString();
    }
    
    throw new GraphQLError(`Value is not a valid DateTime: ${value}`);
  },
  
  parseValue(value) {
    // 从客户端变量解析
    if (typeof value === 'string') {
      const date = new Date(value);
      if (isNaN(date.getTime())) {
        throw new GraphQLError(`Value is not a valid DateTime: ${value}`);
      }
      return date;
    }
    
    throw new GraphQLError(`Value is not a valid DateTime: ${value}`);
  },
  
  parseLiteral(ast) {
    // 从查询字面量解析
    if (ast.kind === Kind.STRING) {
      const date = new Date(ast.value);
      if (isNaN(date.getTime())) {
        throw new GraphQLError(`Value is not a valid DateTime: ${ast.value}`);
      }
      return date;
    }
    
    throw new GraphQLError(`Can only parse strings to DateTime but got a: ${ast.kind}`);
  }
});

module.exports = DateTimeType;
javascript
// scalars/JSON.js
const { GraphQLScalarType, GraphQLError } = require('graphql');
const { Kind } = require('graphql/language');

const JSONType = new GraphQLScalarType({
  name: 'JSON',
  description: 'JSON custom scalar type',
  
  serialize(value) {
    return value;
  },
  
  parseValue(value) {
    return value;
  },
  
  parseLiteral(ast) {
    switch (ast.kind) {
      case Kind.STRING:
      case Kind.BOOLEAN:
        return ast.value;
      case Kind.INT:
      case Kind.FLOAT:
        return parseFloat(ast.value);
      case Kind.OBJECT:
        return parseObject(ast);
      case Kind.LIST:
        return ast.values.map(parseLiteral);
      default:
        return null;
    }
  }
});

function parseObject(ast) {
  const value = Object.create(null);
  ast.fields.forEach(field => {
    value[field.name.value] = parseLiteral(field.value);
  });
  return value;
}

function parseLiteral(ast) {
  switch (ast.kind) {
    case Kind.STRING:
    case Kind.BOOLEAN:
      return ast.value;
    case Kind.INT:
    case Kind.FLOAT:
      return parseFloat(ast.value);
    case Kind.OBJECT:
      return parseObject(ast);
    case Kind.LIST:
      return ast.values.map(parseLiteral);
    default:
      return null;
  }
}

module.exports = JSONType;

Resolver 实现

1. 查询解析器

javascript
// resolvers/userResolvers.js
const { AuthenticationError, ForbiddenError, UserInputError } = require('apollo-server-express');
const DataLoader = require('dataloader');

class UserResolvers {
  constructor() {
    // 创建 DataLoader 实例
    this.userLoader = new DataLoader(this.batchUsers.bind(this));
    this.userPostsLoader = new DataLoader(this.batchUserPosts.bind(this));
    this.userFollowersLoader = new DataLoader(this.batchUserFollowers.bind(this));
  }
  
  // 批量加载用户
  async batchUsers(userIds) {
    const users = await User.find({ _id: { $in: userIds } });
    const userMap = new Map(users.map(user => [user._id.toString(), user]));
    return userIds.map(id => userMap.get(id.toString()) || null);
  }
  
  // 批量加载用户文章
  async batchUserPosts(userIds) {
    const posts = await Post.find({ author: { $in: userIds } });
    const postsByUser = new Map();
    
    posts.forEach(post => {
      const userId = post.author.toString();
      if (!postsByUser.has(userId)) {
        postsByUser.set(userId, []);
      }
      postsByUser.get(userId).push(post);
    });
    
    return userIds.map(id => postsByUser.get(id.toString()) || []);
  }
  
  // 批量加载用户关注者
  async batchUserFollowers(userIds) {
    const follows = await Follow.find({ following: { $in: userIds } }).populate('follower');
    const followersByUser = new Map();
    
    follows.forEach(follow => {
      const userId = follow.following.toString();
      if (!followersByUser.has(userId)) {
        followersByUser.set(userId, []);
      }
      followersByUser.get(userId).push(follow.follower);
    });
    
    return userIds.map(id => followersByUser.get(id.toString()) || []);
  }
  
  // Query 解析器
  Query = {
    user: async (parent, { id }, context) => {
      return this.userLoader.load(id);
    },
    
    users: async (parent, { pagination, sort, search }, context) => {
      const { first, after, last, before } = pagination || {};
      const { field, order } = sort || { field: 'createdAt', order: 'DESC' };
      
      // 构建查询条件
      const query = {};
      if (search) {
        query.$or = [
          { username: { $regex: search, $options: 'i' } },
          { email: { $regex: search, $options: 'i' } },
          { 'profile.firstName': { $regex: search, $options: 'i' } },
          { 'profile.lastName': { $regex: search, $options: 'i' } }
        ];
      }
      
      // 游标分页
      if (after) {
        const afterUser = await User.findById(after);
        if (afterUser) {
          query[field] = order === 'ASC' 
            ? { $gt: afterUser[field] }
            : { $lt: afterUser[field] };
        }
      }
      
      if (before) {
        const beforeUser = await User.findById(before);
        if (beforeUser) {
          query[field] = order === 'ASC'
            ? { $lt: beforeUser[field] }
            : { $gt: beforeUser[field] };
        }
      }
      
      // 执行查询
      const limit = first || last || 10;
      const sortOrder = order === 'ASC' ? 1 : -1;
      
      const users = await User.find(query)
        .sort({ [field]: sortOrder })
        .limit(limit + 1);
      
      const hasNextPage = users.length > limit;
      const hasPreviousPage = !!after;
      
      const edges = users.slice(0, limit).map(user => ({
        node: user,
        cursor: user._id.toString()
      }));
      
      const totalCount = await User.countDocuments(query);
      
      return {
        edges,
        pageInfo: {
          hasNextPage,
          hasPreviousPage,
          startCursor: edges.length > 0 ? edges[0].cursor : null,
          endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null
        },
        totalCount
      };
    },
    
    me: async (parent, args, { user }) => {
      if (!user) {
        throw new AuthenticationError('You must be logged in');
      }
      return this.userLoader.load(user.id);
    }
  };
  
  // User 类型解析器
  User = {
    posts: async (user, args, context) => {
      return this.userPostsLoader.load(user._id.toString());
    },
    
    followers: async (user, args, context) => {
      return this.userFollowersLoader.load(user._id.toString());
    },
    
    following: async (user, args, context) => {
      const follows = await Follow.find({ follower: user._id }).populate('following');
      return follows.map(follow => follow.following);
    }
  };
  
  // Mutation 解析器
  Mutation = {
    createUser: async (parent, { input }, context) => {
      const { username, email, password, profile } = input;
      
      // 验证用户名和邮箱唯一性
      const existingUser = await User.findOne({
        $or: [{ username }, { email }]
      });
      
      if (existingUser) {
        throw new UserInputError('Username or email already exists');
      }
      
      // 创建用户
      const user = new User({
        username,
        email,
        password: await bcrypt.hash(password, 10),
        profile
      });
      
      await user.save();
      
      return user;
    },
    
    updateUser: async (parent, { id, input }, { user }) => {
      if (!user) {
        throw new AuthenticationError('You must be logged in');
      }
      
      if (user.id !== id) {
        throw new ForbiddenError('You can only update your own profile');
      }
      
      const updatedUser = await User.findByIdAndUpdate(
        id,
        { $set: input },
        { new: true, runValidators: true }
      );
      
      if (!updatedUser) {
        throw new UserInputError('User not found');
      }
      
      // 清除缓存
      this.userLoader.clear(id);
      
      return updatedUser;
    },
    
    deleteUser: async (parent, { id }, { user }) => {
      if (!user) {
        throw new AuthenticationError('You must be logged in');
      }
      
      if (user.id !== id && user.role !== 'admin') {
        throw new ForbiddenError('You can only delete your own account');
      }
      
      const deletedUser = await User.findByIdAndDelete(id);
      
      if (!deletedUser) {
        throw new UserInputError('User not found');
      }
      
      // 清除相关数据
      await Post.deleteMany({ author: id });
      await Follow.deleteMany({ $or: [{ follower: id }, { following: id }] });
      
      // 清除缓存
      this.userLoader.clear(id);
      
      return true;
    },
    
    followUser: async (parent, { userId }, { user }) => {
      if (!user) {
        throw new AuthenticationError('You must be logged in');
      }
      
      if (user.id === userId) {
        throw new UserInputError('You cannot follow yourself');
      }
      
      const targetUser = await User.findById(userId);
      if (!targetUser) {
        throw new UserInputError('User not found');
      }
      
      // 检查是否已经关注
      const existingFollow = await Follow.findOne({
        follower: user.id,
        following: userId
      });
      
      if (existingFollow) {
        throw new UserInputError('You are already following this user');
      }
      
      // 创建关注关系
      const follow = new Follow({
        follower: user.id,
        following: userId
      });
      
      await follow.save();
      
      // 清除缓存
      this.userFollowersLoader.clear(userId);
      
      return targetUser;
    },
    
    unfollowUser: async (parent, { userId }, { user }) => {
      if (!user) {
        throw new AuthenticationError('You must be logged in');
      }
      
      const follow = await Follow.findOneAndDelete({
        follower: user.id,
        following: userId
      });
      
      if (!follow) {
        throw new UserInputError('You are not following this user');
      }
      
      // 清除缓存
      this.userFollowersLoader.clear(userId);
      
      const targetUser = await User.findById(userId);
      return targetUser;
    }
  };
}

module.exports = new UserResolvers();

2. 文章解析器

javascript
// resolvers/postResolvers.js
const { AuthenticationError, ForbiddenError, UserInputError } = require('apollo-server-express');
const { PubSub } = require('graphql-subscriptions');
const DataLoader = require('dataloader');

const pubsub = new PubSub();

class PostResolvers {
  constructor() {
    this.postLoader = new DataLoader(this.batchPosts.bind(this));
    this.postCommentsLoader = new DataLoader(this.batchPostComments.bind(this));
    this.postLikesLoader = new DataLoader(this.batchPostLikes.bind(this));
    this.postTagsLoader = new DataLoader(this.batchPostTags.bind(this));
  }
  
  async batchPosts(postIds) {
    const posts = await Post.find({ _id: { $in: postIds } });
    const postMap = new Map(posts.map(post => [post._id.toString(), post]));
    return postIds.map(id => postMap.get(id.toString()) || null);
  }
  
  async batchPostComments(postIds) {
    const comments = await Comment.find({ post: { $in: postIds } })
      .populate('author')
      .sort({ createdAt: -1 });
    
    const commentsByPost = new Map();
    comments.forEach(comment => {
      const postId = comment.post.toString();
      if (!commentsByPost.has(postId)) {
        commentsByPost.set(postId, []);
      }
      commentsByPost.get(postId).push(comment);
    });
    
    return postIds.map(id => commentsByPost.get(id.toString()) || []);
  }
  
  async batchPostLikes(postIds) {
    const likes = await Like.find({ post: { $in: postIds } }).populate('user');
    const likesByPost = new Map();
    
    likes.forEach(like => {
      const postId = like.post.toString();
      if (!likesByPost.has(postId)) {
        likesByPost.set(postId, []);
      }
      likesByPost.get(postId).push(like);
    });
    
    return postIds.map(id => likesByPost.get(id.toString()) || []);
  }
  
  async batchPostTags(postIds) {
    const posts = await Post.find({ _id: { $in: postIds } }).populate('tags');
    const tagsByPost = new Map();
    
    posts.forEach(post => {
      tagsByPost.set(post._id.toString(), post.tags);
    });
    
    return postIds.map(id => tagsByPost.get(id.toString()) || []);
  }
  
  Query = {
    post: async (parent, { id }, context) => {
      const post = await this.postLoader.load(id);
      
      if (!post) {
        throw new UserInputError('Post not found');
      }
      
      // 检查访问权限
      if (!post.published && (!context.user || context.user.id !== post.author.toString())) {
        throw new ForbiddenError('Post not published');
      }
      
      return post;
    },
    
    posts: async (parent, args, context) => {
      const { pagination, sort, authorId, tag, published, search } = args;
      const { first, after, last, before } = pagination || {};
      const { field, order } = sort || { field: 'createdAt', order: 'DESC' };
      
      // 构建查询条件
      const query = {};
      
      if (authorId) {
        query.author = authorId;
      }
      
      if (published !== undefined) {
        query.published = published;
      } else if (!context.user) {
        // 未登录用户只能看到已发布的文章
        query.published = true;
      }
      
      if (tag) {
        const tagDoc = await Tag.findOne({ slug: tag });
        if (tagDoc) {
          query.tags = tagDoc._id;
        } else {
          // 标签不存在,返回空结果
          return {
            edges: [],
            pageInfo: {
              hasNextPage: false,
              hasPreviousPage: false,
              startCursor: null,
              endCursor: null
            },
            totalCount: 0
          };
        }
      }
      
      if (search) {
        query.$or = [
          { title: { $regex: search, $options: 'i' } },
          { content: { $regex: search, $options: 'i' } },
          { excerpt: { $regex: search, $options: 'i' } }
        ];
      }
      
      // 游标分页逻辑
      if (after) {
        const afterPost = await Post.findById(after);
        if (afterPost) {
          query[field] = order === 'ASC'
            ? { $gt: afterPost[field] }
            : { $lt: afterPost[field] };
        }
      }
      
      const limit = first || last || 10;
      const sortOrder = order === 'ASC' ? 1 : -1;
      
      const posts = await Post.find(query)
        .populate('author')
        .sort({ [field]: sortOrder })
        .limit(limit + 1);
      
      const hasNextPage = posts.length > limit;
      const edges = posts.slice(0, limit).map(post => ({
        node: post,
        cursor: post._id.toString()
      }));
      
      const totalCount = await Post.countDocuments(query);
      
      return {
        edges,
        pageInfo: {
          hasNextPage,
          hasPreviousPage: !!after,
          startCursor: edges.length > 0 ? edges[0].cursor : null,
          endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null
        },
        totalCount
      };
    }
  };
  
  Post = {
    author: async (post, args, context) => {
      return context.dataSources.userAPI.getUserById(post.author);
    },
    
    comments: async (post, args, context) => {
      return this.postCommentsLoader.load(post._id.toString());
    },
    
    likes: async (post, args, context) => {
      return this.postLikesLoader.load(post._id.toString());
    },
    
    tags: async (post, args, context) => {
      return this.postTagsLoader.load(post._id.toString());
    },
    
    likesCount: async (post, args, context) => {
      const likes = await this.postLikesLoader.load(post._id.toString());
      return likes.length;
    },
    
    commentsCount: async (post, args, context) => {
      const comments = await this.postCommentsLoader.load(post._id.toString());
      return comments.length;
    }
  };
  
  Mutation = {
    createPost: async (parent, { input }, { user }) => {
      if (!user) {
        throw new AuthenticationError('You must be logged in to create a post');
      }
      
      const { title, content, excerpt, tags, published } = input;
      
      // 处理标签
      const tagIds = [];
      if (tags && tags.length > 0) {
        for (const tagName of tags) {
          let tag = await Tag.findOne({ name: tagName });
          if (!tag) {
            tag = new Tag({
              name: tagName,
              slug: tagName.toLowerCase().replace(/\s+/g, '-')
            });
            await tag.save();
          }
          tagIds.push(tag._id);
        }
      }
      
      // 创建文章
      const post = new Post({
        title,
        content,
        excerpt: excerpt || content.substring(0, 200),
        author: user.id,
        tags: tagIds,
        published,
        publishedAt: published ? new Date() : null
      });
      
      await post.save();
      await post.populate(['author', 'tags']);
      
      // 发布订阅事件
      if (published) {
        pubsub.publish('POST_CREATED', { postCreated: post });
      }
      
      return post;
    },
    
    updatePost: async (parent, { id, input }, { user }) => {
      if (!user) {
        throw new AuthenticationError('You must be logged in');
      }
      
      const post = await Post.findById(id);
      if (!post) {
        throw new UserInputError('Post not found');
      }
      
      if (post.author.toString() !== user.id) {
        throw new ForbiddenError('You can only edit your own posts');
      }
      
      const { tags, ...updateData } = input;
      
      // 处理标签更新
      if (tags) {
        const tagIds = [];
        for (const tagName of tags) {
          let tag = await Tag.findOne({ name: tagName });
          if (!tag) {
            tag = new Tag({
              name: tagName,
              slug: tagName.toLowerCase().replace(/\s+/g, '-')
            });
            await tag.save();
          }
          tagIds.push(tag._id);
        }
        updateData.tags = tagIds;
      }
      
      const updatedPost = await Post.findByIdAndUpdate(
        id,
        { $set: updateData },
        { new: true, runValidators: true }
      ).populate(['author', 'tags']);
      
      // 清除缓存
      this.postLoader.clear(id);
      this.postTagsLoader.clear(id);
      
      // 发布订阅事件
      pubsub.publish('POST_UPDATED', { postUpdated: updatedPost });
      
      return updatedPost;
    },
    
    deletePost: async (parent, { id }, { user }) => {
      if (!user) {
        throw new AuthenticationError('You must be logged in');
      }
      
      const post = await Post.findById(id);
      if (!post) {
        throw new UserInputError('Post not found');
      }
      
      if (post.author.toString() !== user.id && user.role !== 'admin') {
        throw new ForbiddenError('You can only delete your own posts');
      }
      
      // 删除相关数据
      await Comment.deleteMany({ post: id });
      await Like.deleteMany({ post: id });
      await Post.findByIdAndDelete(id);
      
      // 清除缓存
      this.postLoader.clear(id);
      this.postCommentsLoader.clear(id);
      this.postLikesLoader.clear(id);
      this.postTagsLoader.clear(id);
      
      return true;
    },
    
    likePost: async (parent, { postId }, { user }) => {
      if (!user) {
        throw new AuthenticationError('You must be logged in');
      }
      
      const post = await Post.findById(postId);
      if (!post) {
        throw new UserInputError('Post not found');
      }
      
      // 检查是否已经点赞
      const existingLike = await Like.findOne({
        user: user.id,
        post: postId
      });
      
      if (existingLike) {
        throw new UserInputError('You have already liked this post');
      }
      
      // 创建点赞
      const like = new Like({
        user: user.id,
        post: postId
      });
      
      await like.save();
      await like.populate('user');
      
      // 清除缓存
      this.postLikesLoader.clear(postId);
      
      // 发布实时更新
      const likesCount = await Like.countDocuments({ post: postId });
      pubsub.publish('POST_LIKES_COUNT', {
        postLikesCount: likesCount,
        postId
      });
      
      return like;
    }
  };
  
  Subscription = {
    postCreated: {
      subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
    },
    
    postUpdated: {
      subscribe: (parent, { id }) => {
        return pubsub.asyncIterator([`POST_UPDATED_${id}`]);
      }
    },
    
    commentAdded: {
      subscribe: (parent, { postId }) => {
        return pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]);
      }
    },
    
    postLikesCount: {
      subscribe: (parent, { postId }) => {
        return pubsub.asyncIterator([`POST_LIKES_COUNT_${postId}`]);
      }
    }
  };
}

module.exports = new PostResolvers();

数据加载优化

1. DataLoader 实现

javascript
// dataloaders/index.js
const DataLoader = require('dataloader');
const { User, Post, Comment, Like, Tag } = require('../models');

class DataLoaders {
  constructor() {
    // 用户相关
    this.userLoader = new DataLoader(this.batchUsers.bind(this));
    this.userPostsLoader = new DataLoader(this.batchUserPosts.bind(this));
    this.userFollowersLoader = new DataLoader(this.batchUserFollowers.bind(this));
    
    // 文章相关
    this.postLoader = new DataLoader(this.batchPosts.bind(this));
    this.postAuthorLoader = new DataLoader(this.batchPostAuthors.bind(this));
    this.postCommentsLoader = new DataLoader(this.batchPostComments.bind(this));
    this.postLikesLoader = new DataLoader(this.batchPostLikes.bind(this));
    this.postTagsLoader = new DataLoader(this.batchPostTags.bind(this));
    
    // 评论相关
    this.commentLoader = new DataLoader(this.batchComments.bind(this));
    this.commentAuthorLoader = new DataLoader(this.batchCommentAuthors.bind(this));
    this.commentRepliesLoader = new DataLoader(this.batchCommentReplies.bind(this));
    
    // 标签相关
    this.tagLoader = new DataLoader(this.batchTags.bind(this));
    this.tagPostsLoader = new DataLoader(this.batchTagPosts.bind(this));
  }
  
  // 用户批量加载
  async batchUsers(userIds) {
    const users = await User.find({ _id: { $in: userIds } });
    const userMap = new Map(users.map(user => [user._id.toString(), user]));
    return userIds.map(id => userMap.get(id.toString()) || null);
  }
  
  async batchUserPosts(userIds) {
    const posts = await Post.find({ author: { $in: userIds } })
      .sort({ createdAt: -1 });
    
    const postsByUser = new Map();
    posts.forEach(post => {
      const userId = post.author.toString();
      if (!postsByUser.has(userId)) {
        postsByUser.set(userId, []);
      }
      postsByUser.get(userId).push(post);
    });
    
    return userIds.map(id => postsByUser.get(id.toString()) || []);
  }
  
  async batchUserFollowers(userIds) {
    const follows = await Follow.find({ following: { $in: userIds } })
      .populate('follower');
    
    const followersByUser = new Map();
    follows.forEach(follow => {
      const userId = follow.following.toString();
      if (!followersByUser.has(userId)) {
        followersByUser.set(userId, []);
      }
      followersByUser.get(userId).push(follow.follower);
    });
    
    return userIds.map(id => followersByUser.get(id.toString()) || []);
  }
  
  // 文章批量加载
  async batchPosts(postIds) {
    const posts = await Post.find({ _id: { $in: postIds } });
    const postMap = new Map(posts.map(post => [post._id.toString(), post]));
    return postIds.map(id => postMap.get(id.toString()) || null);
  }
  
  async batchPostAuthors(postIds) {
    const posts = await Post.find({ _id: { $in: postIds } }).populate('author');
    const authorsByPost = new Map();
    
    posts.forEach(post => {
      authorsByPost.set(post._id.toString(), post.author);
    });
    
    return postIds.map(id => authorsByPost.get(id.toString()) || null);
  }
  
  async batchPostComments(postIds) {
    const comments = await Comment.find({ post: { $in: postIds } })
      .populate('author')
      .sort({ createdAt: -1 });
    
    const commentsByPost = new Map();
    comments.forEach(comment => {
      const postId = comment.post.toString();
      if (!commentsByPost.has(postId)) {
        commentsByPost.set(postId, []);
      }
      commentsByPost.get(postId).push(comment);
    });
    
    return postIds.map(id => commentsByPost.get(id.toString()) || []);
  }
  
  async batchPostLikes(postIds) {
    const likes = await Like.find({ post: { $in: postIds } })
      .populate('user');
    
    const likesByPost = new Map();
    likes.forEach(like => {
      const postId = like.post.toString();
      if (!likesByPost.has(postId)) {
        likesByPost.set(postId, []);
      }
      likesByPost.get(postId).push(like);
    });
    
    return postIds.map(id => likesByPost.get(id.toString()) || []);
  }
  
  async batchPostTags(postIds) {
    const posts = await Post.find({ _id: { $in: postIds } })
      .populate('tags');
    
    const tagsByPost = new Map();
    posts.forEach(post => {
      tagsByPost.set(post._id.toString(), post.tags);
    });
    
    return postIds.map(id => tagsByPost.get(id.toString()) || []);
  }
  
  // 评论批量加载
  async batchComments(commentIds) {
    const comments = await Comment.find({ _id: { $in: commentIds } });
    const commentMap = new Map(comments.map(comment => [comment._id.toString(), comment]));
    return commentIds.map(id => commentMap.get(id.toString()) || null);
  }
  
  async batchCommentAuthors(commentIds) {
    const comments = await Comment.find({ _id: { $in: commentIds } })
      .populate('author');
    
    const authorsByComment = new Map();
    comments.forEach(comment => {
      authorsByComment.set(comment._id.toString(), comment.author);
    });
    
    return commentIds.map(id => authorsByComment.get(id.toString()) || null);
  }
  
  async batchCommentReplies(commentIds) {
    const replies = await Comment.find({ parent: { $in: commentIds } })
      .populate('author')
      .sort({ createdAt: 1 });
    
    const repliesByComment = new Map();
    replies.forEach(reply => {
      const parentId = reply.parent.toString();
      if (!repliesByComment.has(parentId)) {
        repliesByComment.set(parentId, []);
      }
      repliesByComment.get(parentId).push(reply);
    });
    
    return commentIds.map(id => repliesByComment.get(id.toString()) || []);
  }
  
  // 标签批量加载
  async batchTags(tagIds) {
    const tags = await Tag.find({ _id: { $in: tagIds } });
    const tagMap = new Map(tags.map(tag => [tag._id.toString(), tag]));
    return tagIds.map(id => tagMap.get(id.toString()) || null);
  }
  
  async batchTagPosts(tagIds) {
    const posts = await Post.find({ tags: { $in: tagIds } })
      .populate('author')
      .sort({ createdAt: -1 });
    
    const postsByTag = new Map();
    posts.forEach(post => {
      post.tags.forEach(tagId => {
        const tagIdStr = tagId.toString();
        if (!postsByTag.has(tagIdStr)) {
          postsByTag.set(tagIdStr, []);
        }
        postsByTag.get(tagIdStr).push(post);
      });
    });
    
    return tagIds.map(id => postsByTag.get(id.toString()) || []);
  }
  
  // 清除缓存方法
  clearUser(userId) {
    this.userLoader.clear(userId);
    this.userPostsLoader.clear(userId);
    this.userFollowersLoader.clear(userId);
  }
  
  clearPost(postId) {
    this.postLoader.clear(postId);
    this.postCommentsLoader.clear(postId);
    this.postLikesLoader.clear(postId);
    this.postTagsLoader.clear(postId);
  }
  
  clearComment(commentId) {
    this.commentLoader.clear(commentId);
    this.commentRepliesLoader.clear(commentId);
  }
  
  clearTag(tagId) {
    this.tagLoader.clear(tagId);
    this.tagPostsLoader.clear(tagId);
  }
}

module.exports = DataLoaders;

2. 查询复杂度控制

javascript
// middleware/queryComplexity.js
const depthLimit = require('graphql-depth-limit');
const costAnalysis = require('graphql-query-complexity').costAnalysisMaximumCostRule;
const { createComplexityLimitRule } = require('graphql-query-complexity');

// 查询深度限制
const depthLimitRule = depthLimit(10);

// 查询复杂度分析
const complexityLimitRule = createComplexityLimitRule(1000, {
  maximumCost: 1000,
  onComplete: (cost) => {
    console.log(`Query cost: ${cost}`);
  },
  createError: (max, actual) => {
    return new Error(`Query cost ${actual} exceeds maximum cost ${max}`);
  },
  scalarCost: 1,
  objectCost: 2,
  listFactor: 10,
  introspectionCost: 1000
});

// 自定义复杂度计算
const typeComplexityMap = {
  Query: {
    users: { complexity: ({ args, childComplexity }) => {
      const first = args.first || 10;
      return first * childComplexity;
    }},
    posts: { complexity: ({ args, childComplexity }) => {
      const first = args.first || 10;
      return first * childComplexity;
    }}
  },
  User: {
    posts: { complexity: ({ args, childComplexity }) => {
      return 5 * childComplexity; // 用户文章查询成本较高
    }},
    followers: { complexity: ({ args, childComplexity }) => {
      return 3 * childComplexity;
    }}
  },
  Post: {
    comments: { complexity: ({ args, childComplexity }) => {
      return 2 * childComplexity;
    }}
  }
};

module.exports = {
  depthLimitRule,
  complexityLimitRule,
  typeComplexityMap
};

实时订阅

1. 订阅实现

javascript
// subscriptions/index.js
const { PubSub, withFilter } = require('graphql-subscriptions');
const { RedisPubSub } = require('graphql-redis-subscriptions');
const Redis = require('ioredis');

// 创建 Redis 发布订阅
const redisOptions = {
  host: process.env.REDIS_HOST || 'localhost',
  port: process.env.REDIS_PORT || 6379,
  password: process.env.REDIS_PASSWORD,
  retryDelayOnFailover: 100,
  enableReadyCheck: false,
  maxRetriesPerRequest: null
};

const pubsub = new RedisPubSub({
  publisher: new Redis(redisOptions),
  subscriber: new Redis(redisOptions)
});

// 订阅事件常量
const SUBSCRIPTION_EVENTS = {
  POST_CREATED: 'POST_CREATED',
  POST_UPDATED: 'POST_UPDATED',
  POST_DELETED: 'POST_DELETED',
  COMMENT_ADDED: 'COMMENT_ADDED',
  COMMENT_UPDATED: 'COMMENT_UPDATED',
  COMMENT_DELETED: 'COMMENT_DELETED',
  USER_FOLLOWED: 'USER_FOLLOWED',
  USER_UNFOLLOWED: 'USER_UNFOLLOWED',
  POST_LIKED: 'POST_LIKED',
  POST_UNLIKED: 'POST_UNLIKED',
  NOTIFICATION_CREATED: 'NOTIFICATION_CREATED'
};

// 订阅解析器
const subscriptionResolvers = {
  Subscription: {
    // 文章创建订阅
    postCreated: {
      subscribe: withFilter(
        () => pubsub.asyncIterator([SUBSCRIPTION_EVENTS.POST_CREATED]),
        (payload, variables, context) => {
          // 只有已发布的文章才推送给订阅者
          return payload.postCreated.published;
        }
      )
    },
    
    // 文章更新订阅
    postUpdated: {
      subscribe: withFilter(
        () => pubsub.asyncIterator([SUBSCRIPTION_EVENTS.POST_UPDATED]),
        (payload, variables) => {
          // 只订阅特定文章的更新
          return payload.postUpdated.id === variables.id;
        }
      )
    },
    
    // 评论添加订阅
    commentAdded: {
      subscribe: withFilter(
        () => pubsub.asyncIterator([SUBSCRIPTION_EVENTS.COMMENT_ADDED]),
        (payload, variables) => {
          // 只订阅特定文章的评论
          return payload.commentAdded.post.toString() === variables.postId;
        }
      )
    },
    
    // 用户关注订阅
    userFollowed: {
      subscribe: withFilter(
        () => pubsub.asyncIterator([SUBSCRIPTION_EVENTS.USER_FOLLOWED]),
        (payload, variables, context) => {
          // 只有被关注的用户才能收到通知
          return payload.userFollowed.id === variables.userId;
        }
      ),
      resolve: (payload) => {
        return payload.userFollowed;
      }
    },
    
    // 文章点赞数实时更新
    postLikesCount: {
      subscribe: withFilter(
        () => pubsub.asyncIterator([SUBSCRIPTION_EVENTS.POST_LIKED, SUBSCRIPTION_EVENTS.POST_UNLIKED]),
        (payload, variables) => {
          return payload.postId === variables.postId;
        }
      ),
      resolve: (payload) => {
        return payload.likesCount;
      }
    },
    
    // 用户关注者数量实时更新
    userFollowersCount: {
      subscribe: withFilter(
        () => pubsub.asyncIterator([SUBSCRIPTION_EVENTS.USER_FOLLOWED, SUBSCRIPTION_EVENTS.USER_UNFOLLOWED]),
        (payload, variables) => {
          return payload.userId === variables.userId;
        }
      ),
      resolve: (payload) => {
        return payload.followersCount;
      }
    },
    
    // 通知订阅
    notificationCreated: {
      subscribe: withFilter(
        () => pubsub.asyncIterator([SUBSCRIPTION_EVENTS.NOTIFICATION_CREATED]),
        (payload, variables, context) => {
          // 只推送给目标用户
          return context.user && payload.notificationCreated.userId === context.user.id;
        }
      )
    }
  }
};

// 发布事件的辅助函数
class SubscriptionPublisher {
  static async publishPostCreated(post) {
    await pubsub.publish(SUBSCRIPTION_EVENTS.POST_CREATED, {
      postCreated: post
    });
  }
  
  static async publishPostUpdated(post) {
    await pubsub.publish(SUBSCRIPTION_EVENTS.POST_UPDATED, {
      postUpdated: post
    });
  }
  
  static async publishCommentAdded(comment) {
    await pubsub.publish(SUBSCRIPTION_EVENTS.COMMENT_ADDED, {
      commentAdded: comment
    });
  }
  
  static async publishUserFollowed(follower, following) {
    await pubsub.publish(SUBSCRIPTION_EVENTS.USER_FOLLOWED, {
      userFollowed: following,
      follower: follower
    });
    
    // 同时更新关注者数量
    const followersCount = await Follow.countDocuments({ following: following.id });
    await pubsub.publish(SUBSCRIPTION_EVENTS.USER_FOLLOWED, {
      userId: following.id,
      followersCount
    });
  }
  
  static async publishPostLiked(postId, likesCount) {
    await pubsub.publish(SUBSCRIPTION_EVENTS.POST_LIKED, {
      postId,
      likesCount
    });
  }
  
  static async publishPostUnliked(postId, likesCount) {
    await pubsub.publish(SUBSCRIPTION_EVENTS.POST_UNLIKED, {
      postId,
      likesCount
    });
  }
  
  static async publishNotificationCreated(notification) {
    await pubsub.publish(SUBSCRIPTION_EVENTS.NOTIFICATION_CREATED, {
      notificationCreated: notification
    });
  }
}

module.exports = {
  pubsub,
  subscriptionResolvers,
  SubscriptionPublisher,
  SUBSCRIPTION_EVENTS
};

2. WebSocket 认证

javascript
// auth/websocketAuth.js
const jwt = require('jsonwebtoken');
const { AuthenticationError } = require('apollo-server-express');

class WebSocketAuth {
  // WebSocket 连接认证
  static async authenticateConnection(connectionParams) {
    try {
      const token = connectionParams.authorization?.replace('Bearer ', '') ||
                   connectionParams.Authorization?.replace('Bearer ', '');
      
      if (!token) {
        throw new AuthenticationError('No token provided');
      }
      
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      const user = await User.findById(decoded.id);
      
      if (!user) {
        throw new AuthenticationError('User not found');
      }
      
      return {
        user,
        isAuthenticated: true
      };
    } catch (error) {
      console.error('WebSocket authentication error:', error.message);
      throw new AuthenticationError('Invalid token');
    }
  }
  
  // 订阅权限检查
  static checkSubscriptionPermission(subscriptionName, user, variables) {
    switch (subscriptionName) {
      case 'notificationCreated':
        // 通知订阅需要登录
        if (!user) {
          throw new AuthenticationError('You must be logged in to subscribe to notifications');
        }
        return true;
      
      case 'userFollowed':
        // 用户关注订阅需要是目标用户本人
        if (!user || user.id !== variables.userId) {
          throw new AuthenticationError('You can only subscribe to your own follow notifications');
        }
        return true;
      
      case 'postCreated':
      case 'postUpdated':
      case 'commentAdded':
      case 'postLikesCount':
      case 'userFollowersCount':
        // 这些订阅是公开的
        return true;
      
      default:
        return true;
    }
  }
}

module.exports = WebSocketAuth;

安全性和性能

1. 查询安全

javascript
// security/queryValidation.js
const { ValidationError } = require('apollo-server-express');

class QueryValidator {
  // 查询白名单
  static allowedQueries = new Set([
    'IntrospectionQuery', // GraphQL Playground 内省查询
    'GetUser',
    'GetUsers',
    'GetPost',
    'GetPosts',
    'GetMe',
    'CreateUser',
    'UpdateUser',
    'CreatePost',
    'UpdatePost',
    'LikePost',
    'CreateComment'
  ]);
  
  // 验证查询名称
  static validateQueryName(queryName) {
    if (process.env.NODE_ENV === 'production' && !this.allowedQueries.has(queryName)) {
      throw new ValidationError(`Query "${queryName}" is not allowed`);
    }
    return true;
  }
  
  // 验证查询复杂度
  static validateQueryComplexity(query, variables, context) {
    // 检查嵌套深度
    const maxDepth = 10;
    const depth = this.calculateQueryDepth(query);
    
    if (depth > maxDepth) {
      throw new ValidationError(`Query depth ${depth} exceeds maximum depth ${maxDepth}`);
    }
    
    // 检查字段数量
    const maxFields = 100;
    const fieldCount = this.countQueryFields(query);
    
    if (fieldCount > maxFields) {
      throw new ValidationError(`Query field count ${fieldCount} exceeds maximum ${maxFields}`);
    }
    
    return true;
  }
  
  // 计算查询深度
  static calculateQueryDepth(query, depth = 0) {
    if (!query || !query.selectionSet) {
      return depth;
    }
    
    let maxChildDepth = depth;
    
    for (const selection of query.selectionSet.selections) {
      if (selection.kind === 'Field') {
        const childDepth = this.calculateQueryDepth(selection, depth + 1);
        maxChildDepth = Math.max(maxChildDepth, childDepth);
      }
    }
    
    return maxChildDepth;
  }
  
  // 计算查询字段数量
  static countQueryFields(query) {
    if (!query || !query.selectionSet) {
      return 0;
    }
    
    let count = 0;
    
    for (const selection of query.selectionSet.selections) {
      if (selection.kind === 'Field') {
        count += 1 + this.countQueryFields(selection);
      }
    }
    
    return count;
  }
  
  // 速率限制检查
  static async checkRateLimit(context) {
    const { req } = context;
    const clientId = req.ip || req.connection.remoteAddress;
    const key = `graphql_rate_limit:${clientId}`;
    
    const redis = req.app.locals.redis;
    const current = await redis.incr(key);
    
    if (current === 1) {
      await redis.expire(key, 60); // 1分钟窗口
    }
    
    const limit = context.user ? 1000 : 100; // 登录用户更高限制
    
    if (current > limit) {
      throw new ValidationError('Rate limit exceeded');
    }
    
    return true;
  }
}

// 查询成本分析
class QueryCostAnalyzer {
  static fieldCosts = {
    // 基础字段成本
    'User.id': 1,
    'User.username': 1,
    'User.email': 2, // 敏感信息成本更高
    'User.posts': 10, // 关联查询成本高
    'User.followers': 15,
    'User.following': 15,
    
    'Post.id': 1,
    'Post.title': 1,
    'Post.content': 3,
    'Post.author': 5,
    'Post.comments': 20,
    'Post.likes': 10,
    
    'Comment.id': 1,
    'Comment.content': 2,
    'Comment.author': 5,
    'Comment.replies': 15
  };
  
  static calculateQueryCost(query, variables = {}) {
    return this.calculateSelectionSetCost(query.selectionSet, variables);
  }
  
  static calculateSelectionSetCost(selectionSet, variables, parentType = '') {
    if (!selectionSet) return 0;
    
    let totalCost = 0;
    
    for (const selection of selectionSet.selections) {
      if (selection.kind === 'Field') {
        const fieldName = selection.name.value;
        const fieldKey = parentType ? `${parentType}.${fieldName}` : fieldName;
        
        // 基础字段成本
        let fieldCost = this.fieldCosts[fieldKey] || 1;
        
        // 处理参数影响的成本
        if (selection.arguments) {
          for (const arg of selection.arguments) {
            if (arg.name.value === 'first' || arg.name.value === 'last') {
              const limit = this.getArgumentValue(arg, variables);
              fieldCost *= Math.min(limit || 10, 100); // 限制最大倍数
            }
          }
        }
        
        // 递归计算子字段成本
        if (selection.selectionSet) {
          const childCost = this.calculateSelectionSetCost(
            selection.selectionSet,
            variables,
            this.getFieldType(fieldKey)
          );
          fieldCost += childCost;
        }
        
        totalCost += fieldCost;
      }
    }
    
    return totalCost;
  }
  
  static getArgumentValue(argument, variables) {
    if (argument.value.kind === 'IntValue') {
      return parseInt(argument.value.value);
    }
    if (argument.value.kind === 'Variable') {
      return variables[argument.value.name.value];
    }
    return null;
  }
  
  static getFieldType(fieldKey) {
    const typeMap = {
      'Query.users': 'User',
      'Query.posts': 'Post',
      'User.posts': 'Post',
      'Post.comments': 'Comment',
      'Comment.replies': 'Comment'
    };
    return typeMap[fieldKey] || '';
  }
}

module.exports = {
  QueryValidator,
  QueryCostAnalyzer
};

2. 缓存策略

javascript
// cache/graphqlCache.js
const Redis = require('ioredis');
const { createHash } = require('crypto');

class GraphQLCache {
  constructor() {
    this.redis = new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: process.env.REDIS_PORT || 6379,
      password: process.env.REDIS_PASSWORD,
      db: 1 // 使用专门的数据库
    });
    
    this.defaultTTL = 300; // 5分钟默认缓存时间
    this.maxCacheSize = 1000000; // 1MB 最大缓存大小
  }
  
  // 生成缓存键
  generateCacheKey(query, variables, context) {
    const userId = context.user?.id || 'anonymous';
    const queryString = typeof query === 'string' ? query : query.loc.source.body;
    const variablesString = JSON.stringify(variables || {});
    
    const content = `${queryString}:${variablesString}:${userId}`;
    return `graphql:${createHash('md5').update(content).digest('hex')}`;
  }
  
  // 获取缓存
  async get(key) {
    try {
      const cached = await this.redis.get(key);
      if (cached) {
        return JSON.parse(cached);
      }
      return null;
    } catch (error) {
      console.error('Cache get error:', error);
      return null;
    }
  }
  
  // 设置缓存
  async set(key, data, ttl = this.defaultTTL) {
    try {
      const serialized = JSON.stringify(data);
      
      // 检查缓存大小
      if (serialized.length > this.maxCacheSize) {
        console.warn('Cache data too large, skipping cache');
        return false;
      }
      
      await this.redis.setex(key, ttl, serialized);
      return true;
    } catch (error) {
      console.error('Cache set error:', error);
      return false;
    }
  }
  
  // 删除缓存
  async del(key) {
    try {
      await this.redis.del(key);
      return true;
    } catch (error) {
      console.error('Cache delete error:', error);
      return false;
    }
  }
  
  // 批量删除缓存
  async delPattern(pattern) {
    try {
      const keys = await this.redis.keys(pattern);
      if (keys.length > 0) {
        await this.redis.del(...keys);
      }
      return keys.length;
    } catch (error) {
      console.error('Cache delete pattern error:', error);
      return 0;
    }
  }
  
  // 缓存中间件
  createCacheMiddleware(options = {}) {
    const {
      ttl = this.defaultTTL,
      skipCache = () => false,
      cacheKeyGenerator = this.generateCacheKey.bind(this)
    } = options;
    
    return async (resolve, parent, args, context, info) => {
      // 跳过缓存的条件
      if (skipCache(parent, args, context, info)) {
        return resolve(parent, args, context, info);
      }
      
      // 只缓存查询操作
      if (info.operation.operation !== 'query') {
        return resolve(parent, args, context, info);
      }
      
      const cacheKey = cacheKeyGenerator(info.operation, args, context);
      
      // 尝试从缓存获取
      const cached = await this.get(cacheKey);
      if (cached) {
        console.log(`Cache hit: ${cacheKey}`);
        return cached;
      }
      
      // 执行解析器
      const result = await resolve(parent, args, context, info);
      
      // 缓存结果
      if (result) {
        await this.set(cacheKey, result, ttl);
        console.log(`Cache set: ${cacheKey}`);
      }
      
      return result;
    };
  }
  
  // 智能缓存失效
  async invalidateRelatedCache(entityType, entityId) {
    const patterns = {
      user: [
        `graphql:*User*${entityId}*`,
        `graphql:*users*`,
        `graphql:*followers*`,
        `graphql:*following*`
      ],
      post: [
        `graphql:*Post*${entityId}*`,
        `graphql:*posts*`,
        `graphql:*comments*`
      ],
      comment: [
        `graphql:*Comment*${entityId}*`,
        `graphql:*comments*`
      ]
    };
    
    const patternsToDelete = patterns[entityType] || [];
    let totalDeleted = 0;
    
    for (const pattern of patternsToDelete) {
      const deleted = await this.delPattern(pattern);
      totalDeleted += deleted;
    }
    
    console.log(`Invalidated ${totalDeleted} cache entries for ${entityType}:${entityId}`);
    return totalDeleted;
  }
}

// 缓存装饰器
function cached(ttl = 300, keyGenerator) {
  return function(target, propertyName, descriptor) {
    const method = descriptor.value;
    
    descriptor.value = async function(...args) {
      const cache = this.cache || new GraphQLCache();
      
      // 生成缓存键
      const key = keyGenerator 
        ? keyGenerator.apply(this, args)
        : `${target.constructor.name}:${propertyName}:${JSON.stringify(args)}`;
      
      // 尝试从缓存获取
      const cached = await cache.get(key);
      if (cached) {
        return cached;
      }
      
      // 执行原方法
      const result = await method.apply(this, args);
      
      // 缓存结果
      if (result) {
        await cache.set(key, result, ttl);
      }
      
      return result;
    };
    
    return descriptor;
  };
}

module.exports = {
  GraphQLCache,
  cached
};

3. 错误处理

javascript
// errors/graphqlErrors.js
const { 
  ApolloError,
  AuthenticationError,
  ForbiddenError,
  UserInputError,
  ValidationError
} = require('apollo-server-express');

// 自定义错误类
class GraphQLCustomError extends ApolloError {
  constructor(message, code, extensions = {}) {
    super(message, code, extensions);
    this.name = 'GraphQLCustomError';
  }
}

class ResourceNotFoundError extends GraphQLCustomError {
  constructor(resource, id) {
    super(
      `${resource} with id ${id} not found`,
      'RESOURCE_NOT_FOUND',
      { resource, id }
    );
  }
}

class DuplicateResourceError extends GraphQLCustomError {
  constructor(resource, field, value) {
    super(
      `${resource} with ${field} '${value}' already exists`,
      'DUPLICATE_RESOURCE',
      { resource, field, value }
    );
  }
}

class RateLimitError extends GraphQLCustomError {
  constructor(limit, window) {
    super(
      `Rate limit exceeded: ${limit} requests per ${window}`,
      'RATE_LIMIT_EXCEEDED',
      { limit, window }
    );
  }
}

class QueryComplexityError extends GraphQLCustomError {
  constructor(complexity, maxComplexity) {
    super(
      `Query complexity ${complexity} exceeds maximum ${maxComplexity}`,
      'QUERY_TOO_COMPLEX',
      { complexity, maxComplexity }
    );
  }
}

// 错误格式化器
class ErrorFormatter {
  static formatError(error) {
    // 记录错误
    console.error('GraphQL Error:', {
      message: error.message,
      code: error.extensions?.code,
      path: error.path,
      locations: error.locations,
      stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
    });
    
    // 生产环境下隐藏敏感信息
    if (process.env.NODE_ENV === 'production') {
      // 隐藏内部错误详情
      if (error.message.includes('MongoError') || 
          error.message.includes('ValidationError')) {
        return new Error('Internal server error');
      }
      
      // 移除堆栈跟踪
      delete error.stack;
      delete error.extensions?.exception;
    }
    
    return error;
  }
  
  // 错误分类和统计
  static categorizeError(error) {
    const categories = {
      AUTHENTICATION: ['UNAUTHENTICATED', 'FORBIDDEN'],
      VALIDATION: ['BAD_USER_INPUT', 'GRAPHQL_VALIDATION_FAILED'],
      RATE_LIMIT: ['RATE_LIMIT_EXCEEDED'],
      COMPLEXITY: ['QUERY_TOO_COMPLEX'],
      RESOURCE: ['RESOURCE_NOT_FOUND', 'DUPLICATE_RESOURCE'],
      INTERNAL: ['INTERNAL_ERROR']
    };
    
    const code = error.extensions?.code;
    
    for (const [category, codes] of Object.entries(categories)) {
      if (codes.includes(code)) {
        return category;
      }
    }
    
    return 'UNKNOWN';
  }
}

// 错误监控和报告
class ErrorMonitor {
  constructor() {
    this.errorCounts = new Map();
    this.errorHistory = [];
    this.maxHistorySize = 1000;
  }
  
  recordError(error, context) {
    const category = ErrorFormatter.categorizeError(error);
    const timestamp = new Date();
    
    // 更新错误计数
    const key = `${category}:${error.extensions?.code || 'UNKNOWN'}`;
    this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);
    
    // 记录错误历史
    const errorRecord = {
      timestamp,
      category,
      code: error.extensions?.code,
      message: error.message,
      path: error.path,
      userId: context.user?.id,
      ip: context.req?.ip
    };
    
    this.errorHistory.push(errorRecord);
    
    // 限制历史记录大小
    if (this.errorHistory.length > this.maxHistorySize) {
      this.errorHistory.shift();
    }
    
    // 检查是否需要告警
    this.checkAlerts(category, errorRecord);
  }
  
  checkAlerts(category, errorRecord) {
    const now = Date.now();
    const fiveMinutesAgo = now - 5 * 60 * 1000;
    
    // 统计最近5分钟的错误
    const recentErrors = this.errorHistory.filter(
      record => record.timestamp.getTime() > fiveMinutesAgo &&
                record.category === category
    );
    
    // 错误率告警阈值
    const thresholds = {
      AUTHENTICATION: 50,
      VALIDATION: 100,
      RATE_LIMIT: 20,
      COMPLEXITY: 10,
      INTERNAL: 5
    };
    
    const threshold = thresholds[category] || 50;
    
    if (recentErrors.length >= threshold) {
      this.sendAlert(category, recentErrors.length, threshold);
    }
  }
  
  async sendAlert(category, count, threshold) {
    const message = `GraphQL Error Alert: ${category} errors exceeded threshold. ` +
                   `Count: ${count}, Threshold: ${threshold}`;
    
    console.error(message);
    
    // 这里可以集成邮件、Slack等告警系统
    // await this.notificationService.sendAlert(message);
  }
  
  getErrorStats() {
    const stats = {
      totalErrors: this.errorHistory.length,
      errorsByCategory: {},
      errorsByCode: {},
      recentErrors: []
    };
    
    // 按类别统计
    for (const record of this.errorHistory) {
      stats.errorsByCategory[record.category] = 
        (stats.errorsByCategory[record.category] || 0) + 1;
      
      if (record.code) {
        stats.errorsByCode[record.code] = 
          (stats.errorsByCode[record.code] || 0) + 1;
      }
    }
    
    // 最近的错误
    const oneHourAgo = Date.now() - 60 * 60 * 1000;
    stats.recentErrors = this.errorHistory
      .filter(record => record.timestamp.getTime() > oneHourAgo)
      .slice(-50); // 最近50个错误
    
    return stats;
  }
}

const errorMonitor = new ErrorMonitor();

module.exports = {
  GraphQLCustomError,
  ResourceNotFoundError,
  DuplicateResourceError,
  RateLimitError,
  QueryComplexityError,
  ErrorFormatter,
  ErrorMonitor,
  errorMonitor
};

测试

1. 单元测试

javascript
// tests/resolvers/userResolvers.test.js
const { createTestClient } = require('apollo-server-testing');
const { ApolloServer, gql } = require('apollo-server-express');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');

const typeDefs = require('../../schema');
const resolvers = require('../../resolvers');
const { User, Post } = require('../../models');

describe('User Resolvers', () => {
  let server;
  let query;
  let mutate;
  let mongoServer;
  
  beforeAll(async () => {
    // 启动内存数据库
    mongoServer = await MongoMemoryServer.create();
    const mongoUri = mongoServer.getUri();
    await mongoose.connect(mongoUri);
    
    // 创建测试服务器
    server = new ApolloServer({
      typeDefs,
      resolvers,
      context: () => ({
        user: null,
        dataSources: {
          userAPI: {
            getUserById: jest.fn()
          }
        }
      })
    });
    
    const testClient = createTestClient(server);
    query = testClient.query;
    mutate = testClient.mutate;
  });
  
  afterAll(async () => {
    await mongoose.connection.close();
    await mongoServer.stop();
  });
  
  beforeEach(async () => {
    // 清理数据库
    await User.deleteMany({});
    await Post.deleteMany({});
  });
  
  describe('Query.user', () => {
    it('should return user by id', async () => {
      // 创建测试用户
      const user = new User({
        username: 'testuser',
        email: 'test@example.com',
        password: 'hashedpassword'
      });
      await user.save();
      
      const GET_USER = gql`
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            username
            email
          }
        }
      `;
      
      const { data, errors } = await query({
        query: GET_USER,
        variables: { id: user._id.toString() }
      });
      
      expect(errors).toBeUndefined();
      expect(data.user).toEqual({
        id: user._id.toString(),
        username: 'testuser',
        email: 'test@example.com'
      });
    });
    
    it('should return null for non-existent user', async () => {
      const GET_USER = gql`
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            username
          }
        }
      `;
      
      const { data, errors } = await query({
        query: GET_USER,
        variables: { id: new mongoose.Types.ObjectId().toString() }
      });
      
      expect(errors).toBeUndefined();
      expect(data.user).toBeNull();
    });
  });
  
  describe('Query.users', () => {
    it('should return paginated users', async () => {
      // 创建测试用户
      const users = await User.create([
        { username: 'user1', email: 'user1@example.com', password: 'hash1' },
        { username: 'user2', email: 'user2@example.com', password: 'hash2' },
        { username: 'user3', email: 'user3@example.com', password: 'hash3' }
      ]);
      
      const GET_USERS = gql`
        query GetUsers($pagination: PaginationInput) {
          users(pagination: $pagination) {
            edges {
              node {
                id
                username
              }
              cursor
            }
            pageInfo {
              hasNextPage
              hasPreviousPage
            }
            totalCount
          }
        }
      `;
      
      const { data, errors } = await query({
        query: GET_USERS,
        variables: {
          pagination: { first: 2 }
        }
      });
      
      expect(errors).toBeUndefined();
      expect(data.users.edges).toHaveLength(2);
      expect(data.users.totalCount).toBe(3);
      expect(data.users.pageInfo.hasNextPage).toBe(true);
    });
  });
  
  describe('Mutation.createUser', () => {
    it('should create a new user', async () => {
      const CREATE_USER = gql`
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            id
            username
            email
          }
        }
      `;
      
      const { data, errors } = await mutate({
        mutation: CREATE_USER,
        variables: {
          input: {
            username: 'newuser',
            email: 'newuser@example.com',
            password: 'password123'
          }
        }
      });
      
      expect(errors).toBeUndefined();
      expect(data.createUser).toMatchObject({
        username: 'newuser',
        email: 'newuser@example.com'
      });
      
      // 验证用户已保存到数据库
      const savedUser = await User.findOne({ username: 'newuser' });
      expect(savedUser).toBeTruthy();
    });
    
    it('should throw error for duplicate username', async () => {
      // 先创建一个用户
      await User.create({
        username: 'existinguser',
        email: 'existing@example.com',
        password: 'hashedpassword'
      });
      
      const CREATE_USER = gql`
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            id
            username
          }
        }
      `;
      
      const { data, errors } = await mutate({
        mutation: CREATE_USER,
        variables: {
          input: {
            username: 'existinguser',
            email: 'different@example.com',
            password: 'password123'
          }
        }
      });
      
      expect(data.createUser).toBeNull();
      expect(errors).toHaveLength(1);
      expect(errors[0].message).toContain('already exists');
    });
  });
});

2. 集成测试

javascript
// tests/integration/graphql.integration.test.js
const request = require('supertest');
const { app } = require('../../server');
const { User, Post, Comment } = require('../../models');
const jwt = require('jsonwebtoken');

describe('GraphQL Integration Tests', () => {
  let authToken;
  let testUser;
  
  beforeEach(async () => {
    // 清理数据库
    await User.deleteMany({});
    await Post.deleteMany({});
    await Comment.deleteMany({});
    
    // 创建测试用户
    testUser = await User.create({
      username: 'testuser',
      email: 'test@example.com',
      password: 'hashedpassword'
    });
    
    // 生成认证令牌
    authToken = jwt.sign(
      { id: testUser._id, username: testUser.username },
      process.env.JWT_SECRET
    );
  });
  
  describe('Authentication', () => {
    it('should require authentication for protected queries', async () => {
      const query = `
        query {
          me {
            id
            username
          }
        }
      `;
      
      const response = await request(app)
        .post('/graphql')
        .send({ query })
        .expect(200);
      
      expect(response.body.errors).toBeDefined();
      expect(response.body.errors[0].message).toContain('logged in');
    });
    
    it('should return user data for authenticated requests', async () => {
      const query = `
        query {
          me {
            id
            username
            email
          }
        }
      `;
      
      const response = await request(app)
        .post('/graphql')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ query })
        .expect(200);
      
      expect(response.body.errors).toBeUndefined();
      expect(response.body.data.me).toMatchObject({
        id: testUser._id.toString(),
        username: 'testuser',
        email: 'test@example.com'
      });
    });
  });
  
  describe('Complex Queries', () => {
    it('should handle nested queries with DataLoader', async () => {
      // 创建测试数据
      const post = await Post.create({
        title: 'Test Post',
        content: 'Test content',
        author: testUser._id,
        published: true
      });
      
      const comment = await Comment.create({
        content: 'Test comment',
        author: testUser._id,
        post: post._id
      });
      
      const query = `
        query {
          posts(pagination: { first: 10 }) {
            edges {
              node {
                id
                title
                author {
                  id
                  username
                }
                comments {
                  id
                  content
                  author {
                    id
                    username
                  }
                }
              }
            }
          }
        }
      `;
      
      const response = await request(app)
        .post('/graphql')
        .send({ query })
        .expect(200);
      
      expect(response.body.errors).toBeUndefined();
      expect(response.body.data.posts.edges).toHaveLength(1);
      
      const postData = response.body.data.posts.edges[0].node;
      expect(postData.title).toBe('Test Post');
      expect(postData.author.username).toBe('testuser');
      expect(postData.comments).toHaveLength(1);
      expect(postData.comments[0].content).toBe('Test comment');
    });
  });
  
  describe('Rate Limiting', () => {
    it('should enforce rate limits', async () => {
      const query = `
        query {
          posts {
            edges {
              node {
                id
                title
              }
            }
          }
        }
      `;
      
      // 发送大量请求
      const requests = Array(150).fill().map(() => 
        request(app)
          .post('/graphql')
          .send({ query })
      );
      
      const responses = await Promise.all(requests);
      
      // 检查是否有请求被限制
      const rateLimitedResponses = responses.filter(
        response => response.body.errors && 
                   response.body.errors.some(error => 
                     error.message.includes('Rate limit')
                   )
      );
      
      expect(rateLimitedResponses.length).toBeGreaterThan(0);
    });
  });
});

最佳实践

1. Schema 设计原则

  • 类型优先设计:先设计 Schema,再实现 Resolver
  • 一致性命名:使用一致的命名约定
  • 适当的抽象:使用接口和联合类型
  • 版本控制:通过字段废弃而非破坏性变更

2. 性能优化

  • 使用 DataLoader:解决 N+1 查询问题
  • 查询复杂度限制:防止恶意查询
  • 缓存策略:合理使用查询缓存
  • 分页实现:使用游标分页

3. 安全考虑

  • 认证和授权:保护敏感数据
  • 输入验证:验证所有输入数据
  • 查询深度限制:防止深度嵌套攻击
  • 速率限制:防止滥用

4. 监控和调试

  • 查询分析:监控查询性能
  • 错误跟踪:记录和分析错误
  • 指标收集:收集关键性能指标
  • 日志记录:详细的操作日志

总结

GraphQL 为 Node.js 应用提供了强大而灵活的 API 解决方案。通过合理的 Schema 设计、高效的 Resolver 实现、完善的缓存策略和安全措施,可以构建出高性能、可扩展的 GraphQL API。

关键要点:

  • 使用 DataLoader 优化数据加载
  • 实现查询复杂度控制
  • 建立完善的错误处理机制
  • 采用适当的缓存策略
  • 确保 API 安全性