跳到主要内容

GraphQL

问题

什么是 GraphQL?它与 REST API 有什么区别?在前端项目中如何使用?

答案

GraphQL 是由 Facebook 开发的一种 API 查询语言和运行时。它允许客户端精确指定需要的数据,解决了 REST API 的过度获取(Over-fetching)和获取不足(Under-fetching)问题。


GraphQL vs REST

对比项GraphQLREST
端点单一端点 /graphql多个端点 /users, /posts
数据获取客户端指定服务端决定
请求次数一次获取多个资源可能多次请求
版本控制无需版本(字段演进)URL 版本 /v1/users
类型系统强类型 Schema无内置类型
缓存需要特殊处理HTTP 缓存

核心概念

Schema(模式)

# 定义类型
type User {
id: ID!
name: String!
email: String!
age: Int
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(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}

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

input UpdateUserInput {
name: String
email: String
age: Int
}

# 订阅入口
type Subscription {
postCreated: Post!
}

Query(查询)

# 查询用户列表
query GetUsers {
users {
id
name
email
}
}

# 查询单个用户及其文章
query GetUserWithPosts($userId: ID!) {
user(id: $userId) {
id
name
posts {
id
title
}
}
}

# 一次请求多个资源
query GetDashboard {
currentUser {
id
name
}
recentPosts(limit: 5) {
id
title
}
statistics {
totalUsers
totalPosts
}
}

Mutation(变更)

# 创建用户
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}

# 更新用户
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}

Subscription(订阅)

# 实时订阅新文章
subscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}

服务端实现(Node.js)

Apollo Server

import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import express from 'express';

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

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

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

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

// 模拟数据
const users = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' }
];

const posts = [
{ id: '1', title: 'GraphQL 入门', content: '...', authorId: '1' },
{ id: '2', title: 'TypeScript 进阶', content: '...', authorId: '1' }
];

// 解析器
const resolvers = {
Query: {
users: () => users,
user: (_: unknown, { id }: { id: string }) =>
users.find(user => user.id === id),
posts: () => posts
},

Mutation: {
createUser: (_: unknown, { name, email }: { name: string; email: string }) => {
const user = { id: String(users.length + 1), name, email };
users.push(user);
return user;
},
createPost: (_: unknown, args: { title: string; content: string; authorId: string }) => {
const post = { id: String(posts.length + 1), ...args };
posts.push(post);
return post;
}
},

// 字段解析器
User: {
posts: (parent: { id: string }) =>
posts.filter(post => post.authorId === parent.id)
},

Post: {
author: (parent: { authorId: string }) =>
users.find(user => user.id === parent.authorId)
}
};

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

await server.start();

app.use('/graphql', express.json(), expressMiddleware(server));

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

前端使用

Apollo Client

import { 
ApolloClient,
InMemoryCache,
gql,
useQuery,
useMutation
} from '@apollo/client';

// 创建客户端
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache()
});

// 定义查询
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;

const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
`;

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

React Hooks

import React from 'react';
import { useQuery, useMutation } from '@apollo/client';

// 查询 Hook
function UserList() {
const { loading, error, data, refetch } = useQuery(GET_USERS);

if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;

return (
<ul>
{data.users.map((user: { id: string; name: string }) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

// 带变量的查询
function UserDetail({ userId }: { userId: string }) {
const { loading, error, data } = useQuery(GET_USER, {
variables: { id: userId },
// 缓存策略
fetchPolicy: 'cache-and-network'
});

if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;

const { user } = data;

return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<h3>Posts</h3>
<ul>
{user.posts.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}

// 变更 Hook
function CreateUserForm() {
const [createUser, { loading, error }] = useMutation(CREATE_USER, {
// 更新缓存
update(cache, { data: { createUser } }) {
const existing = cache.readQuery<{ users: Array<{ id: string }> }>({
query: GET_USERS
});
if (existing) {
cache.writeQuery({
query: GET_USERS,
data: { users: [...existing.users, createUser] }
});
}
}
});

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);

await createUser({
variables: {
name: formData.get('name'),
email: formData.get('email')
}
});
};

return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create User'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}

缓存策略

// fetchPolicy 选项
const { data } = useQuery(GET_USERS, {
fetchPolicy: 'cache-first', // 默认:优先缓存
// 'cache-only' // 只用缓存
// 'network-only' // 只用网络
// 'cache-and-network' // 缓存 + 网络
// 'no-cache' // 不缓存
});

// 缓存更新
const [updateUser] = useMutation(UPDATE_USER, {
// 方案1:refetchQueries
refetchQueries: [{ query: GET_USERS }],

// 方案2:update 手动更新
update(cache, { data }) {
cache.modify({
fields: {
users(existingUsers = []) {
return existingUsers.map((user: { __ref: string }) =>
user.__ref === `User:${data.updateUser.id}`
? { ...user }
: user
);
}
}
});
}
});

Fragment 复用

// 定义 Fragment
const USER_FRAGMENT = gql`
fragment UserFields on User {
id
name
email
}
`;

// 使用 Fragment
const GET_USERS_WITH_POSTS = gql`
${USER_FRAGMENT}

