发布于

GraphQL 实战指南:现代 API 设计的最佳实践

作者

GraphQL 实战指南:现代 API 设计的最佳实践

GraphQL 作为一种查询语言和运行时,为 API 设计带来了革命性的变化。本文将深入探讨 GraphQL 的核心概念、实际应用和最佳实践。

GraphQL 基础概念

什么是 GraphQL?

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

# GraphQL 查询示例
query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
    posts {
      id
      title
      content
      createdAt
    }
  }
}

核心概念

# Schema 定义
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  tags: [String!]!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Query {
  user(id: ID!): User
  users(limit: Int, offset: Int): [User!]!
  post(id: ID!): Post
  posts(authorId: ID, tag: String): [Post!]!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!

  createPost(input: CreatePostInput!): Post!
  updatePost(id: ID!, input: UpdatePostInput!): Post!
  deletePost(id: ID!): Boolean!
}

type Subscription {
  postAdded: Post!
  postUpdated(id: ID!): Post!
  userOnline: User!
}

# 输入类型
input CreateUserInput {
  name: String!
  email: String!
}

input UpdateUserInput {
  name: String
  email: String
}

input CreatePostInput {
  title: String!
  content: String!
  authorId: ID!
  tags: [String!]!
}

服务端实现

1. 使用 Apollo Server

const { ApolloServer, gql } = require('apollo-server-express')
const express = require('express')

// 类型定义
const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    createdAt: String!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts: [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createUser(name: String!, email: String!): User!
    createPost(title: String!, content: String!, authorId: ID!): Post!
  }

  type Subscription {
    postAdded: Post!
  }
`

// 解析器
const resolvers = {
  Query: {
    users: async () => {
      return await User.findAll()
    },

    user: async (parent, { id }) => {
      return await User.findByPk(id)
    },

    posts: async (parent, args, context) => {
      const { user } = context
      if (!user) throw new Error('未授权')

      return await Post.findAll({
        order: [['createdAt', 'DESC']],
      })
    },

    post: async (parent, { id }) => {
      return await Post.findByPk(id)
    },
  },

  Mutation: {
    createUser: async (parent, { name, email }) => {
      try {
        const user = await User.create({ name, email })
        return user
      } catch (error) {
        throw new Error(`创建用户失败: ${error.message}`)
      }
    },

    createPost: async (parent, { title, content, authorId }, context) => {
      const { user } = context
      if (!user) throw new Error('未授权')

      const post = await Post.create({
        title,
        content,
        authorId,
      })

      // 发布订阅事件
      pubsub.publish('POST_ADDED', { postAdded: post })

      return post
    },
  },

  Subscription: {
    postAdded: {
      subscribe: () => pubsub.asyncIterator(['POST_ADDED']),
    },
  },

  // 字段解析器
  User: {
    posts: async (parent) => {
      return await Post.findAll({
        where: { authorId: parent.id },
      })
    },
  },

  Post: {
    author: async (parent) => {
      return await User.findByPk(parent.authorId)
    },
  },
}

// 创建服务器
async function startServer() {
  const app = express()

  const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: ({ req }) => {
      // 从请求中提取用户信息
      const token = req.headers.authorization?.replace('Bearer ', '')
      const user = token ? verifyToken(token) : null

      return {
        user,
        dataSources: {
          userAPI: new UserAPI(),
          postAPI: new PostAPI(),
        },
      }
    },
    formatError: (error) => {
      console.error(error)
      return {
        message: error.message,
        code: error.extensions?.code,
        path: error.path,
      }
    },
  })

  await server.start()
  server.applyMiddleware({ app })

  app.listen(4000, () => {
    console.log(`🚀 服务器运行在 http://localhost:4000${server.graphqlPath}`)
  })
}

startServer()

2. 数据源和缓存

const { DataSource } = require('apollo-datasource')
const { RESTDataSource } = require('apollo-datasource-rest')

// REST API 数据源
class UserAPI extends RESTDataSource {
  constructor() {
    super()
    this.baseURL = 'https://api.example.com/'
  }

  willSendRequest(request) {
    request.headers.set('Authorization', this.context.token)
  }

  async getUser(id) {
    return this.get(`users/${id}`)
  }

  async getUsers() {
    return this.get('users')
  }

  async createUser(user) {
    return this.post('users', user)
  }
}

// 数据库数据源
class DatabaseAPI extends DataSource {
  constructor() {
    super()
  }

  initialize(config) {
    this.context = config.context
    this.cache = config.cache
  }

