Apollo Server 中文教程

1. 核心概念

Apollo Server 是一个功能强大的 GraphQL 服务器实现,由 Apollo 团队开发和维护。它允许开发者快速构建和部署 GraphQL API,支持多种集成方式和高级特性。

1.1 什么是 Apollo Server?

Apollo Server 是一个开源的 GraphQL 服务器,提供了完整的 GraphQL 规范实现,包括:

  • schema 定义和验证
  • 解析器(resolvers)的实现
  • 数据源集成
  • 错误处理
  • 性能优化
  • 与各种前端框架的集成

1.2 为什么选择 Apollo Server?

  • 易于集成:支持多种 Node.js 框架和环境
  • 功能丰富:内置了许多高级特性,如缓存、批处理、订阅等
  • 性能优异:优化的执行引擎,支持复杂查询的高效处理
  • 生态完整:与 Apollo Client 无缝配合,形成完整的 GraphQL 解决方案
  • 社区活跃:拥有庞大的社区和丰富的文档资源

2. 安装与配置

2.1 基本安装

在 Node.js 项目中安装 Apollo Server:

npm install @apollo/server graphql

2.2 基本配置

创建一个简单的 Apollo Server 实例:

const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');

// 定义 schema
const typeDefs = `
  type Query {
    hello: String
    user(id: ID!): User
  }
  
  type User {
    id: ID!
    name: String!
    email: String!
  }
`;

// 定义解析器
const resolvers = {
  Query: {
    hello: () => 'Hello world!',
    user: (_, { id }) => {
      // 模拟从数据库获取用户
      return {
        id,
        name: 'John Doe',
        email: 'john@example.com'
      };
    }
  }
};

// 创建服务器实例
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// 启动服务器
async function startServer() {
  const { url } = await startStandaloneServer(server, {
    listen: { port: 4000 },
  });
  console.log(`Server ready at ${url}`);
}

startServer();

2.3 集成到现有框架

Apollo Server 支持与多种 Node.js 框架集成,如 Express、Fastify、Koa 等。

2.3.1 与 Express 集成

npm install @apollo/server express express-graphql graphql http cors
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const { ApolloServerPluginDrainHttpServer } = require('@apollo/server/plugin/drainHttpServer');
const express = require('express');
const http = require('http');
const cors = require('cors');

const app = express();
const httpServer = http.createServer(app);

// 定义 schema 和解析器
const typeDefs = `
  type Query {
    hello: String
  }
`;

const resolvers = {
  Query: {
    hello: () => 'Hello world!',
  },
};

// 创建服务器实例
const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});

async function startServer() {
  await server.start();
  
  app.use(
    '/graphql',
    cors(),
    express.json(),
    expressMiddleware(server),
  );
  
  await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));
  console.log(`Server ready at http://localhost:4000/graphql`);
}

startServer();

3. 基本使用

3.1 定义 Schema

Schema 是 GraphQL API 的核心,定义了可用的类型和操作。使用 SDL(Schema Definition Language)来定义:

const typeDefs = `
  # 定义查询类型
  type Query {
    # 获取所有用户
    users: [User!]!
    # 根据 ID 获取用户
    user(id: ID!): User
    # 获取所有帖子
    posts: [Post!]!
    # 根据 ID 获取帖子
    post(id: ID!): Post
  }
  
  # 定义变更类型
  type Mutation {
    # 创建用户
    createUser(name: String!, email: String!): User!
    # 更新用户
    updateUser(id: ID!, name: String, email: String): User!
    # 删除用户
    deleteUser(id: ID!): Boolean!
    # 创建帖子
    createPost(title: String!, content: String!, authorId: ID!): Post!
  }
  
  # 定义用户类型
  type User {
    id: ID!
    name: String!
    email: String!
    # 用户的帖子
    posts: [Post!]!
  }
  
  # 定义帖子类型
  type Post {
    id: ID!
    title: String!
    content: String!
    authorId: ID!
    # 帖子的作者
    author: User!
  }
`;