query GetUsersWithPosts {
users {
...UserFields
posts {
id
title
}
}
}
`;

错误处理

// 全局错误处理
import { ApolloClient, from, HttpLink } from '@apollo/client';
import { onError } from '@apollo/client/link/error';

const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.error(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
);
});
}

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

const httpLink = new HttpLink({ uri: '/graphql' });

const client = new ApolloClient({
link: from([errorLink, httpLink]),
cache: new InMemoryCache()
});

常见面试问题

Q1: GraphQL 相比 REST 的优缺点?

答案

优点

优点说明
精确获取客户端指定需要的字段
减少请求一次请求获取多个资源
强类型Schema 定义类型,自动校验
自文档Schema 即文档
版本无关添加字段不影响旧客户端

缺点

缺点说明
缓存复杂无法直接使用 HTTP 缓存
学习曲线需要学习新语法
N+1 问题需要 DataLoader 优化
文件上传需要额外处理

Q2: 什么是 N+1 问题?如何解决?

答案

# 查询用户列表及其文章
query {
users { # 1 次查询
posts { # N 次查询(每个用户一次)
title
}
}
}

解决方案:DataLoader

import DataLoader from 'dataloader';

// 批量加载器
const postLoader = new DataLoader(async (userIds: readonly string[]) => {
// 一次查询获取所有用户的文章
const posts = await db.post.findMany({
where: { authorId: { in: [...userIds] } }
});

// 按用户分组
const postsByUser = new Map<string, typeof posts>();
posts.forEach(post => {
const existing = postsByUser.get(post.authorId) || [];
postsByUser.set(post.authorId, [...existing, post]);
});

return userIds.map(id => postsByUser.get(id) || []);
});

// 解析器中使用
const resolvers = {
User: {
posts: (user: { id: string }) => postLoader.load(user.id)
}
};

Q3: GraphQL 如何处理认证?

答案

// 方案1:Context 传递用户信息
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.headers.authorization || '';
const user = verifyToken(token);
return { user };
}
});

// 解析器中验证
const resolvers = {
Mutation: {
createPost: (_, args, context) => {
if (!context.user) {
throw new AuthenticationError('Must be logged in');
}
// ...
}
}
};

// 方案2:Directive 指令
const typeDefs = gql`
directive @auth(requires: Role = USER) on FIELD_DEFINITION

enum Role {
ADMIN
USER
}

type Mutation {
deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
}
`;

Q4: 何时选择 GraphQL,何时选择 REST?

答案

场景推荐
多客户端(Web、App、小程序)GraphQL
数据关系复杂GraphQL
快速迭代GraphQL
简单 CRUDREST
需要 HTTP 缓存REST
文件上传为主REST
团队经验 RESTREST

Q5: 前端如何优化 GraphQL 性能?

答案

// 1. 使用 Fragment 复用
const USER_FRAGMENT = gql`
fragment UserFields on User {
id
name
}
`;

// 2. 合理的 fetchPolicy
useQuery(GET_DATA, {
fetchPolicy: 'cache-and-network'
});

// 3. Pagination
const { data, fetchMore } = useQuery(GET_POSTS, {
variables: { first: 10 }
});

// 加载更多
fetchMore({
variables: { after: data.posts.pageInfo.endCursor }
});

// 4. Persisted Queries(减少传输)
// 将查询存储在服务端,只传 hash

// 5. 延迟加载
const { data } = useQuery(GET_DATA, {
skip: !shouldFetch
});

相关链接