  async getUser(id) {
    const cacheKey = `user-${id}`
    const cached = await this.cache.get(cacheKey)

    if (cached) {
      return JSON.parse(cached)
    }

    const user = await User.findByPk(id)
    await this.cache.set(cacheKey, JSON.stringify(user), { ttl: 300 })

    return user
  }
}

3. 认证和授权

const jwt = require('jsonwebtoken')

// JWT 验证中间件
function verifyToken(token) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET)
  } catch (error) {
    return null
  }
}

// 权限检查指令
const { SchemaDirectiveVisitor } = require('apollo-server-express')

class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field
    const requiredRole = this.args.requires

    field.resolve = async function (...args) {
      const [, , context] = args
      const { user } = context

      if (!user) {
        throw new Error('未授权访问')
      }

      if (requiredRole && user.role !== requiredRole) {
        throw new Error('权限不足')
      }

      return resolve.apply(this, args)
    }
  }
}

// 在 schema 中使用
const typeDefs = gql`
  directive @auth(requires: Role = USER) on FIELD_DEFINITION

  enum Role {
    ADMIN
    USER
  }

  type Query {
    users: [User!]! @auth(requires: ADMIN)
    user(id: ID!): User @auth
  }
`

客户端实现

1. Apollo Client 配置

import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { onError } from '@apollo/client/link/error'

// HTTP 链接
const httpLink = createHttpLink({
  uri: 'http://localhost:4000/graphql',
})

// 认证链接
const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('token')

  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    },
  }
})

// 错误处理链接
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      console.error(`GraphQL error: Message: ${message}, Location: ${locations}, Path: ${path}`)
    })
  }

  if (networkError) {
    console.error(`Network error: ${networkError}`)

    if (networkError.statusCode === 401) {
      // 清除 token 并重定向到登录页
      localStorage.removeItem('token')
      window.location.href = '/login'
    }
  }
})

// 创建 Apollo Client
const client = new ApolloClient({
  link: errorLink.concat(authLink.concat(httpLink)),
  cache: new InMemoryCache({
    typePolicies: {
      User: {
        fields: {
          posts: {
            merge(existing = [], incoming) {
              return [...existing, ...incoming]
            },
          },
        },
      },
    },
  }),
  defaultOptions: {
    watchQuery: {
      errorPolicy: 'all',
    },
    query: {
      errorPolicy: 'all',
    },
  },
})

export default client

2. React Hooks 使用

import { useQuery, useMutation, useSubscription } from '@apollo/client'
import { gql } from '@apollo/client'

// 查询
const GET_USERS = gql`
  query GetUsers($limit: Int, $offset: Int) {
    users(limit: $limit, offset: $offset) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`

// 变更
const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`

// 订阅
const POST_ADDED_SUBSCRIPTION = gql`
  subscription PostAdded {
    postAdded {
      id
      title
      content
      author {
        name
      }
    }
  }
