Vue 3 与 Apollo Client 高级应用
概述
Apollo Client 是一个功能强大的 GraphQL 客户端,用于管理应用程序的数据获取、缓存和状态管理。与 Vue 3 结合使用时,Apollo Client 可以提供高效的数据获取和管理能力,特别是在复杂的数据关系场景下。本集将深入探讨 Vue 3 与 Apollo Client 的高级集成,包括查询优化、缓存管理、订阅、错误处理、分页等高级特性。
核心知识点
1. Apollo Client 基础
核心概念
- GraphQL 查询 - 获取数据的请求
- GraphQL 变更 - 修改数据的请求
- GraphQL 订阅 - 实时数据更新
- 缓存 - 本地存储获取的数据
- 链接 (Link) - 处理 GraphQL 请求的中间件
- 缓存策略 - 控制数据缓存的行为
Apollo Client 架构
┌─────────────────┐
│ Vue 组件 │
└────────┬────────┘
│
┌────────▼────────┐
│ Apollo Client │
└────────┬────────┘
│
┌────────▼────────┐ ┌─────────────────┐
│ 链接链 (Link) │ │ 缓存 (Cache) │
└────────┬────────┘ └─────────────────┘
│
┌────────▼────────┐
│ GraphQL 服务 │
└─────────────────┘2. Vue 3 与 Apollo Client 集成
安装依赖
npm install @apollo/client graphql vue-apollo配置 Apollo Client
// src/core/apollo/apollo-client.ts
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client/core';
import { setContext } from '@apollo/client/link/context';
// 创建 HTTP 链接
const httpLink = createHttpLink({
uri: import.meta.env.VITE_GRAPHQL_API_URL || 'http://localhost:4000/graphql'
});
// 添加认证头部
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ''
}
};
});
// 创建缓存
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
// 配置分页字段
users: {
keyArgs: false,
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
}
}
});
// 创建 Apollo Client 实例
export const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache,
connectToDevTools: true
});在 Vue 3 中使用
// src/main.ts
import { createApp } from 'vue';
import { createApolloProvider } from '@vue/apollo-option';
import App from './App.vue';
import { apolloClient } from './core/apollo/apollo-client';
// 创建 Apollo Provider
const apolloProvider = new createApolloProvider({
defaultClient: apolloClient
});
const app = createApp(App);
app.use(apolloProvider);
app.mount('#app');3. 查询优化
3.1 条件查询
<template>
<div>
<input v-model="searchQuery" placeholder="Search users" />
<button @click="fetchUsers">Search</button>
<ul>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { gql } from '@apollo/client/core';
import { useQuery } from '@vue/apollo-composable';
export default defineComponent({
setup() {
const searchQuery = ref('');
const { result, execute } = useQuery(
gql`
query GetUsers($search: String) {
users(search: $search) {
id
name
email
}
}
`,
{
skip: true // 初始不执行查询
}
);
const fetchUsers = () => {
execute({
search: searchQuery.value
});
};
return {
searchQuery,
users: result.value?.users,
fetchUsers
};
}
});
</script>3.2 预取数据
// 预取用户数据
const { loadQuery } = useApolloClient();
const prefetchUser = async (userId: string) => {
await loadQuery(
gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
{ id: userId }
);
};3.3 延迟查询
const { result, loading, error } = useQuery(
gql`
query GetComplexData {
// 复杂的查询
}
`,
{
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first'
}
);4. 缓存管理
4.1 缓存策略
- cache-first - 优先从缓存获取数据,不存在则请求网络
- network-only - 总是从网络获取数据
- cache-and-network - 同时从缓存和网络获取数据,网络数据更新缓存
- cache-only - 只从缓存获取数据
- no-cache - 总是从网络获取数据,不更新缓存
4.2 缓存更新策略
手动更新缓存
const { mutate } = useMutation(
gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`,
{
update(cache, { data: { createUser } }) {
// 更新缓存中的用户列表
cache.modify({
fields: {
users(existingUsers = []) {
const newUserRef = cache.writeFragment({
data: createUser,
fragment: gql`
fragment NewUser on User {
id
name
email
}
`
});
return [...existingUsers, newUserRef];
}
}
});
}
}
);自动更新缓存
const { mutate } = useMutation(
gql`
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}
`,
{
refetchQueries: [
{ query: gql`query GetUsers { users { id name email } }` }
]
}
);4.3 缓存失效
// 使查询失效
cache.invalidateQueries({
query: gql`query GetUsers { users { id name email } }`
});
// 使特定字段失效
cache.modify({
fields: {
users() {
return undefined; // 使缓存失效,下次查询会重新获取
}
}
});5. GraphQL 订阅
5.1 配置订阅链接
// src/core/apollo/apollo-client.ts
import { WebSocketLink } from '@apollo/client/link/ws';
import { split, HttpLink } from '@apollo/client/link/http';
import { getMainDefinition } from '@apollo/client/utilities';
// 创建 WebSocket 链接
const wsLink = new WebSocketLink({
uri: import.meta.env.VITE_GRAPHQL_WS_URL || 'ws://localhost:4000/graphql',
options: {
reconnect: true,
connectionParams: {
authToken: localStorage.getItem('token')
}
}
});
// 根据操作类型分割链接
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
authLink.concat(httpLink)
);
// 创建 Apollo Client 实例
export const apolloClient = new ApolloClient({
link: splitLink,
cache,
connectToDevTools: true
});5.2 使用订阅
<template>
<div>
<h2>实时消息</h2>
<ul>
<li v-for="message in messages" :key="message.id">
{{ message.user.name }}: {{ message.content }}
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useSubscription } from '@vue/apollo-composable';
import { gql } from '@apollo/client/core';
export default defineComponent({
setup() {
const messages = ref([]);
const { onResult } = useSubscription(
gql`
subscription OnNewMessage {
newMessage {
id
content
createdAt
user {
id
name
}
}
}
`
);
onResult(({ data }) => {
if (data.newMessage) {
messages.value.push(data.newMessage);
}
});
return {
messages
};
}
});
</script>6. 分页与无限滚动
6.1 偏移分页
const { result, fetchMore } = useQuery(
gql`
query GetUsers($offset: Int!, $limit: Int!) {
users(offset: $offset, limit: $limit) {
id
name
email
}
totalUsers
}
`,
{
variables: {
offset: 0,
limit: 10
}
}
);
const loadMore = () => {
fetchMore({
variables: {
offset: result.value?.users?.length || 0
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
users: [...prev.users, ...fetchMoreResult.users],
totalUsers: fetchMoreResult.totalUsers
};
}
});
};6.2 游标分页
const { result, fetchMore } = useQuery(
gql`
query GetUsers($after: String, $first: Int!) {
users(first: $first, after: $after) {
edges {
node {
id
name
email
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}
`,
{
variables: {
first: 10,
after: null
}
}
);
const loadMore = () => {
if (result.value?.users?.pageInfo?.hasNextPage) {
fetchMore({
variables: {
after: result.value.users.pageInfo.endCursor
}
});
}
};7. 错误处理
7.1 查询错误处理
const { result, loading, error } = useQuery(
gql`
query GetUsers {
users {
id
name
email
}
}
`
);
if (error) {
console.error('Query error:', error);
// 显示错误信息
}7.2 变更错误处理
const { mutate, loading, error } = useMutation(
gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`
);
const handleCreateUser = async (input: CreateUserInput) => {
try {
const result = await mutate(input);
// 处理成功
} catch (err) {
// 处理错误
console.error('Mutation error:', err);
}
};7.3 全局错误处理
// src/core/apollo/apollo-client.ts
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.error(`GraphQL error: ${message} at ${path}`);
// 处理 GraphQL 错误
});
}
if (networkError) {
console.error(`Network error: ${networkError}`);
// 处理网络错误
}
});
// 将 errorLink 添加到链接链
const link = from([errorLink, authLink.concat(httpLink)]);8. 组合式函数 (Composables) 封装
8.1 封装 GraphQL 查询组合式函数
// src/shared/composables/useGraphqlQuery.ts
import { useQuery, type UseQueryOptions } from '@vue/apollo-composable';
import type { DocumentNode } from 'graphql';
export function useGraphqlQuery<TData = any, TVariables = any>(
query: DocumentNode,
options?: UseQueryOptions<TData, TVariables>
) {
const { result, loading, error, refetch, onResult, onError } = useQuery<TData, TVariables>(query, options);
return {
data: result,
loading,
error,
refetch,
onResult,
onError
};
}8.2 封装 GraphQL 变更组合式函数
// src/shared/composables/useGraphqlMutation.ts
import { useMutation, type UseMutationOptions } from '@vue/apollo-composable';
import type { DocumentNode } from 'graphql';
export function useGraphqlMutation<TData = any, TVariables = any>(
mutation: DocumentNode,
options?: UseMutationOptions<TData, TVariables>
) {
const { mutate, loading, error, onDone, onError } = useMutation<TData, TVariables>(mutation, options);
return {
mutate,
loading,
error,
onDone,
onError
};
}9. 代码生成
9.1 使用 GraphQL Code Generator
安装依赖:
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset配置 codegen.ts:
// codegen.ts
import { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'http://localhost:4000/graphql',
documents: ['src/**/*.graphql', 'src/**/*.ts', 'src/**/*.vue'],
generates: {
'./src/core/apollo/generated/': {
preset: 'client',
plugins: [],
presetConfig: {
gqlTagName: 'gql'
}
}
},
ignoreNoDocuments: true
};
export default config;添加 npm 脚本:
{
"scripts": {
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch"
}
}生成类型:
npm run codegen9.2 使用生成的类型
import { useGetUsersQuery } from '../core/apollo/generated/graphql';
const { data, loading, error } = useGetUsersQuery({
variables: {
search: 'test'
}
});10. 性能优化
10.1 查询拆分
将大型查询拆分为多个小型查询:
// 不推荐:大型查询
const { result } = useQuery(gql`
query GetAllData {
users { /* ... */ }
products { /* ... */ }
orders { /* ... */ }
}
`);
// 推荐:拆分查询
const { result: usersResult } = useQuery(gql`query GetUsers { users { /* ... */ } }`);
const { result: productsResult } = useQuery(gql`query GetProducts { products { /* ... */ } }`);
const { result: ordersResult } = useQuery(gql`query GetOrders { orders { /* ... */ } }`);10.2 字段选择
只请求需要的字段:
# 不推荐:请求所有字段
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
}
}
# 推荐:只请求需要的字段
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}10.3 缓存优化
// 优化缓存键
const cache = new InMemoryCache({
dataIdFromObject: (object) => {
if (object.__typename && object.id) {
return `${object.__typename}:${object.id}`;
}
return defaultDataIdFromObject(object);
}
});最佳实践
1. 查询设计最佳实践
- 只请求需要的字段 - 避免请求不必要的数据
- 使用片段 (Fragment) - 复用常用的字段组合
- 分页查询 - 处理大量数据
- 变量化查询 - 使查询更灵活
- 查询拆分 - 将大型查询拆分为多个小型查询
2. 缓存管理最佳实践
- 合理配置缓存策略 - 根据数据更新频率选择合适的缓存策略
- 使用自动缓存更新 - 利用 Apollo Client 的自动缓存更新机制
- 手动更新复杂缓存 - 对于复杂的数据关系,手动更新缓存
- 定期清理缓存 - 清除不再需要的数据
- 使用缓存预热 - 预取常用数据
3. 错误处理最佳实践
- 全局错误处理 - 处理通用错误
- 组件级错误处理 - 处理特定组件的错误
- 用户友好的错误信息 - 向用户显示清晰的错误信息
- 错误重试机制 - 对于临时错误,实现自动重试
- 监控错误 - 记录和监控错误
4. 性能优化最佳实践
- 查询拆分 - 避免大型查询
- 预取数据 - 提前获取可能需要的数据
- 使用生成的类型 - 提高类型安全性和开发效率
- 优化缓存配置 - 提高缓存命中率
- 监控性能 - 使用 Apollo Client DevTools 监控性能
5. 开发工作流最佳实践
- 使用 Apollo Client DevTools - 调试和监控 Apollo Client
- 代码生成 - 自动生成类型和查询
- Mock 数据 - 开发阶段使用 Mock 数据
- 自动化测试 - 测试 GraphQL 查询和变更
- 文档化查询 - 为查询添加描述
常见问题与解决方案
1. 缓存不一致
问题:数据更新后,缓存中的数据没有同步更新。
解决方案:
- 使用
refetchQueries更新相关查询 - 手动更新缓存
- 使用
cache.modify修改缓存 - 配置正确的缓存键和类型策略
2. 查询性能问题
问题:查询执行缓慢,影响应用性能。
解决方案:
- 优化 GraphQL 服务端查询
- 减少请求的字段数量
- 拆分大型查询
- 实现查询分页
- 使用合适的缓存策略
3. 订阅连接问题
问题:WebSocket 连接频繁断开或无法连接。
解决方案:
- 配置正确的 WebSocket URL
- 启用自动重连选项
- 检查认证令牌是否有效
- 监控 WebSocket 连接状态
4. 类型错误
问题:GraphQL 查询返回的数据类型与期望不符。
解决方案:
- 使用 GraphQL Code Generator 生成类型
- 检查 GraphQL 服务端类型定义
- 验证查询字段与服务端定义匹配
5. 内存泄漏
问题:长时间运行的应用出现内存泄漏。
解决方案:
- 确保正确清理订阅
- 避免无限查询循环
- 定期清理不再需要的缓存数据
- 使用 Chrome DevTools 分析内存泄漏
进阶学习资源
官方文档:
书籍:
- 《Learning GraphQL》
- 《GraphQL in Action》
- 《Full Stack GraphQL Applications》
视频教程:
示例项目:
社区资源:
实践练习
练习1:Apollo Client 配置
要求:
- 配置 Apollo Client 连接到 GraphQL 服务
- 实现认证头部添加
- 配置缓存策略
- 集成到 Vue 3 应用
练习2:查询优化
要求:
- 实现基本查询
- 配置不同的缓存策略
- 实现预取数据
- 实现延迟查询
练习3:缓存管理
要求:
- 手动更新缓存
- 自动更新缓存
- 实现缓存失效
- 配置缓存类型策略
练习4:GraphQL 订阅
要求:
- 配置 WebSocket 链接
- 实现实时订阅
- 处理订阅连接状态
- 测试实时数据更新
练习5:分页实现
要求:
- 实现偏移分页
- 实现游标分页
- 实现无限滚动
- 测试分页性能
练习6:代码生成
要求:
- 配置 GraphQL Code Generator
- 生成查询和类型
- 使用生成的类型
- 集成到开发工作流
练习7:错误处理
要求:
- 实现查询错误处理
- 实现变更错误处理
- 实现全局错误处理
- 测试错误场景
总结
Vue 3 与 Apollo Client 的结合为复杂应用提供了强大的数据管理能力。通过本集的学习,你应该掌握了 Apollo Client 的核心概念、Vue 3 集成、查询优化、缓存管理、订阅、分页、错误处理等高级特性。
在实际应用中,需要根据项目需求选择合适的配置和策略,同时关注性能优化和错误处理。随着 GraphQL 生态的不断发展,Apollo Client 也在持续更新和改进,开发者需要保持学习和探索的态度,不断提升自己的技能。
在下一集中,我们将探讨 Vue 3 与 Relay 的集成,敬请期待!