3.2 实现解析器

解析器负责处理 GraphQL 查询,返回相应的数据:

// 模拟数据库
const users = [
  { id: '1', name: 'John Doe', email: 'john@example.com' },
  { id: '2', name: 'Jane Smith', email: 'jane@example.com' }
];

const posts = [
  { id: '1', title: 'First Post', content: 'Hello world!', authorId: '1' },
  { id: '2', title: 'Second Post', content: 'GraphQL is awesome!', authorId: '2' }
];

const resolvers = {
  Query: {
    users: () => users,
    user: (_, { id }) => users.find(user => user.id === id),
    posts: () => posts,
    post: (_, { id }) => posts.find(post => post.id === id)
  },
  Mutation: {
    createUser: (_, { name, email }) => {
      const newUser = {
        id: String(users.length + 1),
        name,
        email
      };
      users.push(newUser);
      return newUser;
    },
    updateUser: (_, { id, name, email }) => {
      const user = users.find(user => user.id === id);
      if (!user) throw new Error('User not found');
      
      if (name) user.name = name;
      if (email) user.email = email;
      
      return user;
    },
    deleteUser: (_, { id }) => {
      const index = users.findIndex(user => user.id === id);
      if (index === -1) throw new Error('User not found');
      
      users.splice(index, 1);
      return true;
    },
    createPost: (_, { title, content, authorId }) => {
      const newPost = {
        id: String(posts.length + 1),
        title,
        content,
        authorId
      };
      posts.push(newPost);
      return newPost;
    }
  },
  User: {
    posts: (parent) => posts.filter(post => post.authorId === parent.id)
  },
  Post: {
    author: (parent) => users.find(user => user.id === parent.authorId)
  }
};

3.3 执行查询

启动服务器后,可以通过 GraphQL Playground 或其他客户端工具执行查询:

3.3.1 查询所有用户

query GetAllUsers {
  users {
    id
    name
    email
    posts {
      id
      title
    }
  }
}

3.3.2 创建用户

mutation CreateUser {
  createUser(name: "Bob Brown", email: "bob@example.com") {
    id
    name
    email
  }
}

4. 高级特性

4.1 数据源集成

Apollo Server 支持多种数据源集成,如 REST APIs、数据库等。

4.1.1 使用 RESTDataSource

npm install @apollo/datasource-rest
const { RESTDataSource } = require('@apollo/datasource-rest');

class UserAPI extends RESTDataSource {
  baseURL = 'https://api.example.com';
  
  async getUsers() {
    return this.get('/users');
  }
  
  async getUserById(id) {
    return this.get(`/users/${id}`);
  }
  
  async createUser(userData) {
    return this.post('/users', userData);
  }
}

// 在服务器配置中使用
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  context: async () => ({
    dataSources: {
      userAPI: new UserAPI(),
    },
  }),
});

// 在解析器中使用
const resolvers = {
  Query: {
    users: async (_, __, { dataSources }) => {
      return dataSources.userAPI.getUsers();
    },
    user: async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    },
  },
  Mutation: {
    createUser: async (_, { name, email }, { dataSources }) => {
      return dataSources.userAPI.createUser({ name, email });
    },
  },
};

4.2 订阅(Subscriptions)

Apollo Server 支持 GraphQL 订阅,实现实时数据更新:

npm install @apollo/server graphql-subscriptions subscriptions-transport-ws
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const { ApolloServerPluginDrainHttpServer } = require('@apollo/server/plugin/drainHttpServer');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const express = require('express');
const http = require('http');
const cors = require('cors');
const { makeExecutableSchema } = require('@graphql-tools/schema');

const app = express();
const httpServer = http.createServer(app);

// 创建 WebSocket 服务器
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql',
});

// 定义 schema
const typeDefs = `
  type Query {
    message: String
  }
  
  type Mutation {
    sendMessage(content: String!): Message!
  }
  
  type Subscription {
    messageAdded: Message!
  }
  
  type Message {
    id: ID!
    content: String!
  }
`;

