项目结构
my-app/
├── app/
│ ├── api/graphql/route.ts # Next.js API Route (Apollo Server)
│ ├── posts/page.tsx # 服务端组件(SSR 查询)
│ └── layout.tsx # ApolloProvider 包装
├── graphql/
│ ├── schema.graphql # SDL Schema 定义
│ ├── queries.graphql # 客户端查询定义
│ └── generated/ # GraphQL Codegen 生成的类型
├── lib/
│ └── apollo-client.ts # Apollo Client 配置
└── codegen.ts # GraphQL Codegen 配置
Next.js API Route 中的 Apollo Server
// app/api/graphql/route.ts
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { typeDefs, resolvers } from '@/graphql/schema';
const server = new ApolloServer({ typeDefs, resolvers });
const handler = startServerAndCreateNextHandler(server, {
context: async (req) => ({
user: await getUserFromRequest(req),
db,
}),
});
export { handler as GET, handler as POST };
Apollo Client 配置
// lib/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';
const httpLink = new HttpLink({ uri: '/api/graphql' });
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:3000/api/graphql',
}));
// Query/Mutation 走 HTTP,Subscription 走 WebSocket
const splitLink = split(
({ query }) => {
const def = getMainDefinition(query);
return def.kind === 'OperationDefinition' && def.operation === 'subscription';
},
wsLink,
httpLink,
);
export const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
GraphQL Codegen 类型生成
// codegen.ts
import { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: './graphql/schema.graphql',
documents: ['./graphql/**/*.graphql', './app/**/*.tsx'],
generates: {
'./graphql/generated/index.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo',
],
config: {
withHooks: true, // 生成 useQuery/useMutation hooks
withResultType: true,
},
},
},
};
export default config;
# 生成类型和 hooks
npx graphql-codegen --config codegen.ts
# 监视模式(开发时自动重新生成)
npx graphql-codegen --config codegen.ts --watch
客户端使用生成的 Hooks
// graphql/queries.graphql 中定义的查询
// query GetPosts { posts { id title author { name } } }
// 使用 Codegen 生成的类型安全 Hook
import { useGetPostsQuery, useCreatePostMutation } from '@/graphql/generated';
function PostList() {
const { data, loading, error } = useGetPostsQuery();
const [createPost, { loading: creating }] = useCreatePostMutation({
// 乐观更新:立即更新 UI,等服务器确认
optimisticResponse: ({ input }) => ({
createPost: {
__typename: 'Post',
id: 'temp-id',
title: input.title,
author: { name: 'You' },
},
}),
// 成功后更新缓存
update(cache, { data }) {
cache.modify({
fields: {
posts(existingPosts = []) {
return [...existingPosts, data?.createPost];
},
},
});
},
});
if (loading) return <Loading />;
if (error) return <Error message={error.message} />;
return (
<div>
{data?.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
Codegen 的最大价值
GraphQL Codegen 基于 Schema 和你写的查询自动生成 TypeScript 类型和 React Hooks。当 Schema 变化时,只需重新运行 Codegen,TypeScript 编译器会立即告诉你哪些地方需要更新——这是 GraphQL 类型安全的精髓所在。
Apollo Client 缓存策略
InMemoryCache 规范化缓存
Apollo Client 默认使用规范化缓存:将每个对象按
__typename:id(如 Post:123)存储为独立条目。相同 ID 的对象在多个查询中共享同一缓存条目,Mutation 更新一个对象后,所有显示该对象的组件自动刷新。缓存更新策略
有三种方式更新缓存:1) 重新查询(refetch)——最简单但有网络请求;2) cache.modify——精确修改某个字段;3) 乐观更新(optimisticResponse)——立即更新 UI,等服务器确认后再替换,提升感知响应速度。
fetchPolicy 控制
cache-first(默认,优先缓存)、network-only(总是请求服务器)、cache-and-network(先返回缓存,再用网络结果更新)、no-cache(不使用也不写入缓存)。根据数据实时性要求选择。
服务端渲染(RSC)与 GraphQL
// app/posts/page.tsx(React Server Component)
// 服务端直接调用 GraphQL,无需 Apollo Client
import { request, gql } from 'graphql-request';
const GET_POSTS = gql`
query GetPosts {
posts(first: 10) {
edges {
node { id title author { name } }
}
}
}
`;
// Next.js 会在构建时或每次请求时执行此函数
export default async function PostsPage() {
// 服务端直接 fetch,利用 Next.js 的 fetch 缓存
const data = await request('http://localhost:3000/api/graphql', GET_POSTS);
return (
<main>
<h1>帖子列表</h1>
{data.posts.edges.map(({ node }) => (
<article key={node.id}>
<h2>{node.title}</h2>
<p>作者:{node.author.name}</p>
</article>
))}
</main>
);
}
// 使用 Next.js 15 的 fetch 缓存控制
async function fetchPosts() {
const res = await fetch('/api/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: GET_POSTS }),
// 60 秒内复用缓存结果(ISR)
next: { revalidate: 60 },
});
return res.json();
}
错误处理:GraphQL vs HTTP 错误
// GraphQL 响应结构:即使有错误,HTTP 状态码仍然是 200
// {
// "data": { "post": null },
// "errors": [{ "message": "Post not found", "extensions": { "code": "NOT_FOUND" } }]
// }
// 客户端统一错误处理
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (const { message, extensions } of graphQLErrors) {
switch (extensions?.code) {
case 'UNAUTHENTICATED':
// Token 过期:刷新 Token 并重试
return fromPromise(refreshToken()).flatMap(() => forward(operation));
case 'FORBIDDEN':
// 权限不足:显示提示并跳转
router.push('/unauthorized');
break;
default:
console.error(`GraphQL error: ${message}`);
}
}
}
if (networkError) {
console.error(`Network error: ${networkError}`);
}
});
const client = new ApolloClient({
// 错误处理链接放在最前面
link: errorLink.concat(splitLink),
cache: new InMemoryCache(),
});
本章小结
本章核心要点
- Next.js 集成方式:API Route(app/api/graphql/route.ts)使用 @as-integrations/next 运行 Apollo Server;React Server Component 可直接用 graphql-request 在服务端查询,无需 Apollo Client。
- GraphQL Codegen:根据 Schema + .graphql 查询文件自动生成 TypeScript 类型和 React Hooks(useXxxQuery/useMutation);开发时用 --watch 模式,Schema 变更立即反映到类型系统中。
- Apollo Client 规范化缓存:以 __typename:id 为 key 规范化存储所有对象;Mutation 更新某对象后,所有引用该对象的查询自动刷新——这是 Apollo Client 最核心的能力。
- 乐观更新:通过 optimisticResponse 提前更新 UI,提升交互响应速度;服务器返回后替换乐观值(或出错时回滚)——适合 Like/Follow 等高频操作。
- 错误处理差异:GraphQL 错误(逻辑错误、权限错误)通过 errors 字段返回(HTTP 200);网络错误(服务不可用)才是 HTTP 非 200;需用 Apollo Error Link 统一处理。
- 全栈类型安全链路:Schema(SDL)→ Codegen 生成 TypeScript 类型 → 前端 useQuery hook 有完整类型推断 → Server Resolver 使用同一 Schema 类型 → 任何 Schema 变更都能被 TypeScript 编译器检测。