使用 GraphQL 和 Apollo 客户端在 React Native 中的实现

使用 GraphQL 和 Apollo 客户端在 React Native 中的实现

关键要点
  • GraphQL 提供灵活的数据查询:与 REST 相比,GraphQL 允许客户端按需请求数据,减少过量或不足的数据传输。
  • Apollo 客户端简化开发 :通过 @apollo/client,React Native 应用可以高效地查询、修改和订阅数据。
  • 后端快速搭建:使用 Hasura、Prisma 或 Apollo Server 可快速构建 GraphQL 服务。
  • 离线支持与缓存 :Apollo 的缓存机制和 apollo3-cache-persist 支持离线功能。
  • TypeScript 增强开发体验:GraphQL Code Generator 自动生成类型代码,提升类型安全。
  • 与 REST + Redux 的对比:GraphQL 减少了状态管理复杂性,但学习曲线较陡。
GraphQL 基础

GraphQL 是一种 API 查询语言,允许客户端精确指定所需数据。它通过单一端点处理查询(Query)、修改(Mutation)和实时更新(Subscription),与 REST 的多端点设计形成对比。

后端搭建

您可以使用 Apollo Server 快速搭建 GraphQL 服务,定义模式(Schema)并实现解析器(Resolver)以连接数据源。

Apollo 客户端集成

在 React Native 中,通过 @apollo/client 配置 ApolloProvider,使用 useQueryuseMutation 钩子获取和修改数据,处理加载状态和错误。

离线支持

Apollo 的 InMemoryCache 和 apollo3-cache-persist 提供缓存持久化,支持离线操作。

TypeScript 集成

GraphQL Code Generator 可根据模式和查询生成 TypeScript 类型,确保类型安全。

开始实践

通过本文的代码示例,您可以构建一个任务管理应用,体验 GraphQL 和 Apollo 的强大功能。建议在实际项目中尝试这些技术,并结合团队需求优化配置。


1. 引言:GraphQL 和 Apollo 在 React Native 中的价值

在移动应用开发中,数据交互是核心环节。传统的 REST API 通常需要多个端点来获取不同数据,可能导致过量数据传输(over-fetching)或需要多次请求(under-fetching)。GraphQL 通过单一端点和灵活的查询机制解决了这些问题,允许客户端精确指定所需数据结构。这对于 React Native 应用尤其重要,因为移动设备对网络效率和性能敏感。

Apollo 客户端是 GraphQL 生态系统中的关键工具,为 React Native 提供了强大的功能,包括数据查询、修改、缓存管理和实时更新。与传统的 REST 和 Redux 组合相比,Apollo 客户端减少了状态管理复杂性,并通过内置缓存优化了性能。此外,结合 TypeScript 和 GraphQL Code Generator,可以实现类型安全,进一步提升开发效率。

本文的目标是通过一个"任务管理"应用示例,展示如何在 React Native 中使用 GraphQL 和 Apollo 客户端。您将学习如何搭建 GraphQL 后端、集成 Apollo 客户端、管理离线数据、实现类型安全,并了解其与 REST 的优劣对比。无论您是 GraphQL 新手还是希望深入了解其在 React Native 中的应用,本文都将提供全面的指导。

2. GraphQL 基础概念

2.1 什么是 GraphQL?

GraphQL 是一种由 Facebook 开发并于 2015 年开源的 API 查询语言和运行时环境。它允许客户端通过单一端点请求精确的数据结构,而无需依赖服务器预定义的响应格式。GraphQL 的核心优势在于其灵活性和效率,特别适合移动应用开发。

与传统的 REST API 不同,GraphQL 使用强类型系统(Schema)定义数据结构,客户端可以根据需求查询特定字段。GraphQL 支持三种主要操作:

  • 查询(Query):用于从服务器获取数据,类似于 REST 的 GET 请求。
  • 变更(Mutation):用于修改服务器数据,类似于 REST 的 POST、PUT 或 DELETE 请求。
  • 订阅(Subscription):用于实时接收服务器推送的更新,适合聊天或通知等场景。

2.2 与 REST 的对比

REST 和 GraphQL 是两种常见的 API 设计风格。以下是它们的主要对比:

