Appearance
GraphQL基础
概述
GraphQL是一种用于API的查询语言和运行时,由Facebook开发。与传统REST API不同,GraphQL允许客户端精确指定需要的数据,避免过度获取或获取不足的问题。本文将介绍在React应用中使用GraphQL的基础知识。
GraphQL核心概念
Schema定义
graphql
# 类型定义
type User {
id: ID!
name: String!
email: String!
age: Int
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
tags: [String!]!
createdAt: String!
}
# 查询
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
post(id: ID!): Post
posts(authorId: ID): [Post!]!
searchPosts(query: String!): [Post!]!
}
# 变更
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
# 订阅
type Subscription {
postCreated: Post!
postUpdated(id: ID!): Post!
userOnline(userId: ID!): User!
}
# 输入类型
input CreateUserInput {
name: String!
email: String!
age: Int
}
input UpdateUserInput {
name: String
email: String
age: Int
}
input CreatePostInput {
title: String!
content: String!
authorId: ID!
tags: [String!]
}
input UpdatePostInput {
title: String
content: String
published: Boolean
tags: [String!]
}查询(Query)
graphql
# 基础查询
query GetUser {
user(id: "123") {
id
name
email
}
}
# 查询别名
query GetUsers {
mainUser: user(id: "123") {
id
name
}
secondUser: user(id: "456") {
id
name
}
}
# 嵌套查询
query GetUserWithPosts {
user(id: "123") {
id
name
posts {
id
title
content
}
}
}
# 带参数的查询
query GetPosts($authorId: ID!, $limit: Int) {
posts(authorId: $authorId, limit: $limit) {
id
title
author {
id
name
}
}
}
# 片段(Fragment)
fragment UserFields on User {
id
name
email
createdAt
}
query GetUserData {
user(id: "123") {
...UserFields
posts {
id
title
}
}
}
# 内联片段
query GetContent {
search(query: "test") {
... on User {
id
name
}
... on Post {
id
title
}
}
}变更(Mutation)
graphql
# 创建数据
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
# 更新数据
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}
# 删除数据
mutation DeleteUser($id: ID!) {
deleteUser(id: $id)
}
# 多个mutation
mutation CreateUserAndPost(
$userInput: CreateUserInput!
$postInput: CreatePostInput!
) {
user: createUser(input: $userInput) {
id
name
}
post: createPost(input: $postInput) {
id
title
}
}订阅(Subscription)
graphql
# 订阅新帖子
subscription OnPostCreated {
postCreated {
id
title
author {
id
name
}
}
}
# 订阅特定帖子更新
subscription OnPostUpdated($id: ID!) {
postUpdated(id: $id) {
id
title
content
published
}
}
# 订阅用户在线状态
subscription OnUserOnline($userId: ID!) {
userOnline(userId: $userId) {
id
name
isOnline
}
}GraphQL客户端
Fetch方式
jsx
async function fetchGraphQL(query, variables = {}) {
const response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`,
},
body: JSON.stringify({
query,
variables,
}),
});
const json = await response.json();
if (json.errors) {
throw new Error(json.errors[0].message);
}
return json.data;
}
// 使用
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
`;
fetchGraphQL(query, { id: userId })
.then(data => {
setUser(data.user);
setLoading(false);
})
.catch(error => {
console.error(error);
setLoading(false);
});
}, [userId]);
if (loading) return <div>Loading...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<h2>Posts</h2>
<ul>
{user.posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}GraphQL Request
bash
npm install graphql-request graphqljsx
import { GraphQLClient, gql } from 'graphql-request';
const client = new GraphQLClient('https://api.example.com/graphql', {
headers: {
authorization: `Bearer ${getToken()}`,
},
});
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
const query = gql`
query GetUsers {
users {
id
name
email
}
}
`;
client.request(query)
.then(data => setUsers(data.users))
.catch(error => console.error(error));
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// 带变量的查询
function UserPosts({ userId }) {
const [posts, setPosts] = useState([]);
useEffect(() => {
const query = gql`
query GetUserPosts($userId: ID!) {
posts(authorId: $userId) {
id
title
content
}
}
`;
client.request(query, { userId })
.then(data => setPosts(data.posts))
.catch(error => console.error(error));
}, [userId]);
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</article>
))}
</div>
);
}React Hook封装
useGraphQL Hook
jsx
import { useState, useEffect } from 'react';
function useGraphQL(query, variables = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetchGraphQL(query, variables)
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [query, JSON.stringify(variables)]);
return { data, loading, error };
}
// 使用
function UserProfile({ userId }) {
const { data, loading, error } = useGraphQL(
gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
{ id: userId }
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
</div>
);
}useGraphQLMutation Hook
jsx
function useGraphQLMutation(mutation) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const execute = async (variables) => {
setLoading(true);
setError(null);
try {
const data = await client.request(mutation, variables);
setLoading(false);
return data;
} catch (err) {
setError(err);
setLoading(false);
throw err;
}
};
return { execute, loading, error };
}
// 使用
function CreateUserForm() {
const { execute, loading, error } = useGraphQLMutation(gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`);
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const input = {
name: formData.get('name'),
email: formData.get('email'),
};
try {
const result = await execute({ input });
console.log('User created:', result.createUser);
toast.success('User created successfully');
} catch (err) {
toast.error('Failed to create user');
}
};
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create User'}
</button>
{error && <div className="error">{error.message}</div>}
</form>
);
}错误处理
GraphQL错误
jsx
function handleGraphQLError(error) {
if (error.response) {
const { errors } = error.response;
errors.forEach(err => {
if (err.extensions?.code === 'UNAUTHENTICATED') {
// 未认证错误
localStorage.removeItem('token');
window.location.href = '/login';
} else if (err.extensions?.code === 'FORBIDDEN') {
// 无权限错误
toast.error('You do not have permission');
} else if (err.extensions?.code === 'NOT_FOUND') {
// 资源不存在
toast.error('Resource not found');
} else {
// 其他错误
toast.error(err.message);
}
});
} else {
// 网络错误
toast.error('Network error occurred');
}
}
// 使用
function UserData({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const query = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`;
client.request(query, { id: userId })
.then(data => setUser(data.user))
.catch(error => {
handleGraphQLError(error);
})
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return <div>{user.name}</div>;
}部分错误处理
jsx
async function fetchWithPartialErrors(query, variables) {
const response = await client.rawRequest(query, variables);
const { data, errors } = response;
if (errors && errors.length > 0) {
// 处理部分错误
console.warn('Partial errors:', errors);
// 仍然返回部分数据
return {
data,
errors,
hasErrors: true,
};
}
return {
data,
errors: null,
hasErrors: false,
};
}
// 使用
function Dashboard() {
const [stats, setStats] = useState(null);
const [warnings, setWarnings] = useState([]);
useEffect(() => {
const query = gql`
query GetDashboardStats {
userCount
postCount
revenue
analytics {
views
clicks
}
}
`;
fetchWithPartialErrors(query)
.then(result => {
setStats(result.data);
if (result.hasErrors) {
setWarnings(result.errors.map(e => e.message));
}
});
}, []);
return (
<div>
{warnings.length > 0 && (
<div className="warnings">
{warnings.map((warning, i) => (
<div key={i}>{warning}</div>
))}
</div>
)}
{stats && (
<div>
<div>Users: {stats.userCount}</div>
<div>Posts: {stats.postCount}</div>
<div>Revenue: ${stats.revenue}</div>
</div>
)}
</div>
);
}文件上传
Multipart Request
jsx
async function uploadFile(file, mutation) {
const operations = {
query: mutation,
variables: {
file: null,
},
};
const map = {
'0': ['variables.file'],
};
const formData = new FormData();
formData.append('operations', JSON.stringify(operations));
formData.append('map', JSON.stringify(map));
formData.append('0', file);
const response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: {
'Authorization': `Bearer ${getToken()}`,
},
body: formData,
});
const json = await response.json();
if (json.errors) {
throw new Error(json.errors[0].message);
}
return json.data;
}
// 使用
function FileUpload() {
const [uploading, setUploading] = useState(false);
const handleUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
setUploading(true);
const mutation = `
mutation UploadFile($file: Upload!) {
uploadFile(file: $file) {
id
url
filename
}
}
`;
try {
const result = await uploadFile(file, mutation);
console.log('Uploaded:', result.uploadFile);
toast.success('File uploaded successfully');
} catch (error) {
toast.error('Upload failed');
} finally {
setUploading(false);
}
};
return (
<div>
<input
type="file"
onChange={handleUpload}
disabled={uploading}
/>
{uploading && <div>Uploading...</div>}
</div>
);
}总结
GraphQL基础要点:
- 核心概念:Schema、Query、Mutation、Subscription
- 查询语法:字段选择、参数、别名、片段
- 变更操作:创建、更新、删除数据
- GraphQL客户端:Fetch、graphql-request
- React集成:自定义Hook封装
- 错误处理:GraphQL错误、部分错误
- 文件上传:Multipart请求
GraphQL提供了灵活高效的数据获取方式,是现代Web应用的重要技术选择。
第四部分:GraphQL高级特性
4.1 GraphQL指令
graphql
# 1. @include和@skip指令
query GetUser($id: ID!, $includePosts: Boolean!, $skipComments: Boolean!) {
user(id: $id) {
id
name
posts @include(if: $includePosts) {
id
title
}
comments @skip(if: $skipComments) {
id
text
}
}
}
# 2. @deprecated指令
type User {
id: ID!
name: String!
email: String! @deprecated(reason: "Use contactEmail instead")
contactEmail: String!
}
# 3. 自定义指令
directive @auth(requires: Role = USER) on FIELD_DEFINITION
type Query {
adminData: [Data] @auth(requires: ADMIN)
userData: [Data] @auth(requires: USER)
}
# 4. 客户端使用指令
function useConditionalQuery() {
const [includeDetails, setIncludeDetails] = useState(false);
const query = gql`
query GetProduct($id: ID!, $includeDetails: Boolean!) {
product(id: $id) {
id
name
price
details @include(if: $includeDetails) {
description
specifications
reviews
}
}
}
`;
const { data } = useQuery(query, {
variables: { id: '1', includeDetails }
});
return (
<div>
<button onClick={() => setIncludeDetails(!includeDetails)}>
Toggle Details
</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}4.2 GraphQL联合类型和接口
graphql
# 1. 联合类型
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
# 查询联合类型
query Search($query: String!) {
search(query: $query) {
... on User {
__typename
id
name
email
}
... on Post {
__typename
id
title
author {
name
}
}
... on Comment {
__typename
id
text
author {
name
}
}
}
}
# 2. 接口
interface Node {
id: ID!
createdAt: DateTime!
}
type User implements Node {
id: ID!
createdAt: DateTime!
name: String!
email: String!
}
type Post implements Node {
id: ID!
createdAt: DateTime!
title: String!
content: String!
}
# 查询接口
query GetNodes {
nodes {
id
createdAt
... on User {
name
email
}
... on Post {
title
content
}
}
}
# 3. React中处理联合类型
function SearchResults() {
const { data } = useQuery(gql`
query Search($query: String!) {
search(query: $query) {
__typename
... on User {
id
name
}
... on Post {
id
title
}
... on Comment {
id
text
}
}
}
`, {
variables: { query: 'React' }
});
const renderResult = (result) => {
switch (result.__typename) {
case 'User':
return <UserCard user={result} />;
case 'Post':
return <PostCard post={result} />;
case 'Comment':
return <CommentCard comment={result} />;
default:
return null;
}
};
return (
<div>
{data?.search.map(result => (
<div key={result.id}>
{renderResult(result)}
</div>
))}
</div>
);
}4.3 GraphQL订阅进阶
jsx
// 1. WebSocket订阅
import { createClient } from 'graphql-ws';
const wsClient = createClient({
url: 'ws://localhost:4000/graphql',
connectionParams: () => ({
authToken: localStorage.getItem('token')
}),
on: {
connected: () => console.log('Connected'),
closed: () => console.log('Closed')
}
});
// 2. 订阅Hook
function useGraphQLSubscription(query, options = {}) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const unsubscribe = wsClient.subscribe(
{ query, variables: options.variables },
{
next: (value) => {
setData(value.data);
options.onData?.(value.data);
},
error: (err) => {
setError(err);
options.onError?.(err);
},
complete: () => {
options.onComplete?.();
}
}
);
return () => {
unsubscribe();
};
}, [query, options.variables]);
return { data, error };
}
// 3. 实时消息系统
function ChatRoom({ roomId }) {
const { data: messages } = useGraphQLSubscription(
gql`
subscription OnMessageAdded($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
text
author {
id
name
}
createdAt
}
}
`,
{
variables: { roomId },
onData: (newMessage) => {
console.log('New message:', newMessage);
}
}
);
return (
<div>
{messages?.messageAdded.map(msg => (
<div key={msg.id}>
<strong>{msg.author.name}:</strong> {msg.text}
</div>
))}
</div>
);
}
// 4. 订阅管理器
class SubscriptionManager {
constructor() {
this.subscriptions = new Map();
}
subscribe(key, subscription, options) {
if (this.subscriptions.has(key)) {
return;
}
const unsubscribe = wsClient.subscribe(subscription, options);
this.subscriptions.set(key, unsubscribe);
}
unsubscribe(key) {
const unsubscribe = this.subscriptions.get(key);
if (unsubscribe) {
unsubscribe();
this.subscriptions.delete(key);
}
}
unsubscribeAll() {
this.subscriptions.forEach(unsubscribe => unsubscribe());
this.subscriptions.clear();
}
}
const subManager = new SubscriptionManager();4.4 GraphQL缓存策略
jsx
// 1. 简单内存缓存
class GraphQLCache {
constructor() {
this.cache = new Map();
}
generateKey(query, variables) {
return `${query}:${JSON.stringify(variables || {})}`;
}
get(query, variables) {
const key = this.generateKey(query, variables);
const cached = this.cache.get(key);
if (!cached) return null;
// 检查是否过期
if (Date.now() - cached.timestamp > cached.ttl) {
this.cache.delete(key);
return null;
}
return cached.data;
}
set(query, variables, data, ttl = 60000) {
const key = this.generateKey(query, variables);
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl
});
}
invalidate(query, variables) {
const key = this.generateKey(query, variables);
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
}
const cache = new GraphQLCache();
// 2. 带缓存的查询Hook
function useCachedGraphQLQuery(query, variables, options = {}) {
const [data, setData] = useState(() => cache.get(query, variables));
const [loading, setLoading] = useState(!data);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
const cached = cache.get(query, variables);
if (cached) {
setData(cached);
setLoading(false);
return;
}
setLoading(true);
try {
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables })
});
const result = await response.json();
if (result.errors) {
throw new Error(result.errors[0].message);
}
cache.set(query, variables, result.data, options.ttl);
setData(result.data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [query, JSON.stringify(variables), options.ttl]);
const refetch = useCallback(() => {
cache.invalidate(query, variables);
return fetchData();
}, [query, variables]);
return { data, loading, error, refetch };
}
// 3. 规范化缓存
class NormalizedGraphQLCache {
constructor() {
this.entities = {};
this.queryResults = new Map();
}
normalize(data, typename) {
if (!data || typeof data !== 'object') return data;
if (Array.isArray(data)) {
return data.map(item => this.normalize(item, typename));
}
const id = data.id || data._id;
const type = data.__typename || typename;
if (id && type) {
const key = `${type}:${id}`;
this.entities[key] = { ...data };
return { __ref: key };
}
const normalized = {};
for (const [key, value] of Object.entries(data)) {
normalized[key] = this.normalize(value, key);
}
return normalized;
}
denormalize(data) {
if (!data || typeof data !== 'object') return data;
if (Array.isArray(data)) {
return data.map(item => this.denormalize(item));
}
if (data.__ref) {
const entity = this.entities[data.__ref];
return entity ? this.denormalize(entity) : null;
}
const denormalized = {};
for (const [key, value] of Object.entries(data)) {
denormalized[key] = this.denormalize(value);
}
return denormalized;
}
writeQuery(query, variables, data) {
const normalized = this.normalize(data);
const key = `${query}:${JSON.stringify(variables)}`;
this.queryResults.set(key, normalized);
}
readQuery(query, variables) {
const key = `${query}:${JSON.stringify(variables)}`;
const normalized = this.queryResults.get(key);
return normalized ? this.denormalize(normalized) : null;
}
updateEntity(typename, id, updates) {
const key = `${typename}:${id}`;
if (this.entities[key]) {
this.entities[key] = { ...this.entities[key], ...updates };
}
}
}
const normalizedCache = new NormalizedGraphQLCache();4.5 GraphQL批量请求
jsx
// 1. 批量请求处理器
class GraphQLBatcher {
constructor(endpoint, options = {}) {
this.endpoint = endpoint;
this.batchInterval = options.batchInterval || 10;
this.maxBatchSize = options.maxBatchSize || 10;
this.queue = [];
this.timer = null;
}
enqueue(query, variables) {
return new Promise((resolve, reject) => {
this.queue.push({ query, variables, resolve, reject });
if (this.queue.length >= this.maxBatchSize) {
this.flush();
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.batchInterval);
}
});
}
async flush() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.maxBatchSize);
const batchedQuery = batch.map((item, index) => ({
query: item.query,
variables: item.variables,
operationName: `Operation${index}`
}));
try {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(batchedQuery)
});
const results = await response.json();
results.forEach((result, index) => {
if (result.errors) {
batch[index].reject(new Error(result.errors[0].message));
} else {
batch[index].resolve(result.data);
}
});
} catch (error) {
batch.forEach(item => item.reject(error));
}
}
}
const batcher = new GraphQLBatcher('/graphql');
// 2. 使用批量请求
function useBatchedQuery(query, variables) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
batcher.enqueue(query, variables)
.then(result => {
setData(result);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [query, JSON.stringify(variables)]);
return { data, loading, error };
}
// 3. DataLoader模式
class GraphQLDataLoader {
constructor(batchLoadFn) {
this.batchLoadFn = batchLoadFn;
this.queue = [];
this.cache = new Map();
}
load(key) {
const cached = this.cache.get(key);
if (cached) return Promise.resolve(cached);
return new Promise((resolve, reject) => {
this.queue.push({ key, resolve, reject });
if (this.queue.length === 1) {
process.nextTick(() => this.dispatch());
}
});
}
async dispatch() {
const queue = this.queue;
this.queue = [];
const keys = queue.map(item => item.key);
try {
const results = await this.batchLoadFn(keys);
queue.forEach((item, index) => {
const result = results[index];
this.cache.set(item.key, result);
item.resolve(result);
});
} catch (error) {
queue.forEach(item => item.reject(error));
}
}
clear(key) {
if (key) {
this.cache.delete(key);
} else {
this.cache.clear();
}
}
}
// 使用DataLoader
const userLoader = new GraphQLDataLoader(async (userIds) => {
const query = gql`
query GetUsers($ids: [ID!]!) {
users(ids: $ids) {
id
name
email
}
}
`;
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: { ids: userIds } })
});
const { data } = await response.json();
return data.users;
});
function UserDisplay({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
userLoader.load(userId).then(setUser);
}, [userId]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}GraphQL最佳实践总结
1. 高级特性
✅ 指令使用
✅ 联合类型和接口
✅ 订阅实时通信
✅ 文件上传
2. 缓存策略
✅ 内存缓存
✅ 规范化缓存
✅ 缓存失效
✅ 持久化缓存
3. 性能优化
✅ 批量请求
✅ DataLoader模式
✅ 查询优化
✅ 字段选择
4. 错误处理
✅ 部分错误处理
✅ 网络错误重试
✅ 错误边界
✅ 错误追踪
5. 类型安全
✅ TypeScript集成
✅ 代码生成
✅ 类型检查
✅ 运行时验证GraphQL提供了强大的数据查询能力,合理运用这些高级特性和优化策略能够构建高性能的数据层。