`

// React 组件
function UserList() {
  const { loading, error, data, fetchMore } = useQuery(GET_USERS, {
    variables: { limit: 10, offset: 0 },
    notifyOnNetworkStatusChange: true,
  })

  const [createUser, { loading: creating }] = useMutation(CREATE_USER, {
    update(cache, { data: { createUser } }) {
      const { users } = cache.readQuery({ query: GET_USERS })
      cache.writeQuery({
        query: GET_USERS,
        data: { users: [createUser, ...users] },
      })
    },
  })

  const { data: subscriptionData } = useSubscription(POST_ADDED_SUBSCRIPTION, {
    onSubscriptionData: ({ subscriptionData }) => {
      console.log('新文章:', subscriptionData.data.postAdded)
    },
  })

  const handleCreateUser = async (userData) => {
    try {
      await createUser({
        variables: userData,
        optimisticResponse: {
          createUser: {
            __typename: 'User',
            id: 'temp-id',
            ...userData,
          },
        },
      })
    } catch (error) {
      console.error('创建用户失败:', error)
    }
  }

  const loadMore = () => {
    fetchMore({
      variables: {
        offset: data.users.length,
      },
    })
  }

  if (loading) return <div>加载中...</div>
  if (error) return <div>错误: {error.message}</div>

  return (
    <div>
      {data.users.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
      <button onClick={loadMore}>加载更多</button>
    </div>
  )
}

3. 缓存策略

// 缓存配置
const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        posts: {
          keyArgs: ['authorId', 'tag'],
          merge(existing = [], incoming, { args }) {
            if (args?.offset === 0) {
              return incoming
            }
            return [...existing, ...incoming]
          },
        },
      },
    },

    User: {
      fields: {
        posts: relayStylePagination(),
      },
    },
  },
})

// 手动缓存更新
const [deletePost] = useMutation(DELETE_POST, {
  update(cache, { data: { deletePost } }) {
    cache.modify({
      fields: {
        posts(existingPosts, { readField }) {
          return existingPosts.filter((postRef) => deletePost.id !== readField('id', postRef))
        },
      },
    })
  },
})

高级特性

1. 数据加载器(DataLoader)

const DataLoader = require('dataloader')

// 批量加载用户
const userLoader = new DataLoader(async (userIds) => {
  const users = await User.findAll({
    where: { id: userIds },
  })

  // 确保返回顺序与输入顺序一致
  return userIds.map((id) => users.find((user) => user.id === id))
})

// 在解析器中使用
const resolvers = {
  Post: {
    author: async (parent, args, { loaders }) => {
      return loaders.user.load(parent.authorId)
    },
  },
}

2. 查询复杂度分析

const depthLimit = require('graphql-depth-limit')
const costAnalysis = require('graphql-cost-analysis')

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(10), // 限制查询深度
    costAnalysis({
      maximumCost: 1000,
      defaultCost: 1,
      scalarCost: 1,
      objectCost: 2,
      listFactor: 10,
      introspectionCost: 1000,
      createError: (max, actual) => {
        return new Error(`查询成本过高: ${actual}, 最大允许: ${max}`)
      },
    }),
  ],
})

3. 实时订阅

const { PubSub } = require('graphql-subscriptions')
const { withFilter } = require('graphql-subscriptions')

const pubsub = new PubSub()

const resolvers = {
  Subscription: {
    postUpdated: {
      subscribe: withFilter(
        () => pubsub.asyncIterator('POST_UPDATED'),
        (payload, variables) => {
          return payload.postUpdated.id === variables.id
        }
      ),
    },

    commentAdded: {
      subscribe: withFilter(
        () => pubsub.asyncIterator('COMMENT_ADDED'),
        (payload, variables, context) => {
          // 只有文章作者才能收到评论通知
          return payload.commentAdded.post.authorId === context.user.id
        }
      ),
    },
  },
}

// 在变更中触发订阅
const updatePost = async (parent, { id, input }) => {
  const post = await Post.update(input, { where: { id } })

  pubsub.publish('POST_UPDATED', { postUpdated: post })

  return post
}

性能优化

1. 查询优化

// N+1 问题解决
const resolvers = {
  Query: {
    posts: async () => {
      return await Post.findAll({
        include: [{ model: User, as: 'author' }], // 预加载关联数据
      })
    },
  },

  Post: {
    author: (parent) => {
      // 如果已经预加载,直接返回
      return parent.author || userLoader.load(parent.authorId)
    },
  },
}

// 字段级缓存
const resolvers = {
  User: {
    posts: async (parent, args, context, info) => {
      const cacheKey = `user-${parent.id}-posts`
      const cached = await context.cache.get(cacheKey)

      if (cached) {
        return JSON.parse(cached)
      }

      const posts = await Post.findAll({
        where: { authorId: parent.id },
      })

      await context.cache.set(cacheKey, JSON.stringify(posts), { ttl: 300 })

      return posts
    },
  },
}

2. 分页实现

// 游标分页
const typeDefs = gql`
  type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
  }

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

  type PostEdge {
    node: Post!
    cursor: String!
  }

  type Query {
    posts(first: Int, after: String, last: Int, before: String): PostConnection!
  }
`

const resolvers = {
  Query: {
    posts: async (parent, { first, after, last, before }) => {
      const limit = first || last || 10
      const offset = after ? parseInt(Buffer.from(after, 'base64').toString()) : 0

      const posts = await Post.findAndCountAll({
        limit: limit + 1, // 多查一个判断是否有下一页
        offset,
        order: [['createdAt', 'DESC']],
      })

      const hasNextPage = posts.rows.length > limit
      const edges = posts.rows.slice(0, limit).map((post, index) => ({
        node: post,
        cursor: Buffer.from((offset + index).toString()).toString('base64'),
      }))

      return {
        edges,
        pageInfo: {
          hasNextPage,
          hasPreviousPage: offset > 0,
          startCursor: edges[0]?.cursor,
          endCursor: edges[edges.length - 1]?.cursor,
        },
        totalCount: posts.count,
      }
    },
  },
}

总结

GraphQL 为现代 API 开发提供了强大的能力:

  1. 类型安全:强类型系统确保 API 的可靠性
  2. 灵活查询:客户端可以精确获取所需数据
  3. 实时更新:内置订阅支持实时功能
  4. 开发体验:优秀的工具链和调试支持
  5. 性能优化:数据加载器和缓存策略

掌握 GraphQL,你就能构建出更高效、更灵活的现代 API!


GraphQL 是现代 API 设计的趋势,值得深入学习和实践。