- 发布于
GraphQL 实战指南:现代 API 设计的最佳实践
- 作者

- 姓名
- 全能波
- GitHub
- @weicracker
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 开发提供了强大的能力:
- 类型安全:强类型系统确保 API 的可靠性
- 灵活查询:客户端可以精确获取所需数据
- 实时更新:内置订阅支持实时功能
- 开发体验:优秀的工具链和调试支持
- 性能优化:数据加载器和缓存策略
掌握 GraphQL,你就能构建出更高效、更灵活的现代 API!
GraphQL 是现代 API 设计的趋势,值得深入学习和实践。