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 codegen

9.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 分析内存泄漏

进阶学习资源

  1. 官方文档

  2. 书籍

    • 《Learning GraphQL》
    • 《GraphQL in Action》
    • 《Full Stack GraphQL Applications》
  3. 视频教程

  4. 示例项目

  5. 社区资源

实践练习

练习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 的集成,敬请期待!

« 上一篇 Vue 3 大规模应用架构:可扩展可维护的系统设计 下一篇 » Vue 3 与 Relay 集成:基于片段的GraphQL客户端框架实践