为什么前端需要 GraphQL?
如果你经历过 REST API 的“接口爆炸”和“过度获取”,一定深有体会:一个页面需要调用 3~5 个接口,返回的数据字段可能只用了一半,或者为了拿一个字段不得不请求整个对象。GraphQL 让前端可以精确声明需要的数据,一次请求获取所有资源,极大提升了开发效率和性能。
但 GraphQL 在前端落地并非只是换个请求库那么简单。本文将通过一个 Todo 应用,手把手教你如何设计 Schema、集成 Apollo Client、处理缓存与错误,并分享我在生产环境中总结的最佳实践。
技术栈选择
- 前端:React 18 + TypeScript
- GraphQL 客户端:Apollo Client 3
- 后端:Node.js + Apollo Server(示例用内存数据,方便运行)
- 构建工具:Vite
> 注意:本文假设你已了解 GraphQL 基础语法(Query、Mutation、Subscription)。如果不熟悉,建议先阅读官方文档。
第 1 步:定义 Schema 与搭建服务端
首先,我们需要一个 GraphQL 服务端。创建 server.ts:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// 内存数据
let todos = [
{ id: '1', text: '学习 GraphQL', completed: false },
{ id: '2', text: '写一篇教程', completed: true },
];
const typeDefs = `#graphql
type Todo {
id: ID!
text: String!
completed: Boolean!
}
type Query {
todos: [Todo!]!
todo(id: ID!): Todo
}
type Mutation {
addTodo(text: String!): Todo!
toggleTodo(id: ID!): Todo!
deleteTodo(id: ID!): Boolean!
}
`;
const resolvers = {
Query: {
todos: () => todos,
todo: (_, { id }) => todos.find(t => t.id === id),
},
Mutation: {
addTodo: (_, { text }) => {
const newTodo = { id: String(Date.now()), text, completed: false };
todos.push(newTodo);
return newTodo;
},
toggleTodo: (_, { id }) => {
const todo = todos.find(t => t.id === id);
if (!todo) throw new Error('Todo not found');
todo.completed = !todo.completed;
return todo;
},
deleteTodo: (_, { id }) => {
const index = todos.findIndex(t => t.id === id);
if (index === -1) return false;
todos.splice(index, 1);
return true;
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
startStandaloneServer(server, { listen: { port: 4000 } }).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
运行 node server.js(需先编译或使用 ts-node),服务端就启动了。
第 2 步:前端集成 Apollo Client
安装依赖:
npm install @apollo/client graphql
创建 apolloClient.ts:
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const client = new ApolloClient({
link: new HttpLink({ uri: 'http://localhost:4000/graphql' }),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
todos: {
merge(existing, incoming) {
return incoming; // 简单覆盖,后续可优化
},
},
},
},
},
}),
});
export default client;
在 main.tsx 中包裹 ApolloProvider:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import client from './apolloClient';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
);
第 3 步:编写 Query 和 Mutation
使用 gql 模板字符串定义操作。创建 graphql/operations.ts:
import { gql } from '@apollo/client';
export const GET_TODOS = gql`
query GetTodos {
todos {
id
text
completed
}
}
`;
export const ADD_TODO = gql`
mutation AddTodo($text: String!) {
addTodo(text: $text) {
id
text
completed
}
}
`;
export const TOGGLE_TODO = gql`
mutation ToggleTodo($id: ID!) {
toggleTodo(id: $id) {
id
completed
}
}
`;
export const DELETE_TODO = gql`
mutation DeleteTodo($id: ID!) {
deleteTodo(id: $id)
}
`;
第 4 步:在组件中使用
创建 TodoList.tsx:
import { useQuery, useMutation } from '@apollo/client';
import { GET_TODOS, ADD_TODO, TOGGLE_TODO, DELETE_TODO } from './graphql/operations';
import { useState } from 'react';
interface Todo {
id: string;
text: string;
completed: boolean;
}
export default function TodoList() {
const { loading, error, data } = useQuery<{ todos: Todo[] }>(GET_TODOS);
const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: [{ query: GET_TODOS }], // 简单粗暴,后续用缓存更新替代
});
const [toggleTodo] = useMutation(TOGGLE_TODO);
const [deleteTodo] = useMutation(DELETE_TODO, {
refetchQueries: [{ query: GET_TODOS }],
});
const [input, setInput] = useState('');
if (loading) return <p>加载中...</p>;
if (error) return <p>错误: {error.message}</p>;
const handleAdd = () => {
if (!input.trim()) return;
addTodo({ variables: { text: input } });
setInput('');
};
return (
<div>
<input value={input} onChange={e => setInput(e.target.value)} />
<button onClick={handleAdd}>添加</button>
<ul>
{data?.todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo({ variables: { id: todo.id } })}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => deleteTodo({ variables: { id: todo.id } })}>删除</button>
</li>
))}
</ul>
</div>
);
}
运行应用,你应该能看到完整的 Todo 增删改查功能。
最佳实践与踩坑指南
1. 用缓存更新代替 refetchQueries
上面的 refetchQueries 会重新请求整个列表,性能较差。更优雅的方式是利用 Apollo 的缓存自动更新:
const [addTodo] = useMutation(ADD_TODO, {
update(cache, { data }) {
const newTodo = data?.addTodo;
if (!newTodo) return;
cache.modify({
fields: {
todos(existing = []) {
const newTodoRef = cache.writeFragment({
data: newTodo,
fragment: gql`
fragment NewTodo on Todo {
id
text
completed
}
`,
});
return [...existing, newTodoRef];
},
},
});
},
});
这样列表会自动更新,无需额外网络请求。
2. 处理错误与加载状态
Apollo 的 error 对象包含网络错误和 GraphQL 错误。建议使用 errorPolicy: 'all' 来同时获取部分数据和错误:
const { data, error } = useQuery(GET_TODOS, { errorPolicy: 'all' });
3. 分页与游标
对于列表数据,建议使用 fetchMore 实现无限滚动:
const { data, fetchMore } = useQuery(GET_TODOS, {
variables: { offset: 0, limit: 10 },
});
const loadMore = () => {
fetchMore({
variables: { offset: data.todos.length, limit: 10 },
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return { todos: [...prev.todos, ...fetchMoreResult.todos] };
},
});
};
4. 避免 N+1 查询
GraphQL 服务端容易产生 N+1 问题。例如,查询用户及其文章时,每个用户都会单独查一次文章。解决方案是使用 DataLoader 批量加载。
import DataLoader from 'dataloader';
const postLoader = new DataLoader(async (userIds) => {
const posts = await db.posts.find({ userId: { $in: userIds } });
return userIds.map(id => posts.filter(p => p.userId === id));
});
5. 安全与性能
- 限制查询深度和复杂度,防止恶意查询。
- 使用持久化查询(Persisted Queries)减少请求体积。
- 生产环境启用 CDN 缓存 GET 请求。
总结
GraphQL 在前端落地需要关注 Schema 设计、客户端缓存、错误处理和性能优化。通过 Apollo Client,我们可以快速构建类型安全、高效的数据层。本文的示例代码已覆盖核心用法,你可以在此基础上扩展订阅、文件上传等高级功能。
下一步建议:
- 学习 GraphQL Code Generator 自动生成 TypeScript 类型。
- 探索 urql 或 Relay 等替代客户端。
- 在服务端实施查询成本分析。
希望这篇教程能帮助你顺利将 GraphQL 引入生产项目!