// 模拟消息存储
const messages = [];
// 订阅发布器
const pubsub = {
  subscriptions: new Map(),
  
  publish(channel, payload) {
    if (this.subscriptions.has(channel)) {
      this.subscriptions.get(channel).forEach(callback => callback(payload));
    }
  },
  
  subscribe(channel, callback) {
    if (!this.subscriptions.has(channel)) {
      this.subscriptions.set(channel, new Set());
    }
    this.subscriptions.get(channel).add(callback);
    
    return () => {
      this.subscriptions.get(channel).delete(callback);
    };
  },
};

// 定义解析器
const resolvers = {
  Query: {
    message: () => 'Hello from subscription!',
  },
  Mutation: {
    sendMessage: (_, { content }) => {
      const newMessage = {
        id: String(messages.length + 1),
        content,
      };
      messages.push(newMessage);
      
      // 发布消息
      pubsub.publish('MESSAGE_ADDED', { messageAdded: newMessage });
      
      return newMessage;
    },
  },
  Subscription: {
    messageAdded: {
      subscribe: () => {
        return {
          [Symbol.asyncIterator]: async function* () {
            const queue = [];
            const unsubscribe = pubsub.subscribe('MESSAGE_ADDED', (payload) => {
              queue.push(payload);
            });
            
            try {
              while (true) {
                if (queue.length > 0) {
                  yield queue.shift();
                } else {
                  await new Promise(resolve => setTimeout(resolve, 100));
                }
              }
            } finally {
              unsubscribe();
            }
          },
        };
      },
    },
  },
};

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

// 启动 WebSocket 服务器
const serverCleanup = useServer({ schema }, wsServer);

// 创建 Apollo Server 实例
const server = new ApolloServer({
  schema,
  plugins: [
    ApolloServerPluginDrainHttpServer({ httpServer }),
    {
      async serverWillStart() {
        return {
          async drainServer() {
            await serverCleanup.dispose();
          },
        };
      },
    },
  ],
});

async function startServer() {
  await server.start();
  
  app.use(
    '/graphql',
    cors(),
    express.json(),
    expressMiddleware(server),
  );
  
  await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));
  console.log(`Server ready at http://localhost:4000/graphql`);
}

startServer();

4.3 缓存

Apollo Server 提供了多种缓存策略,提高 API 性能:

4.3.1 响应缓存

const { ApolloServer } = require('@apollo/server');
const responseCachePlugin = require('@apollo/server-plugin-response-cache').default;

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    responseCachePlugin({
      // 缓存配置
      sessionId: (requestContext) => requestContext.request.http.headers.get('session-id') || null,
    }),
  ],
});

4.4 错误处理

Apollo Server 提供了灵活的错误处理机制:

const { ApolloServer } = require('@apollo/server');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (err) => {
    // 自定义错误格式化
    return {
      message: err.message,
      code: err.extensions?.code || 'INTERNAL_ERROR',
      stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
    };
  },
});

5. 最佳实践

5.1 项目结构

推荐的 Apollo Server 项目结构:

├── src/
│   ├── schema/             # Schema 定义
│   │   ├── index.js        # Schema 导出
│   │   ├── types.js        # 类型定义
│   │   └── directives.js   # 自定义指令
│   ├── resolvers/          # 解析器
│   │   ├── index.js        # 解析器导出
│   │   ├── Query.js        # 查询解析器
│   │   ├── Mutation.js     # 变更解析器
│   │   └── Subscription.js # 订阅解析器
│   ├── datasources/        # 数据源
│   │   ├── UserAPI.js      # 用户 API 数据源
│   │   └── PostAPI.js      # 帖子 API 数据源
│   ├── utils/              # 工具函数
│   ├── context.js          # 上下文创建
│   └── server.js           # 服务器配置
├── package.json
└── .env

5.2 性能优化

  • 使用数据加载器(DataLoader):避免 N+1 查询问题
  • 实现缓存:减少重复请求
  • 优化解析器:避免在解析器中执行昂贵操作
  • 使用分页:限制大型数据集的返回
  • 监控和分析:使用 Apollo Studio 监控 API 性能