特性 REST GraphQL
数据获取 多个端点(如 /users/<id>/users/<id>/posts 单一端点,客户端指定数据结构
数据量控制 服务器定义响应结构,可能过量或不足 客户端精确请求所需字段
请求次数 可能需要多次请求获取完整数据 一次查询获取所有数据
实时更新 需额外实现 WebSocket 或轮询 内置订阅支持实时更新
版本管理 通常通过 URL 版本(如 /v1/users 通过模式演进避免版本化
复杂性 客户端简单,服务器设计复杂 服务器简单,客户端查询复杂
2.2.1 REST 的局限性

在 REST 中,客户端无法控制响应数据的结构。例如,获取用户信息可能返回包括生日、地址等无关字段,导致过量数据传输。如果需要用户的帖子列表,则需额外请求 /users/<id>/posts,增加网络开销。

2.2.2 GraphQL 的优势

GraphQL 允许客户端通过查询指定所需字段。例如:

graphql 复制代码
query {
  user(id: "1") {
    name
    posts {
      title
    }
  }
}

此查询仅返回用户的姓名和帖子标题,减少数据传输量。此外,GraphQL 的强类型系统和模式定义使前后端开发更独立,客户端无需等待后端调整端点。

2.2.3 使用场景
  • REST 适合:简单应用、固定数据结构、少量端点。
  • GraphQL 适合:复杂数据关系、实时更新、移动端优化。

2.3 Query、Mutation 和 Subscription 的使用场景

2.3.1 Query

查询用于读取数据。例如,获取任务列表:

graphql 复制代码
query GetTasks {
  tasks {
    id
    title
    completed
  }
}
2.3.2 Mutation

变更用于修改数据。例如,添加新任务:

graphql 复制代码
mutation AddTask($title: String!) {
  addTask(title: $title) {
    id
    title
    completed
  }
}
2.3.3 Subscription

订阅用于实时更新。例如,监听新任务添加:

graphql 复制代码
subscription OnTaskAdded {
  taskAdded {
    id
    title
    completed
  }
}

这些操作将在后续的 Apollo 客户端集成中详细展示。

3. 搭建 GraphQL 服务

要使用 GraphQL,您需要一个后端服务提供 GraphQL API。本节将介绍如何使用 Apollo Server 快速搭建一个简单的 GraphQL 服务,并简要提及 Hasura 和 Prisma 作为替代方案。

3.1 Apollo Server 搭建

Apollo Server 是一个开源的 GraphQL 服务器,支持 Node.js 环境,易于配置和扩展。以下是搭建步骤。

3.1.1 初始化项目

创建一个新的 Node.js 项目:

bash 复制代码
mkdir graphql-server
cd graphql-server
npm init -y
3.1.2 安装依赖

安装 Apollo Server 和 GraphQL:

bash 复制代码
npm install @apollo/server graphql
3.1.3 定义模式

schema.js 中定义 GraphQL 模式:

javascript 复制代码
const { gql } = require('@apollo/server');

const typeDefs = gql`
  type Task {
    id: ID!
    title: String!
    completed: Boolean!
  }

  type Query {
    tasks: [Task]
  }

  type Mutation {
    addTask(title: String!): Task
  }
`;

module.exports = typeDefs;
3.1.4 实现解析器

resolvers.js 中定义解析器,处理查询和变更逻辑:

javascript 复制代码
const tasks = [
  { id: '1', title: '学习 GraphQL', completed: false },
  { id: '2', title: '构建 React Native 应用', completed: true },
];

const resolvers = {
  Query: {
    tasks: () => tasks,
  },
  Mutation: {
    addTask: (_, { title }) => {
      const newTask = { id: String(tasks.length + 1), title, completed: false };
      tasks.push(newTask);
      return newTask;
    },
  },
};

module.exports = resolvers;
3.1.5 配置服务器

index.js 中创建 Apollo Server 实例并启动:

javascript 复制代码
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

startStandaloneServer(server, {
  listen: { port: 4000 },
}).then(({ url }) => {
  console.log(`服务器运行在 ${url}`);
});
3.1.6 启动服务器

运行服务器:

bash 复制代码
node index.js

访问 [invalid url, do not cite],使用 GraphiQL 测试查询:

graphql 复制代码
query {
  tasks {
    id
    title
    completed
  }
}

3.2 其他后端选项

3.2.1 Hasura

Hasura 是一个开源引擎,可连接现有数据库(如 PostgreSQL)并自动生成 GraphQL API。它适合快速原型开发,无需手动编写解析器。

3.2.2 Prisma

Prisma 是一个现代 ORM,支持 GraphQL 服务器搭建。它通过 Prisma Schema 定义数据模型,自动生成类型安全的 API。

3.2.3 选择建议
  • Apollo Server:适合需要自定义逻辑的项目。
  • Hasura:适合快速连接数据库的项目。
  • Prisma:适合需要类型安全和 ORM 的项目。

4. 在 React Native 中集成 Apollo 客户端

Apollo 客户端是 React Native 中使用 GraphQL 的首选工具,提供数据查询、变更、缓存管理和实时更新功能。

4.1 安装与配置

4.1.1 安装依赖

在 React Native 项目中安装 Apollo 客户端:

bash 复制代码
npm install @apollo/client graphql
4.1.2 配置 Apollo 客户端

apolloClient.js 中创建客户端实例:

javascript 复制代码
import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: '[invalid url, do not cite]',
  cache: new InMemoryCache(),
});

