GraphQL Yoga 中文教程

1. 核心概念

GraphQL Yoga 是一个功能强大、易于使用的 GraphQL 服务器库,由 The Guild 团队开发和维护。它基于 Envelop 和 GraphQL Helix,提供了一个现代化、高性能的 GraphQL 服务器实现。

1.1 什么是 GraphQL Yoga?

GraphQL Yoga 是一个全功能的 GraphQL 服务器库,提供了:

  • 简单直观的 API
  • 内置的 GraphQL Playground
  • 支持多种数据源
  • 错误处理和日志记录
  • 性能优化
  • 与各种前端框架的集成

1.2 为什么选择 GraphQL Yoga?

  • 简单易用:简洁的 API 设计,易于上手
  • 功能完整:内置了许多实用功能,如 GraphQL Playground、CORS 支持等
  • 性能优异:基于 Envelop 和 GraphQL Helix,提供了高性能的执行引擎
  • 可扩展:通过插件系统支持各种扩展
  • 生态系统:与其他 The Guild 项目无缝集成

2. 安装与配置

2.1 基本安装

在 Node.js 项目中安装 GraphQL Yoga:

npm install graphql-yoga

2.2 基本配置

创建一个简单的 GraphQL Yoga 服务器:

import { createYoga, createSchema } from 'graphql-yoga';
import { createServer } from 'node:http';

// 定义 schema
const schema = createSchema({
  typeDefs: `
    type Query {
      hello: String
      user(id: ID!): User
    }
    
    type User {
      id: ID!
      name: String!
      email: String!
    }
  `,
  resolvers: {
    Query: {
      hello: () => 'Hello world!',
      user: (_, { id }) => {
        // 模拟从数据库获取用户
        return {
          id,
          name: 'John Doe',
          email: 'john@example.com'
        };
      }
    }
  }
});

// 创建 Yoga 实例
const yoga = createYoga({
  schema,
});

// 创建 HTTP 服务器
const server = createServer(yoga);

// 启动服务器
server.listen(4000, () => {
  console.log('Server running on http://localhost:4000/graphql');
});

2.3 集成到现有框架

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

2.3.1 与 Express 集成

npm install express graphql-yoga
import express from 'express';
import { createYoga, createSchema } from 'graphql-yoga';

const app = express();

// 定义 schema
const schema = createSchema({
  typeDefs: `
    type Query {
      hello: String
    }
  `,
  resolvers: {
    Query: {
      hello: () => 'Hello world!'
    }
  }
});

// 创建 Yoga 实例
const yoga = createYoga({
  schema,
  // 禁用默认的 HTTP 服务器
  graphiql: { endpoint: '/graphql' }
});

// 将 Yoga 作为中间件使用
app.use('/graphql', yoga);

// 启动服务器
app.listen(4000, () => {
  console.log('Server running on http://localhost:4000/graphql');
});

3. 基本使用

3.1 定义 Schema

使用 SDL(Schema Definition Language)定义 schema:

import { createSchema } from 'graphql-yoga';

