React 18.x 学习计划 - 第十五天:GraphQL 与实时应用实战

学习目标

  • 理解GraphQL的核心概念(Schema、Query、Mutation、Subscription)
  • 掌握在React中集成GraphQL(Apollo Client)
  • 学会使用GraphQL进行增删改查
  • 实现基于WebSocket的实时更新(Subscription)
  • 完成一个使用GraphQL的实时任务管理小项目

学习时间安排

总时长:8-9小时

  • GraphQL基础与Schema设计:2小时
  • Apollo Client集成与Query/Mutation:2小时
  • Subscription与实时更新:2小时
  • 综合实战项目:2-3小时

第一部分:GraphQL 基础与 Schema 设计 (2小时)

1.1 GraphQL 核心概念

对比 REST 与 GraphQL(简要说明)
  • REST:

    • 多个端点,例如 /users/users/:id/posts
    • 前端经常出现"要么多拉要么少拉"数据的问题
    • 版本管理复杂
  • GraphQL:

    • 单一端点,例如 /graphql
    • 前端按需声明数据结构,后端按"Schema"执行
    • 使用类型系统描述数据结构

1.2 示例 Schema 设计(任务系统)

GraphQL Schema(SDL)示例
graphql 复制代码
# schema.graphql

# 任务优先级枚举
enum TaskPriority {
  LOW
  MEDIUM
  HIGH
}

# 任务状态枚举
enum TaskStatus {
  PENDING
  IN_PROGRESS
  COMPLETED
  CANCELLED
}

# 任务类型
type Task {
  id: ID!
  title: String!
  description: String
  priority: TaskPriority!
  status: TaskStatus!
  completed: Boolean!
  dueDate: String
  createdAt: String!
  updatedAt: String!
}

# 查询类型
type Query {
  tasks(
    status: TaskStatus
    priority: TaskPriority
    search: String
  ): [Task!]!

  task(id: ID!): Task
}

# 创建任务的输入类型
input CreateTaskInput {
  title: String!
  description: String
  priority: TaskPriority = MEDIUM
  dueDate: String
}

# 更新任务的输入类型
input UpdateTaskInput {
  title: String
  description: String
  priority: TaskPriority
  status: TaskStatus
  completed: Boolean
  dueDate: String
}

# Mutation 类型
type Mutation {
  createTask(input: CreateTaskInput!): Task!
  updateTask(id: ID!, input: UpdateTaskInput!): Task!
  deleteTask(id: ID!): Boolean!
}

# Subscription 类型(实时推送)
type Subscription {
  taskCreated: Task!
  taskUpdated: Task!
  taskDeleted: ID!
}

1.3 简单后端 Resolver 思路(伪代码)

javascript 复制代码
// resolvers.js

const tasks = []; // 示例内存数据,真实项目中使用数据库

const resolvers = {
  Query: {
    tasks: (_, args) => {
      // 根据状态、优先级、搜索条件过滤
      let result = tasks;
      if (args.status) {
        result = result.filter(t => t.status === args.status);
      }
      if (args.priority) {
        result = result.filter(t => t.priority === args.priority);
      }
      if (args.search) {
        const q = args.search.toLowerCase();
        result = result.filter(
          t =>
            t.title.toLowerCase().includes(q) ||
            (t.description && t.description.toLowerCase().includes(q))
        );
      }
      return result;
    },
    task: (_, { id }) => tasks.find(t => t.id === id),
  },
  Mutation: {
    createTask: (_, { input }, { pubsub }) => {
      const task = {
        id: String(Date.now()),
        title: input.title,
        description: input.description || '',
        priority: input.priority || 'MEDIUM',
        status: 'PENDING',
        completed: false,
        dueDate: input.dueDate || null,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      };
      tasks.push(task);
      // 发布事件
      pubsub.publish('TASK_CREATED', { taskCreated: task });
      return task;
    },
    updateTask: (_, { id, input }, { pubsub }) => {
      const idx = tasks.findIndex(t => t.id === id);
      if (idx === -1) throw new Error('Task not found');
      const updated = {
        ...tasks[idx],
        ...input,
        updatedAt: new Date().toISOString(),
      };
      tasks[idx] = updated;
      pubsub.publish('TASK_UPDATED', { taskUpdated: updated });
      return updated;
    },
    deleteTask: (_, { id }, { pubsub }) => {
      const idx = tasks.findIndex(t => t.id === id);
      if (idx === -1) return false;
      tasks.splice(idx, 1);
      pubsub.publish('TASK_DELETED', { taskDeleted: id });
      return true;
    },
  },
  Subscription: {
    taskCreated: {
      subscribe: (_, __, { pubsub }) => pubsub.asyncIterator('TASK_CREATED'),
    },
    taskUpdated: {
      subscribe: (_, __, { pubsub }) => pubsub.asyncIterator('TASK_UPDATED'),
    },
    taskDeleted: {
      subscribe: (_, __, { pubsub }) => pubsub.asyncIterator('TASK_DELETED'),
    },
  },
};