5.3 安全性

  • 验证和授权:实现用户认证和权限控制
  • 输入验证:验证所有用户输入
  • 速率限制:防止 API 滥用
  • 安全头部:设置适当的安全 HTTP 头部
  • 使用 HTTPS:在生产环境中使用 HTTPS

6. 实用案例

6.1 构建博客 API

6.1.1 Schema 定义

const typeDefs = `
  type Query {
    # 获取所有文章
    articles: [Article!]!
    # 根据 ID 获取文章
    article(id: ID!): Article
    # 获取所有用户
    users: [User!]!
    # 根据 ID 获取用户
    user(id: ID!): User
  }
  
  type Mutation {
    # 创建文章
    createArticle(title: String!, content: String!, authorId: ID!): Article!
    # 更新文章
    updateArticle(id: ID!, title: String, content: String): Article!
    # 删除文章
    deleteArticle(id: ID!): Boolean!
    # 创建用户
    createUser(name: String!, email: String!, password: String!): User!
    # 用户登录
    login(email: String!, password: String!): Token!
  }
  
  type Subscription {
    # 文章创建订阅
    articleCreated: Article!
  }
  
  type Article {
    id: ID!
    title: String!
    content: String!
    createdAt: String!
    updatedAt: String!
    authorId: ID!
    author: User!
  }
  
  type User {
    id: ID!
    name: String!
    email: String!
    createdAt: String!
    articles: [Article!]!
  }
  
  type Token {
    token: String!
    user: User!
  }
`;

6.1.2 解析器实现

const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

// 模拟数据库
const users = [];
const articles = [];
const pubsub = {
  subscriptions: new Map(),
  
  publish(channel, payload) {
    if (this.subscriptions.has(channel)) {
      this.subscriptions.get(channel).forEach(callback => callback(payload));
    }
  },
  
  subscribe(channel, callback) {
    if (!this.subscriptions.has(channel)) {
      this.subscriptions.set(channel, new Set());
    }
    this.subscriptions.get(channel).add(callback);
    
    return () => {
      this.subscriptions.get(channel).delete(callback);
    };
  },
};

const resolvers = {
  Query: {
    articles: () => articles,
    article: (_, { id }) => articles.find(article => article.id === id),
    users: () => users,
    user: (_, { id }) => users.find(user => user.id === id),
  },
  Mutation: {
    createArticle: (_, { title, content, authorId }, context) => {
      // 验证用户是否登录
      if (!context.user) throw new Error('Unauthorized');
      
      const newArticle = {
        id: String(articles.length + 1),
        title,
        content,
        authorId,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      };
      articles.push(newArticle);
      
      // 发布文章创建事件
      pubsub.publish('ARTICLE_CREATED', { articleCreated: newArticle });
      
      return newArticle;
    },
    updateArticle: (_, { id, title, content }, context) => {
      // 验证用户是否登录
      if (!context.user) throw new Error('Unauthorized');
      
      const article = articles.find(article => article.id === id);
      if (!article) throw new Error('Article not found');
      
      // 验证用户是否是文章作者
      if (article.authorId !== context.user.id) throw new Error('Forbidden');
      
      if (title) article.title = title;
      if (content) article.content = content;
      article.updatedAt = new Date().toISOString();
      
      return article;
    },
    deleteArticle: (_, { id }, context) => {
      // 验证用户是否登录
      if (!context.user) throw new Error('Unauthorized');
      
      const index = articles.findIndex(article => article.id === id);
      if (index === -1) throw new Error('Article not found');
      
      const article = articles[index];
      // 验证用户是否是文章作者
      if (article.authorId !== context.user.id) throw new Error('Forbidden');
      
      articles.splice(index, 1);
      return true;
    },
    createUser: async (_, { name, email, password }) => {
      // 检查邮箱是否已存在
      if (users.some(user => user.email === email)) {
        throw new Error('Email already exists');
      }
      
      // 加密密码
      const hashedPassword = await bcrypt.hash(password, 10);
      
      const newUser = {
        id: String(users.length + 1),
        name,
        email,
        password: hashedPassword,
        createdAt: new Date().toISOString(),
      };
      users.push(newUser);
      
      return newUser;
    },
    login: async (_, { email, password }) => {
      // 查找用户
      const user = users.find(user => user.email === email);
      if (!user) throw new Error('Invalid credentials');
      
      // 验证密码
      const isValid = await bcrypt.compare(password, user.password);
      if (!isValid) throw new Error('Invalid credentials');
      
      // 生成 JWT token
      const token = jwt.sign({ id: user.id }, 'secret_key', { expiresIn: '1h' });
      
      return {
        token,
        user,
      };
    },
  },
  Subscription: {
    articleCreated: {
      subscribe: () => {
        return {
          [Symbol.asyncIterator]: async function* () {
            const queue = [];
            const unsubscribe = pubsub.subscribe('ARTICLE_CREATED', (payload) => {
              queue.push(payload);
            });
            
            try {
              while (true) {
                if (queue.length > 0) {
                  yield queue.shift();
                } else {
                  await new Promise(resolve => setTimeout(resolve, 100));
                }
              }
            } finally {
              unsubscribe();
            }
          },
        };
      },
    },
  },
  Article: {
    author: (parent) => users.find(user => user.id === parent.authorId),
  },
  User: {
    articles: (parent) => articles.filter(article => article.authorId === parent.id),
  },
};