const schema = createSchema({
  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!
    }
  `,
  resolvers: {
    // 解析器实现
  }
});

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 schema = createSchema({
  typeDefs: /* 省略 */,
  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 插件系统

GraphQL Yoga 使用 Envelop 插件系统,支持各种扩展:

import { createYoga, createSchema } from 'graphql-yoga';
import { useLogger, useErrorHandler, useCors } from '@envelop/core';

const schema = createSchema({
  typeDefs: `
    type Query {
      hello: String
    }
  `,
  resolvers: {
    Query: {
      hello: () => 'Hello world!'
    }
  }
});

const yoga = createYoga({
  schema,
  plugins: [
    // 使用日志插件
    useLogger(),
    // 使用错误处理插件
    useErrorHandler((error) => {
      console.error('GraphQL Error:', error);
    }),
    // 使用 CORS 插件
    useCors({
      origin: '*',
      methods: ['POST', 'GET']
    })
  ]
});

4.2 上下文创建

在 GraphQL Yoga 中,可以通过 context 函数创建上下文:

const yoga = createYoga({
  schema,
  context: async ({ request, params }) => {
    // 从请求头获取认证信息
    const authHeader = request.headers.get('authorization');
    let user = null;
    
    if (authHeader) {
      const token = authHeader.split(' ')[1];
      // 验证 token 并获取用户信息
      // 这里只是示例,实际应用中需要实现真实的验证逻辑
      user = { id: '1', name: 'John Doe' };
    }
    
    return {
      user,
      // 可以添加其他上下文信息
      db: { /* 数据库连接 */ }
    };
  }
});

4.3 文件上传

GraphQL Yoga 内置支持文件上传:

import { createYoga, createSchema } from 'graphql-yoga';

const schema = createSchema({
  typeDefs: `
    scalar Upload
    
    type Query {
      hello: String
    }
    
    type Mutation {
      uploadFile(file: Upload!): String!
    }
  `,
  resolvers: {
    Upload: require('graphql-upload').GraphQLUpload,
    Mutation: {
      uploadFile: async (_, { file }) => {
        const { createReadStream, filename } = await file;
        
        // 处理文件上传
        // 这里只是示例,实际应用中需要实现真实的文件处理逻辑
        return `File ${filename} uploaded successfully`;
      }
    }
  }
});

const yoga = createYoga({
  schema,
  // 启用文件上传
  multipart: true
});

4.4 订阅(Subscriptions)

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

import { createYoga, createSchema } from 'graphql-yoga';
import { createServer } from 'node:http';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

const schema = createSchema({
  typeDefs: `
    type Query {
      message: String
    }
    
    type Mutation {
      sendMessage(content: String!): Message!
    }
    
    type Subscription {
      messageAdded: Message!
    }
    
    type Message {
      id: ID!
      content: String!
    }
  `,
  resolvers: {
    Query: {
      message: () => 'Hello from subscription!',
    },
    Mutation: {
      sendMessage: (_, { content }) => {
        const newMessage = {
          id: String(Date.now()),
          content,
        };
        
        // 发布消息
        pubsub.publish('MESSAGE_ADDED', { messageAdded: newMessage });
        
        return newMessage;
      },
    },
    Subscription: {
      messageAdded: {
        subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED']),
      },
    },
  }
});

const yoga = createYoga({
  schema,
});

const server = createServer(yoga);

server.listen(4000, () => {
  console.log('Server running on http://localhost:4000/graphql');
});

5. 最佳实践

5.1 项目结构

推荐的 GraphQL Yoga 项目结构:

├── src/
│   ├── schema/             # Schema 定义
│   │   ├── typeDefs.js     # 类型定义
│   │   └── resolvers/      # 解析器
│   │       ├── Query.js    # 查询解析器
│   │       ├── Mutation.js # 变更解析器
│   │       └── Subscription.js # 订阅解析器
│   ├── utils/              # 工具函数
│   ├── context.js          # 上下文创建
│   ├── plugins/            # 自定义插件
│   └── server.js           # 服务器配置
├── package.json
└── .env

5.2 性能优化

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

5.3 安全性

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

6. 实用案例

6.1 构建博客 API

6.1.1 Schema 定义

import { createSchema } from 'graphql-yoga';

const schema = createSchema({
  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!
    }
  `,
  resolvers: {
    // 解析器实现
  }
});

6.1.2 解析器实现

import { PubSub } from 'graphql-subscriptions';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';

const pubsub = new PubSub();

// 模拟数据库
const users = [];
const articles = [];

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: () => pubsub.asyncIterator(['ARTICLE_CREATED']),
    },
  },
  Article: {
    author: (parent) => users.find(user => user.id === parent.authorId),
  },
  User: {
    articles: (parent) => articles.filter(article => article.authorId === parent.id),
  },
};

6.1.3 上下文创建

import jwt from 'jsonwebtoken';

function createContext({ request }) {
  const context = {};
  
  // 从请求头获取 token
  const authHeader = request.headers.get('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;
}

const yoga = createYoga({
  schema,
  context: createContext
});

6.2 集成数据库

6.2.1 与 MongoDB 集成

npm install mongoose
import mongoose from '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. 总结

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

  • GraphQL Yoga 的核心概念和基本原理
  • 如何安装和配置 GraphQL Yoga
  • 如何定义 schema 和实现解析器
  • 如何使用 GraphQL Yoga 的高级特性,如插件系统、上下文创建、文件上传和订阅
  • 最佳实践和实用案例

GraphQL Yoga 基于 Envelop 和 GraphQL Helix,提供了高性能、可扩展的 GraphQL 服务器实现。无论是构建小型应用还是大型企业系统,GraphQL Yoga 都能满足你的需求。

8. 参考资料

« 上一篇 Apollo Server 中文教程 下一篇 » Fastify GQL 中文教程