GraphQL
问题
什么是 GraphQL?它与 REST API 有什么区别?在前端项目中如何使用?
答案
GraphQL 是由 Facebook 开发的一种 API 查询语言和运行时。它允许客户端精确指定需要的数据,解决了 REST API 的过度获取(Over-fetching)和获取不足(Under-fetching)问题。
GraphQL vs REST
| 对比项 | GraphQL | REST |
|---|---|---|
| 端点 | 单一端点 /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 |
| 简单 CRUD | REST |
| 需要 HTTP 缓存 | REST |
| 文件上传为主 | REST |
| 团队经验 REST | REST |
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
});