export default client;
4.1.3 提供 Apollo 客户端

App.js 中使用 ApolloProvider 包装应用:

javascript 复制代码
import React from 'react';
import { ApolloProvider } from '@apollo/client';
import client from './apolloClient';
import TaskList from './TaskList';

const App = () => {
  return (
    <ApolloProvider client={client}>
      <TaskList />
    </ApolloProvider>
  );
};

export default App;

4.2 使用 useQuery 获取数据

TaskList.js 中使用 useQuery 获取任务列表:

javascript 复制代码
import React from 'react';
import { View, Text, FlatList } from 'react-native';
import { useQuery, gql } from '@apollo/client';

const GET_TASKS = gql`
  query GetTasks {
    tasks {
      id
      title
      completed
    }
  }
`;

const TaskList = () => {
  const { loading, error, data } = useQuery(GET_TASKS);

  if (loading) return <Text>加载中...</Text>;
  if (error) return <Text>错误: {error.message}</Text>;

  return (
    <FlatList
      data={data.tasks}
      renderItem={({ item }) => <Text>{item.title}</Text>}
      keyExtractor={item => item.id}
    />
  );
};

export default TaskList;
4.2.1 状态管理
  • loading:表示查询是否正在进行。
  • error:包含查询失败的错误信息。
  • data:查询返回的数据。

4.3 使用 useMutation 修改数据

AddTask.js 中使用 useMutation 添加任务:

javascript 复制代码
import React, { useState } from 'react';
import { View, TextInput, Button } from 'react-native';
import { useMutation, gql } from '@apollo/client';

const ADD_TASK = gql`
  mutation AddTask($title: String!) {
    addTask(title: $title) {
      id
      title
      completed
    }
  }
`;

const GET_TASKS = gql`
  query GetTasks {
    tasks {
      id
      title
      completed
    }
  }
`;

const AddTask = () => {
  const [title, setTitle] = useState('');
  const [addTask, { data, loading, error }] = useMutation(ADD_TASK, {
    refetchQueries: [{ query: GET_TASKS }],
  });

  const handleAdd = () => {
    addTask({ variables: { title } });
    setTitle('');
  };

  return (
    <View>
      <TextInput value={title} onChangeText={setTitle} placeholder="任务标题" />
      <Button title="添加任务" onPress={handleAdd} disabled={loading} />
      {error && <Text>错误: {error.message}</Text>}
    </View>
  );
};

export default AddTask;
4.3.1 缓存更新

通过 refetchQueries,在添加任务后自动重新获取任务列表。另一种方法是手动更新缓存:

javascript 复制代码
addTask({
  variables: { title },
  update(cache, { data: { addTask } }) {
    const { tasks } = cache.readQuery({ query: GET_TASKS });
    cache.writeQuery({
      query: GET_TASKS,
      data: { tasks: [...tasks, addTask] },
    });
  },
});

4.4 错误捕获与重试

为用户提供友好的错误提示,并支持重试:

javascript 复制代码
const TaskList = () => {
  const { loading, error, data, refetch } = useQuery(GET_TASKS);

  if (loading) return <Text>加载中...</Text>;
  if (error) {
    return (
      <View>
        <Text>错误: {error.message}</Text>
        <Button title="重试" onPress={refetch} />
      </View>
    );
  }

  return (
    <FlatList
      data={data.tasks}
      renderItem={({ item }) => <Text>{item.title}</Text>}
      keyExtractor={item => item.id}
    />
  );
};

5. 离线支持与缓存优化

Apollo 客户端的缓存机制是其核心优势之一,支持离线操作和性能优化。

5.1 InMemoryCache

InMemoryCache 是 Apollo 的默认缓存,基于数据类型和 ID 规范化存储。例如,任务数据按 Task:id 存储,允许多个查询共享缓存。

5.1.1 缓存策略
  • 默认缓存:查询结果自动缓存,后续查询优先从缓存读取。

  • 字段策略 :通过 cache-first(默认)、network-onlycache-and-network 控制缓存行为:

    javascript 复制代码
    useQuery(GET_TASKS, { fetchPolicy: 'cache-and-network' });