6.1.3 上下文创建

const jwt = require('jsonwebtoken');

function createContext({ req }) {
  const context = {};
  
  // 从请求头获取 token
  const authHeader = req.headers.authorization;
  if (authHeader) {
    const token = authHeader.split(' ')[1];
    try {
      const user = jwt.verify(token, 'secret_key');
      context.user = user;
    } catch (error) {
      console.error('Invalid token:', error);
    }
  }
  
  return context;
}

6.2 集成数据库

6.2.1 与 MongoDB 集成

npm install mongoose
const mongoose = require('mongoose');

// 连接数据库
mongoose.connect('mongodb://localhost:27017/blog', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

// 定义模型
const User = mongoose.model('User', new mongoose.Schema({
  name: String,
  email: String,
  password: String,
}));

const Post = mongoose.model('Post', new mongoose.Schema({
  title: String,
  content: String,
  authorId: mongoose.Schema.Types.ObjectId,
}));

// 解析器
const resolvers = {
  Query: {
    users: async () => await User.find(),
    user: async (_, { id }) => await User.findById(id),
    posts: async () => await Post.find(),
    post: async (_, { id }) => await Post.findById(id),
  },
  Mutation: {
    createUser: async (_, { name, email, password }) => {
      const user = new User({ name, email, password });
      return await user.save();
    },
    createPost: async (_, { title, content, authorId }) => {
      const post = new Post({ title, content, authorId });
      return await post.save();
    },
  },
  User: {
    posts: async (parent) => await Post.find({ authorId: parent.id }),
  },
  Post: {
    author: async (parent) => await User.findById(parent.authorId),
  },
};

7. 总结

Apollo Server 是一个功能强大、灵活且易于使用的 GraphQL 服务器实现,为开发者提供了构建现代 API 的完整解决方案。通过本教程的学习,你应该已经掌握了:

  • Apollo Server 的核心概念和基本原理
  • 如何安装和配置 Apollo Server
  • 如何定义 schema 和实现解析器
  • 如何使用 Apollo Server 的高级特性,如订阅、数据源集成和缓存
  • 最佳实践和实用案例

Apollo Server 与 Apollo Client 一起,构成了完整的 GraphQL 生态系统,为前端和后端开发提供了统一的数据层解决方案。无论是构建小型应用还是大型企业系统,Apollo Server 都能满足你的需求。

8. 参考资料

« 上一篇 Hasura 教程 下一篇 » GraphQL Yoga 中文教程