第二部分:React 中集成 Apollo Client (2小时)

2.1 安装 Apollo Client

bash 复制代码
npm install @apollo/client graphql

2.2 Apollo Client 初始化

Apollo Client 配置(详细注释版)
javascript 复制代码
// src/graphql/client.js
import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
// 导入WebSocket链接(用于订阅)
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';

// HTTP链接,用于Query和Mutation
const httpLink = new HttpLink({
  uri: process.env.REACT_APP_GRAPHQL_HTTP || 'http://localhost:4000/graphql',
});

// WebSocket链接,用于Subscription
const wsLink = new GraphQLWsLink(
  createClient({
    url: process.env.REACT_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
  })
);

// 使用split根据操作类型选择不同的链接
const splitLink = split(
  ({ query }) => {
    // 获取操作的主定义
    const definition = getMainDefinition(query);
    // 判断是否为订阅操作
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,   // 如果是subscription,使用wsLink
  httpLink, // 否则使用httpLink
);

// 创建Apollo Client实例
export const apolloClient = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          tasks: {
            // 合并策略(用于分页等)
            merge(existing = [], incoming = []) {
              return incoming;
            },
          },
        },
      },
    },
  }),
});
在根组件中注入 ApolloProvider
javascript 复制代码
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import { apolloClient } from './graphql/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <ApolloProvider client={apolloClient}>
    <App />
  </ApolloProvider>
);

2.3 定义 GraphQL 查询与操作

定义 GraphQL 文档(详细注释版)
javascript 复制代码
// src/graphql/queries.js
import { gql } from '@apollo/client';

// 查询任务列表
export const GET_TASKS = gql`
  query GetTasks($status: TaskStatus, $priority: TaskPriority, $search: String) {
    tasks(status: $status, priority: $priority, search: $search) {
      id
      title
      description
      priority
      status
      completed
      dueDate
      createdAt
      updatedAt
    }
  }
`;

// 查询单个任务
export const GET_TASK = gql`
  query GetTask($id: ID!) {
    task(id: $id) {
      id
      title
      description
      priority
      status
      completed
      dueDate
      createdAt
      updatedAt
    }
  }
`;

// 创建任务
export const CREATE_TASK = gql`
  mutation CreateTask($input: CreateTaskInput!) {
    createTask(input: $input) {
      id
      title
      description
      priority
      status
      completed
      dueDate
      createdAt
      updatedAt
    }
  }
`;

// 更新任务
export const UPDATE_TASK = gql`
  mutation UpdateTask($id: ID!, $input: UpdateTaskInput!) {
    updateTask(id: $id, input: $input) {
      id
      title
      description
      priority
      status
      completed
      dueDate
      createdAt
      updatedAt
    }
  }
`;

// 删除任务
export const DELETE_TASK = gql`
  mutation DeleteTask($id: ID!) {
    deleteTask(id: $id)
  }
`;

// 订阅任务创建
export const TASK_CREATED = gql`
  subscription OnTaskCreated {
    taskCreated {
      id
      title
      description
      priority
      status
      completed
      dueDate
      createdAt
      updatedAt
    }
  }
`;

// 订阅任务更新
export const TASK_UPDATED = gql`
  subscription OnTaskUpdated {
    taskUpdated {
      id
      title
      description
      priority
      status
      completed
      dueDate
      createdAt
      updatedAt
    }
  }
`;

// 订阅任务删除
export const TASK_DELETED = gql`
  subscription OnTaskDeleted {
    taskDeleted
  }
`;

第三部分:Query、Mutation 与 Subscription (2小时)

3.1 使用 useQuery 获取数据

任务列表组件(详细注释版)
javascript 复制代码
// src/components/TaskListGraphQL.js
import React, { useState, useMemo } from 'react';
import { useQuery } from '@apollo/client';
import { GET_TASKS } from '../graphql/queries';