5.2 离线支持与 apollo3-cache-persist

apollo3-cache-persist 将缓存持久化到本地存储,支持离线操作。

5.2.1 安装
bash 复制代码
npm install apollo3-cache-persist @react-native-async-storage/async-storage
5.2.2 配置

apolloClient.js 中配置持久化:

javascript 复制代码
import { InMemoryCache } from '@apollo/client';
import { persistCache } from 'apollo3-cache-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';

const cache = new InMemoryCache();

persistCache({
  cache,
  storage: AsyncStorage,
});

const client = new ApolloClient({
  uri: '[invalid url, do not cite]',
  cache,
});

export default client;
5.2.3 离线操作

缓存持久化后,应用可在离线状态下显示缓存数据。网络恢复时,重新查询可更新数据。

5.3 最佳实践

  • 缓存规范化 :确保模式中的类型包含唯一 ID(如 id)。
  • 离线优先:优先使用缓存数据,失败时提示用户。
  • 清理缓存:定期清除过期缓存以节省存储空间。

6. 实时更新与订阅

GraphQL 订阅通过 WebSocket 提供实时更新,适合任务通知等场景。

6.1 服务器配置

在 Apollo Server 中,使用 subscriptions-transport-ws 支持订阅:

6.1.1 安装
bash 复制代码
npm install subscriptions-transport-ws
6.1.2 更新模式

schema.js 中添加订阅:

javascript 复制代码
const { gql } = require('@apollo/server');

const typeDefs = gql`
  type Task {
    id: ID!
    title: String!
    completed: Boolean!
  }

  type Query {
    tasks: [Task]
  }

  type Mutation {
    addTask(title: String!): Task
  }

  type Subscription {
    taskAdded: Task
  }
`;

module.exports = typeDefs;
6.1.3 更新解析器

resolvers.js 中添加订阅逻辑:

javascript 复制代码
const { PubSub } = require('apollo-server');

const pubsub = new PubSub();
const TASK_ADDED = 'TASK_ADDED';

const resolvers = {
  Query: {
    tasks: () => tasks,
  },
  Mutation: {
    addTask: (_, { title }) => {
      const newTask = { id: String(tasks.length + 1), title, completed: false };
      tasks.push(newTask);
      pubsub.publish(TASK_ADDED, { taskAdded: newTask });
      return newTask;
    },
  },
  Subscription: {
    taskAdded: {
      subscribe: () => pubsub.asyncIterator([TASK_ADDED]),
    },
  },
};

module.exports = resolvers;
6.1.4 配置 WebSocket

更新 index.js 支持订阅(需额外配置,参考 Apollo 文档)。

6.2 客户端订阅

在 React Native 中,使用 useSubscription 接收实时更新:

javascript 复制代码
import React from 'react';
import { View, Text } from 'react-native';
import { useSubscription, gql } from '@apollo/client';

const TASK_ADDED = gql`
  subscription OnTaskAdded {
    taskAdded {
      id
      title
      completed
    }
  }
`;

const TaskSubscription = () => {
  const { data, loading } = useSubscription(TASK_ADDED);

  if (loading) return null;
  return <Text>新任务: {data.taskAdded.title}</Text>;
};

export default TaskSubscription;
6.2.1 配置 WebSocket

apolloClient.js 中添加 WebSocket 链接:

javascript 复制代码
import { ApolloClient, InMemoryCache, split } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { HttpLink } from '@apollo/client/link/http';
import { getMainDefinition } from '@apollo/client/utilities';

const httpLink = new HttpLink({
  uri: '[invalid url, do not cite]',
});

const wsLink = new WebSocketLink({
  uri: 'ws://localhost:4000/graphql',
  options: {
    reconnect: true,
  },
});

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink,
);

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

export default client;

7. 与 REST + Redux 的对比

在选择 GraphQL 和 Apollo 客户端还是 REST 和 Redux 时,需根据项目需求权衡优劣。

7.1 REST + Redux

7.1.1 优势
  • 简单性:对于小型应用,REST 端点设计简单,易于实现。
  • 社区支持:REST 和 Redux 有广泛的社区和工具支持。
  • 控制粒度:开发者可精确控制每个请求和状态更新。
7.1.2 劣势
  • 数据冗余:可能导致过量或不足的数据传输。
  • 状态管理复杂:需要手动同步 API 数据和 Redux 状态。
  • 实时更新:需额外实现 WebSocket 或轮询。
7.1.3 示例

使用 REST 获取任务列表:

