学习目标
- 理解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编辑任务
你可以将前面代码片段组合成一个完整的小项目。
练习题目
基础练习
-
GraphQL 查询与修改
- 使用
useQuery实现任务列表。 - 使用
useMutation实现创建、更新、删除任务。
- 使用
-
Apollo 缓存
- 在创建任务后,通过
update或refetchQueries保持列表同步。 - 删除任务后,同样更新缓存移除该任务。
- 在创建任务后,通过
进阶练习
-
实时更新
- 使用
useSubscription或subscribeToMore实现任务创建、更新、删除时的实时更新。 - 在UI中显示"实时更新"的提示,例如任务列表顶部显示"实时更新中"的标记(不使用表情)。
- 使用
-
错误与 Loading 处理
- 为每一次 Query / Mutation / Subscription 加上友好的 Loading 和 Error 状态展示。
- 在错误发生时,将错误同时上报到之前实现的日志服务。
学习检查点
- 理解 GraphQL Schema、Query、Mutation、Subscription 的基本概念。
- 能在 React 中配置 Apollo Client,并通过
ApolloProvider提供客户端。 - 能使用
useQuery、useMutation完成基本的数据操作。 - 能使用 Subscription 实现基于 WebSocket 的实时数据更新。
- 能结合缓存、错误处理和UI状态,完成一个完整的小型 GraphQL 实时应用。
扩展阅读
- GraphQL 官方文档:https://graphql.org/learn
- Apollo Client 文档:https://www.apollographql.com/docs/react
- GraphQL 与 REST 对比:https://graphql.org/learn/thinking-in-graphs
到这里,你已经在前 14 天的基础上,把 React 与 GraphQL、实时能力结合起来,形成从前端到数据层的完整知识闭环。接下来可以根据自己的项目需求,选择重点方向继续深入,例如:更复杂的 GraphQL Schema 设计、GraphQL + 微服务、或移动端(React Native)等。