function TaskListGraphQL() {
  const [status, setStatus] = useState(null);
  const [priority, setPriority] = useState(null);
  const [search, setSearch] = useState('');

  // 使用useQuery获取数据
  const { data, loading, error, refetch } = useQuery(GET_TASKS, {
    variables: { status, priority, search: '' },
    fetchPolicy: 'cache-and-network',
  });

  const tasks = useMemo(() => data?.tasks ?? [], [data]);

  const handleFilterChange = () => {
    refetch({ status, priority, search });
  };

  if (loading && !data) {
    return <div>Loading tasks...</div>;
  }

  if (error) {
    return (
      <div>
        <p>Error loading tasks: {error.message}</p>
        <button onClick={() => refetch()}>Retry</button>
      </div>
    );
  }

  return (
    <div className="task-list-graphql">
      <h2>Tasks (GraphQL)</h2>
      <div className="filters">
        <select
          value={status || ''}
          onChange={e => setStatus(e.target.value || null)}
        >
          <option value="">All Status</option>
          <option value="PENDING">Pending</option>
          <option value="IN_PROGRESS">In Progress</option>
          <option value="COMPLETED">Completed</option>
        </select>
        <select
          value={priority || ''}
          onChange={e => setPriority(e.target.value || null)}
        >
          <option value="">All Priority</option>
          <option value="LOW">Low</option>
          <option value="MEDIUM">Medium</option>
          <option value="HIGH">High</option>
        </select>
        <input
          placeholder="Search..."
          value={search}
          onChange={e => setSearch(e.target.value)}
        />
        <button onClick={handleFilterChange}>Apply</button>
      </div>

      <ul>
        {tasks.map(task => (
          <li key={task.id}>
            <span>{task.title} [{task.priority}]</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TaskListGraphQL;

3.2 使用 useMutation 修改数据

任务表单组件(详细注释版)
javascript 复制代码
// src/components/TaskFormGraphQL.js
import React, { useState, useEffect } from 'react';
import { useMutation } from '@apollo/client';
import { CREATE_TASK, UPDATE_TASK, GET_TASKS } from '../graphql/queries';

function TaskFormGraphQL({ task, onCompleted }) {
  const [title, setTitle] = useState(task?.title || '');
  const [description, setDescription] = useState(task?.description || '');
  const [priority, setPriority] = useState(task?.priority || 'MEDIUM');

  useEffect(() => {
    if (task) {
      setTitle(task.title);
      setDescription(task.description || '');
      setPriority(task.priority || 'MEDIUM');
    }
  }, [task]);

  const [createTask, { loading: creating }] = useMutation(CREATE_TASK, {
    // 更新缓存:将新任务添加到任务列表
    update(cache, { data }) {
      const newTask = data?.createTask;
      if (!newTask) return;
      const existing = cache.readQuery({ query: GET_TASKS });
      if (!existing?.tasks) return;
      cache.writeQuery({
        query: GET_TASKS,
        data: { tasks: [...existing.tasks, newTask] },
      });
    },
  });

  const [updateTask, { loading: updating }] = useMutation(UPDATE_TASK);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const input = {
      title,
      description,
      priority,
    };

    try {
      if (task) {
        await updateTask({
          variables: { id: task.id, input },
        });
      } else {
        await createTask({
          variables: { input },
        });
      }
      if (onCompleted) onCompleted();
    } catch (err) {
      console.error(err);
    }
  };

  const isSubmitting = creating || updating;

  return (
    <form className="task-form-graphql" onSubmit={handleSubmit}>
      <div>
        <label htmlFor="title">Title</label>
        <input
          id="title"
          value={title}
          onChange={e => setTitle(e.target.value)}
          required
        />
      </div>
      <div>
        <label htmlFor="desc">Description</label>
        <textarea
          id="desc"
          value={description}
          onChange={e => setDescription(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="priority">Priority</label>
        <select
          id="priority"
          value={priority}
          onChange={e => setPriority(e.target.value)}
        >
          <option value="LOW">Low</option>
          <option value="MEDIUM">Medium</option>
          <option value="HIGH">High</option>
        </select>
      </div>
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Saving...' : task ? 'Update Task' : 'Create Task'}
      </button>
    </form>
  );
}

export default TaskFormGraphQL;

3.3 使用 useSubscription 实现实时更新

实时任务列表组件(详细注释版)
javascript 复制代码
// src/components/TaskListRealtime.js
import React, { useEffect } from 'react';
import { useQuery, useSubscription } from '@apollo/client';
import {
  GET_TASKS,
  TASK_CREATED,
  TASK_UPDATED,
  TASK_DELETED,
} from '../graphql/queries';

function TaskListRealtime() {
  const { data, loading, error, subscribeToMore } = useQuery(GET_TASKS);

  // 使用订阅更新
  useEffect(() => {
    // 订阅任务创建
    const unsubscribeCreated = subscribeToMore({
      document: TASK_CREATED,
      updateQuery: (prev, { subscriptionData }) => {
        const newTask = subscriptionData.data?.taskCreated;
        if (!newTask) return prev;
        return {
          ...prev,
          tasks: [...prev.tasks, newTask],
        };
      },
    });

    // 订阅任务更新
    const unsubscribeUpdated = subscribeToMore({
      document: TASK_UPDATED,
      updateQuery: (prev, { subscriptionData }) => {
        const updatedTask = subscriptionData.data?.taskUpdated;
        if (!updatedTask) return prev;
        return {
          ...prev,
          tasks: prev.tasks.map(t => (t.id === updatedTask.id ? updatedTask : t)),
        };
      },
    });

    // 订阅任务删除
    const unsubscribeDeleted = subscribeToMore({
      document: TASK_DELETED,
      updateQuery: (prev, { subscriptionData }) => {
        const deletedId = subscriptionData.data?.taskDeleted;
        if (!deletedId) return prev;
        return {
          ...prev,
          tasks: prev.tasks.filter(t => t.id !== deletedId),
        };
      },
    });

    // 清理订阅
    return () => {
      unsubscribeCreated();
      unsubscribeUpdated();
      unsubscribeDeleted();
    };
  }, [subscribeToMore]);

  if (loading && !data) return <div>Loading tasks...</div>;
  if (error) return <div>Error: {error.message}</div>;

  const tasks = data?.tasks ?? [];

  return (
    <div className="task-list-realtime">
      <h2>Realtime Tasks</h2>
      <ul>
        {tasks.map(t => (
          <li key={t.id}>
            {t.title} [{t.status}] {t.completed ? '(Completed)' : ''}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TaskListRealtime;

第四部分:综合实战项目 (2-3小时)

项目:GraphQL 实时任务管理应用

项目要点
  • 使用 Apollo Client 管理 GraphQL 通信
  • 使用 Query 显示任务列表
  • 使用 Mutation 创建、更新、删除任务
  • 使用 Subscription 实时推送任务变化
  • 集成之前学过的:
    • 状态管理(局部 state 即可,或结合 Redux)
    • 路由(任务详情页)
    • 错误边界与错误提示
    • 性能优化(memo、useMemo)
示例页面结构
  • /tasks-graphql:GraphQL 版任务列表(含实时更新)
  • /tasks-graphql/:id:任务详情页
  • 模态框中使用 TaskFormGraphQL 编辑任务

你可以将前面代码片段组合成一个完整的小项目。


练习题目

基础练习

  1. GraphQL 查询与修改

    • 使用 useQuery 实现任务列表。
    • 使用 useMutation 实现创建、更新、删除任务。
  2. Apollo 缓存

    • 在创建任务后,通过 updaterefetchQueries 保持列表同步。
    • 删除任务后,同样更新缓存移除该任务。

进阶练习

  1. 实时更新

    • 使用 useSubscriptionsubscribeToMore 实现任务创建、更新、删除时的实时更新。
    • 在UI中显示"实时更新"的提示,例如任务列表顶部显示"实时更新中"的标记(不使用表情)。
  2. 错误与 Loading 处理

    • 为每一次 Query / Mutation / Subscription 加上友好的 Loading 和 Error 状态展示。
    • 在错误发生时,将错误同时上报到之前实现的日志服务。

学习检查点

  • 理解 GraphQL Schema、Query、Mutation、Subscription 的基本概念。
  • 能在 React 中配置 Apollo Client,并通过 ApolloProvider 提供客户端。
  • 能使用 useQueryuseMutation 完成基本的数据操作。
  • 能使用 Subscription 实现基于 WebSocket 的实时数据更新。
  • 能结合缓存、错误处理和UI状态,完成一个完整的小型 GraphQL 实时应用。

扩展阅读

到这里,你已经在前 14 天的基础上,把 React 与 GraphQL、实时能力结合起来,形成从前端到数据层的完整知识闭环。接下来可以根据自己的项目需求,选择重点方向继续深入,例如:更复杂的 GraphQL Schema 设计、GraphQL + 微服务、或移动端(React Native)等。

相关推荐
qq_406176142 小时前
React 状态管理完全指南:从入门到选型
前端·javascript·react.js
禹中一只鱼12 小时前
【力扣热题100学习笔记】 - 哈希
java·学习·leetcode·哈希算法
SteveSenna14 小时前
项目:Trossen Arm MuJoCo
人工智能·学习·算法
m0_7473041614 小时前
GNN学习
学习
西洼工作室14 小时前
React轮播图优化:通过延迟 + 动画的组合,彻底消除视觉上的闪烁感
前端·react.js·前端框架
Sagittarius_A*14 小时前
监督学习(Supervised Learning)
人工智能·学习·机器学习·监督学习
qqty121715 小时前
Java进阶学习之路
java·开发语言·学习
WHS-_-202215 小时前
Python 算法题学习笔记一
python·学习·算法
_李小白15 小时前
【OSG学习笔记】Day 22: StateSet 与 StateAttribute (渲染状态)
笔记·学习