javascript 复制代码
import axios from 'axios';
import { createSlice } from '@reduxjs/toolkit';

const taskSlice = createSlice({
  name: 'tasks',
  initialState: { list: [], loading: false, error: null },
  reducers: {
    fetchTasksStart(state) {
      state.loading = true;
    },
    fetchTasksSuccess(state, action) {
      state.list = action.payload;
      state.loading = false;
    },
    fetchTasksFailure(state, action) {
      state.error = action.payload;
      state.loading = false;
    },
  },
});

export const { fetchTasksStart, fetchTasksSuccess, fetchTasksFailure } = taskSlice.actions;

export const fetchTasks = () => async (dispatch) => {
  dispatch(fetchTasksStart());
  try {
    const response = await axios.get('[invalid url, do not cite]
    dispatch(fetchTasksSuccess(response.data));
  } catch (error) {
    dispatch(fetchTasksFailure(error.message));
  }
};

export default taskSlice.reducer;

7.2 GraphQL + Apollo

7.2.1 优势
  • 高效数据获取:客户端精确指定数据,减少网络开销。
  • 内置缓存:InMemoryCache 自动管理数据,减少状态管理复杂性。
  • 实时更新:订阅支持开箱即用的实时功能。
  • 代码简洁:减少样板代码,集成查询和变更逻辑。
7.2.2 劣势
  • 学习曲线:GraphQL 和 Apollo 的概念对新手较复杂。
  • 后端复杂性:需要设计模式和解析器,可能增加后端工作量。
  • 查询性能:复杂查询可能影响服务器性能,需优化。

7.3 选择建议

  • REST + Redux:适合简单应用、固定数据结构或已有 REST 后端。
  • GraphQL + Apollo:适合复杂数据关系、实时更新或移动端优化。

8. GraphQL 与 TypeScript 自动集成

TypeScript 与 GraphQL 的结合通过类型安全提升开发效率。GraphQL Code Generator 可根据模式和查询生成 TypeScript 类型。

8.1 配置 GraphQL Code Generator

8.1.1 安装
bash 复制代码
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
8.1.2 创建配置文件

在项目根目录创建 codegen.yml

yaml 复制代码
schema: '[invalid url, do not cite]'
documents: 'src/**/*.graphql'
generates:
  src/generated/graphql.tsx:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withHooks: true
8.1.3 定义查询

src/queries/getTasks.graphql 中定义查询:

graphql 复制代码
query GetTasks {
  tasks {
    id
    title
    completed
  }
}
8.1.4 生成类型

运行生成命令:

bash 复制代码
npx graphql-codegen

生成的文件 src/generated/graphql.tsx 包含类型和钩子。

8.2 使用生成类型

在组件中使用生成的钩子:

javascript 复制代码
import React from 'react';
import { View, Text, FlatList } from 'react-native';
import { useGetTasksQuery } from './generated/graphql';

const TaskList = () => {
  const { data, loading, error } = useGetTasksQuery();

  if (loading) return <Text>加载中...</Text>;
  if (error) return <Text>错误: {error.message}</Text>;

  return (
    <FlatList
      data={data?.tasks}
      renderItem={({ item }) => <Text>{item.title}</Text>}
      keyExtractor={item => item.id}
    />
  );
};

export default TaskList;
8.2.1 优势
  • 类型安全:编译时捕获字段错误。
  • 自动补全:IDE 支持字段和类型的自动补全。
  • 一致性:确保查询与模式一致。

9. 综合示例:任务管理应用

以下是一个完整的任务管理应用示例,结合查询、变更和订阅:

javascript 复制代码
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { ApolloProvider } from '@apollo/client';
import client from './apolloClient';
import TaskList from './TaskList';
import AddTask from './AddTask';

const Stack = createStackNavigator();

const App = () => {
  return (
    <ApolloProvider client={client}>
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen name="TaskList" component={TaskList} />
          <Stack.Screen name="AddTask" component={AddTask} />
        </Stack.Navigator>
      </NavigationContainer>
    </ApolloProvider>
  );
};

export default App;

10. 结论

通过本文,您掌握了在 React Native 中使用 GraphQL 和 Apollo 客户端的完整流程,包括搭建后端、集成客户端、管理缓存、实现离线支持和类型安全。以下是关键总结:

  • GraphQL 优势:灵活的数据查询减少网络开销,适合移动端。
  • Apollo 客户端:提供强大的数据管理和缓存功能。
  • 离线支持 :通过 apollo3-cache-persist 实现缓存持久化。
  • TypeScript 集成:GraphQL Code Generator 提升开发效率。