𝑻𝒆𝒏𝑪𝒍𝒂𝒘正在头脑风暴···
𝑻𝒆𝒏𝑲𝒊𝑺𝒆𝒀𝒂の𝑨𝒈𝒆𝒏𝒕助手
𝑻𝒆𝒏-𝒇𝒍𝒂𝒔𝒉

GraphQL查询语言入门

前言

GraphQL是Facebook开发的一种API查询语言和运行时,作为REST API的替代方案。它让客户端能够精确指定需要的数据,避免了过度获取或获取不足的问题。本文将带你从零开始学习GraphQL,包括其基本概念、查询语法、服务器和客户端实现,以及实际应用场景。

1. GraphQL基础概念

1.1 什么是GraphQL

GraphQL是一种用于API的查询语言,也是一个满足你数据查询的运行时。GraphQL有以下核心特点:

  • 客户端驱动:客户端决定获取什么数据
  • 单一端点:一个GraphQL服务通常只有一个端点
  • 强类型系统:使用Schema定义数据结构
  • 无版本控制:通过演化而非版本控制来扩展API
  • 实时功能:支持订阅功能

1.2 GraphQL vs REST

特性RESTGraphQL
URL结构多个端点,每个端点对应一个资源单一端点
数据获取可能获取过多或过少数据精确获取所需数据
版本控制需要版本控制通过Schema演化
请求方式HTTP方法(GET, POST等)主要是POST
批量请求多个HTTP请求单个请求可包含多个查询
实时更新通常需要轮询或WebSocket支持订阅

1.3 GraphQL核心组件

  • Schema:定义API的数据结构和可执行操作
  • Type:数据类型的定义(如String, Int, Object等)
  • Query:获取数据的操作
  • Mutation:修改数据的操作
  • Subscription:实时数据更新的订阅
  • Resolver:实现Schema中定义的操作的函数

2. Schema定义

2.1 基本类型

# 标量类型
type Query {
# 基本类型
id: ID!
name: String!
age: Int
price: Float
isActive: Boolean
}

# 枚举类型
enum UserRole {
ADMIN
USER
GUEST
}

# 输入类型
type Mutation {
createUser(input: UserInput!): User!
}

input UserInput {
name: String!
email: String!
role: UserRole
}

2.2 对象类型

type User {
id: ID!
name: String!
email: String!
age: Int
role: UserRole!
createdAt: String!
posts: [Post!]!
friends: [User!]!
}

type Post {
id: ID!
title: String!
content: String!
author: User!
publishedAt: String!
comments: [Comment!]!
}

type Comment {
id: ID!
text: String!
author: User!
post: Post!
createdAt: String!
}

type Book {
id: ID!
title: String!
author: String!
publishedAt: String!
rating: Float
tags: [String!]!
}

2.3 接口和联合类型

# 接口
interface Node {
id: ID!
}

interface Content {
id: ID!
title: String!
createdAt: String!
}

# 实现接口的类型
type User implements Node {
id: ID!
name: String!
email: String!
role: UserRole!
}

type Post implements Node & Content {
id: ID!
title: String!
content: String!
author: User!
publishedAt: String!
comments: [Comment!]!
}

# 联合类型
union SearchResult = User | Post | Book

type Query {
search(term: String!): [SearchResult!]!
}

2.4 Query和Mutation

type Query {
# 获取单个用户
user(id: ID!): User

# 获取用户列表
users(
first: Int = 10
after: String
filter: UserFilter
sort: UserSort
): UserConnection!

# 获取用户的帖子
userPosts(userId: ID!): [Post!]!

# 获取所有帖子
posts(filter: PostFilter): [Post!]!

# 获取单个帖子
post(id: ID!): Post

# 搜索功能
search(term: String!): [SearchResult!]!
}

type Mutation {
# 用户操作
createUser(input: UserInput!): CreateUserPayload!
updateUser(id: ID!, input: UserInput!): UpdateUserPayload!
deleteUser(id: ID!): DeleteUserPayload!

# 帖子操作
createPost(input: PostInput!): CreatePostPayload!
updatePost(id: ID!, input: PostInput!): UpdatePostPayload!
deletePost(id: ID!): DeletePostPayload!

# 评论操作
createComment(input: CommentInput!): CreateCommentPayload!
}

type Subscription {
# 订阅新用户
newUser: User!

# 订阅新帖子
newPost: Post!

# 订阅评论
newComment(postId: ID!): Comment!
}

3. GraphQL查询语法

3.1 基本查询

# 简单查询
query {
user(id: "1") {
id
name
email
}
}

# 嵌套查询
query {
user(id: "1") {
id
name
email
posts {
id
title
content
publishedAt
}
}
}

# 别名
query {
user: getUser(id: "1") {
id
name
email
}
post: getPost(id: "1") {
id
title
content
}
}

# 片段
query {
user(id: "1") {
...userFields
}
}

fragment userFields on User {
id
name
email
posts {
id
title
content
}
}

# 变量
query GetUser($id: ID!, $withPosts: Boolean = false) {
user(id: $id) {
id
name
email
posts @include(if: $withPosts) {
id
title
content
}
}
}

3.2 高级查询功能

# 查询参数
query {
users(
first: 10
after: "cursor123"
filter: {
role: ADMIN
createdAt: { gte: "2023-01-01" }
}
sort: {
field: createdAt
direction: DESC
}
) {
edges {
node {
id
name
email
posts {
id
title
}
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}

# 条件查询
query {
users {
id
name
email
posts @include(if: $showPosts) {
id
title
}
}
}

# 批量查询
query {
user(id: "1") {
id
name
email
}
post(id: "1") {
id
title
content
}
users(first: 5) {
id
name
}
}

# 变量定义
query GetPosts($limit: Int!, $published: Boolean!) {
posts(limit: $limit, published: $published) {
id
title
content
author {
id
name
}
comments {
id
text
author {
id
name
}
}
}
}

# 指令
query GetFilteredUsers($activeOnly: Boolean!) {
users {
id
name
email
isActive @include(if: $activeOnly)
posts @skip(if: $activeOnly) {
id
title
}
}
}

4. 服务器端实现

4.1 使用Apollo Server

npm install apollo-server graphql
// server.js
const { ApolloServer, gql } = require('apollo-server');
const { v4: uuidv4 } = require('uuid');

// 模拟数据库
const db = {
users: [
{
id: '1',
name: 'John Doe',
email: 'john@example.com',
age: 30,
role: 'ADMIN',
createdAt: '2023-01-01T00:00:00Z',
posts: ['1', '2'],
},
{
id: '2',
name: 'Jane Smith',
email: 'jane@example.com',
age: 25,
role: 'USER',
createdAt: '2023-02-01T00:00:00Z',
posts: ['3'],
},
],
posts: [
{
id: '1',
title: 'Hello World',
content: 'This is my first post',
author: '1',
publishedAt: '2023-01-15T00:00:00Z',
comments: ['1'],
},
{
id: '2',
title: 'GraphQL Basics',
content: 'Learn GraphQL fundamentals',
author: '1',
publishedAt: '2023-01-20T00:00:00Z',
comments: [],
},
{
id: '3',
title: 'React & GraphQL',
content: 'Integrating React with GraphQL',
author: '2',
publishedAt: '2023-02-10T00:00:00Z',
comments: ['2'],
},
],
comments: [
{
id: '1',
text: 'Great post!',
author: '2',
post: '1',
createdAt: '2023-01-16T00:00:00Z',
},
{
id: '2',
text: 'Very helpful, thanks!',
author: '1',
post: '3',
createdAt: '2023-02-11T00:00:00Z',
},
],
};

// 定义Schema
const typeDefs = gql`
type UserRole {
ADMIN
USER
GUEST
}

scalar DateTime

type User {
id: ID!
name: String!
email: String!
age: Int
role: UserRole!
createdAt: DateTime!
posts: [Post!]!
friends: [User!]!
}

type Post {
id: ID!
title: String!
content: String!
author: User!
publishedAt: DateTime!
comments: [Comment!]!
}

type Comment {
id: ID!
text: String!
author: User!
post: Post!
createdAt: DateTime!
}

type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
posts: [Post!]!
comment(id: ID!): Comment
comments: [Comment!]!
}

type Mutation {
createUser(name: String!, email: String!, age: Int): User
updateUser(id: ID!, name: String, email: String, age: Int): User
deleteUser(id: ID!): User
createPost(title: String!, content: String!, authorId: ID!): Post
updatePost(id: ID!, title: String, content: String): Post
deletePost(id: ID!): Post
createComment(text: String!, authorId: ID!, postId: ID!): Comment
}

type Subscription {
newUser: User!
newPost: Post!
newComment: Comment!
}
`;

// Resolvers
const resolvers = {
Query: {
user: (parent, { id }, context) => {
return db.users.find(user => user.id === id);
},
users: () => db.users,
post: (parent, { id }) => {
return db.posts.find(post => post.id === id);
},
posts: () => db.posts,
comment: (parent, { id }) => {
return db.comments.find(comment => comment.id === id);
},
comments: () => db.comments,
},

Mutation: {
createUser: (parent, { name, email, age }) => {
const newUser = {
id: uuidv4(),
name,
email,
age,
role: 'USER',
createdAt: new Date().toISOString(),
posts: [],
};
db.users.push(newUser);
return newUser;
},
updateUser: (parent, { id, name, email, age }) => {
const userIndex = db.users.findIndex(user => user.id === id);
if (userIndex !== -1) {
db.users[userIndex] = {
...db.users[userIndex],
name: name || db.users[userIndex].name,
email: email || db.users[userIndex].email,
age: age !== undefined ? age : db.users[userIndex].age,
};
return db.users[userIndex];
}
return null;
},
deleteUser: (parent, { id }) => {
const userIndex = db.users.findIndex(user => user.id === id);
if (userIndex !== -1) {
const deletedUser = db.users.splice(userIndex, 1)[0];
return deletedUser;
}
return null;
},
createPost: (parent, { title, content, authorId }) => {
const newPost = {
id: uuidv4(),
title,
content,
author: authorId,
publishedAt: new Date().toISOString(),
comments: [],
};
db.posts.push(newPost);

// 更新用户的posts列表
const user = db.users.find(u => u.id === authorId);
if (user) {
user.posts.push(newPost.id);
}

return newPost;
},
updatePost: (parent, { id, title, content }) => {
const postIndex = db.posts.findIndex(post => post.id === id);
if (postIndex !== -1) {
db.posts[postIndex] = {
...db.posts[postIndex],
title: title || db.posts[postIndex].title,
content: content || db.posts[postIndex].content,
};
return db.posts[postIndex];
}
return null;
},
deletePost: (parent, { id }) => {
const postIndex = db.posts.findIndex(post => post.id === id);
if (postIndex !== -1) {
const deletedPost = db.posts.splice(postIndex, 1)[0];

// 从用户的posts列表中移除
db.users.forEach(user => {
user.posts = user.posts.filter(postId => postId !== id);
});

return deletedPost;
}
return null;
},
createComment: (parent, { text, authorId, postId }) => {
const newComment = {
id: uuidv4(),
text,
author: authorId,
post: postId,
createdAt: new Date().toISOString(),
};
db.comments.push(newComment);

// 更新帖子的comments列表
const post = db.posts.find(p => p.id === postId);
if (post) {
post.comments.push(newComment.id);
}

return newComment;
},
},

Subscription: {
newUser: {
subscribe: (parent, args, { pubsub }) => {
return pubsub.asyncIterator('NEW_USER');
},
},
newPost: {
subscribe: (parent, args, { pubsub }) => {
return pubsub.asyncIterator('NEW_POST');
},
},
newComment: {
subscribe: (parent, args, { pubsub }) => {
return pubsub.asyncIterator('NEW_COMMENT');
},
},
},

User: {
posts: (parent) => {
return parent.posts.map(postId =>
db.posts.find(p => p.id === postId)
);
},
friends: (parent) => {
// 简化实现,返回所有其他用户作为朋友
return db.users.filter(user => user.id !== parent.id);
},
},

Post: {
author: (parent) => {
return db.users.find(user => user.id === parent.author);
},
comments: (parent) => {
return parent.comments.map(commentId =>
db.comments.find(c => c.id === commentId)
);
},
},

Comment: {
author: (parent) => {
return db.users.find(user => user.id === parent.author);
},
post: (parent) => {
return db.posts.find(post => post.id === parent.post);
},
},
};

// 创建服务器
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// 在这里添加认证逻辑
return {
user: req.headers.user, // 示例:从headers获取用户信息
};
},
});

// 启动服务器
server.listen().then(({ url }) => {
console.log(`Server ready at ${url}`);
});

4.2 使用GraphQL Yoga

npm install @graphql-yoga/node graphql
// yoga-server.js
import { createServer } from '@graphql-yoga/node';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

// 模拟数据库
const db = {
users: [
{
id: '1',
name: 'John Doe',
email: 'john@example.com',
role: 'ADMIN',
},
{
id: '2',
name: 'Jane Smith',
email: 'jane@example.com',
role: 'USER',
},
],
};

// 定义Schema
const typeDefs = `
type User {
id: ID!
name: String!
email: String!
role: String!
}

type Query {
users: [User!]!
user(id: ID!): User
}

type Mutation {
createUser(name: String!, email: String!): User
}

type Subscription {
newUser: User!
}
`;

// Resolvers
const resolvers = {
Query: {
users: () => db.users,
user: (parent, { id }) => {
return db.users.find(user => user.id === id);
},
},

Mutation: {
createUser: (parent, { name, email }) => {
const newUser = {
id: String(db.users.length + 1),
name,
email,
role: 'USER',
};
db.users.push(newUser);

// 发布订阅事件
pubsub.publish('NEW_USER', { newUser });

return newUser;
},
},

Subscription: {
newUser: {
subscribe: () => pubsub.asyncIterator(['NEW_USER']),
},
},
};

// 创建服务器
const server = createServer({
schema: {
typeDefs,
resolvers,
},
// 添加中间件
middleware: [
(request, response, next) => {
console.log('Request received:', request.request.body);
next();
},
],
});

// 启动服务器
server.start().then(() => {
console.log('Server is running on http://localhost:4000');
});

5. 客户端实现

5.1 Apollo Client

npm install @apollo/client graphql
// client.js
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
errorPolicy: 'all',
},
query: {
errorPolicy: 'all',
},
},
});

// App.jsx
import React from 'react';
import { ApolloProvider } from '@apollo/client';
import client from './client';
import UserList from './components/UserList';
import PostList from './components/PostList';
import AddUser from './components/AddUser';

function App() {
return (
<ApolloProvider client={client}>
<div>
<h1>GraphQL App</h1>
<UserList />
<hr />
<PostList />
<hr />
<AddUser />
</div>
</ApolloProvider>
);
}

export default App;

// components/UserList.jsx
import React from 'react';
import { useQuery, gql } from '@apollo/client';

const GET_USERS = gql`
query GetUsers {
users {
id
name
email
role
}
}
`;

const UserList = () => {
const { loading, error, data } = useQuery(GET_USERS);

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

return (
<div>
<h2>Users</h2>
{data.users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
<p>Role: {user.role}</p>
</div>
))}
</div>
);
};

export default UserList;

// components/AddUser.jsx
import React, { useState } from 'react';
import { useMutation, gql } from '@apollo/client';

const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
role
}
}
`;

const AddUser = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [createUser, { loading, error }] = useMutation(CREATE_USER);

const handleSubmit = (e) => {
e.preventDefault();
createUser({ variables: { name, email } });
setName('');
setEmail('');
};

if (error) return <div>Error: {error.message}</div>;

return (
<div>
<h2>Add User</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
required
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Adding...' : 'Add User'}
</button>
</form>
</div>
);
};

export default AddUser;

5.2 React Query + GraphQL

npm install react-query graphql-fetch
// components/UserList.jsx
import React from 'react';
import { useQuery } from 'react-query';
import { graphql } from 'graphql-fetch';

const client = graphql('http://localhost:4000/graphql');

const GET_USERS = `
query GetUsers {
users {
id
name
email
role
}
}
`;

const UserList = () => {
const { data, error, isLoading } = useQuery('users', () => client(GET_USERS));

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
<div>
<h2>Users</h2>
{data.users.map(user => (
<div key={user.id} style={{ marginBottom: '20px', padding: '10px', border: '1px solid #ccc' }}>
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
<p>Role: {user.role}</p>
</div>
))}
</div>
);
};

export default UserList;

// components/AddUser.jsx
import React, { useState } from 'react';
import { useMutation, useQueryClient } from 'react-query';
import { graphql } from 'graphql-fetch';

const client = graphql('http://localhost:4000/graphql');

const CREATE_USER = `
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
role
}
}
`;

const AddUser = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const queryClient = useQueryClient();

const mutation = useMutation(
(variables) => client(CREATE_USER, variables),
{
onSuccess: () => {
queryClient.invalidateQueries('users');
},
}
);

const handleSubmit = (e) => {
e.preventDefault();
mutation.mutate({ name, email });
setName('');
setEmail('');
};

if (mutation.error) {
return <div>Error: {mutation.error.message}</div>;
}

return (
<div>
<h2>Add User</h2>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<button type="submit" disabled={mutation.isLoading}>
{mutation.isLoading ? 'Adding...' : 'Add User'}
</button>
</form>
</div>
);
};

export default AddUser;

6. 高级特性

6.1 订阅功能

// 服务器端
const typeDefs = `
type User {
id: ID!
name: String!
email: String!
}

type Post {
id: ID!
title: String!
content: String!
author: User!
}

type Comment {
id: ID!
text: String!
author: User!
post: Post!
}

type Query {
users: [User!]!
posts: [Post!]!
comments: [Comment!]!
}

type Mutation {
createUser(name: String!, email: String!): User
createPost(title: String!, content: String!, authorId: ID!): Post
createComment(text: String!, authorId: ID!, postId: ID!): Comment
}

type Subscription {
newUser: User!
newPost: Post!
newComment: Comment!
}
`;

const resolvers = {
Subscription: {
newUser: {
subscribe: () => pubsub.asyncIterator(['NEW_USER']),
},
newPost: {
subscribe: () => pubsub.asyncIterator(['NEW_POST']),
},
newComment: {
subscribe: () => pubsub.asyncIterator(['NEW_COMMENT']),
},
},
};
// 客户端
import { useSubscription, gql } from '@apollo/client';

const NEW_USER_SUBSCRIPTION = gql`
subscription NewUser {
newUser {
id
name
email
}
}
`;

const UserListWithSubscription = () => {
const { data, loading, error } = useSubscription(NEW_USER_SUBSCRIPTION);

const { users } = useQuery(GET_USERS);

const allUsers = data ? [...users, data.newUser] : users;

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

return (
<div>
<h2>Users (Live)</h2>
{allUsers.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
</div>
))}
</div>
);
};

6.2 缓存策略

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache({
typePolicies: {
User: {
fields: {
posts: {
merge(existing = [], incoming) {
return [...incoming, ...existing];
},
},
},
},
Post: {
fields: {
comments: {
merge(existing = [], incoming) {
return [...incoming, ...existing];
},
},
},
},
},
}),
});

// 使用refetchQueries
const updateUser = useMutation(UPDATE_USER, {
refetchQueries: ['GetUser', 'GetUsers'],
onCompleted: () => {
// 成功后的回调
},
});

// 使用updateQuery
const addPost = useMutation(ADD_POST, {
update: (cache, { data: { addPost } }) => {
try {
const { posts } = cache.readQuery({ query: GET_POSTS });
cache.writeQuery({
query: GET_POSTS,
data: { posts: [addPost, ...posts] },
});
} catch (error) {
// 忽略错误
}
},
});

6.3 分页实现

# 服务器端Schema
type Query {
posts(
first: Int = 10
after: String
filter: PostFilter
): PostConnection!
}

type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}

type PostEdge {
node: Post!
cursor: String!
}

type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
// 服务器端Resolver
const resolvers = {
Query: {
posts: (parent, { first, after, filter }) => {
let filteredPosts = [...db.posts];

// 应用过滤
if (filter) {
filteredPosts = filteredPosts.filter(post => {
if (filter.authorId && post.author !== filter.authorId) {
return false;
}
if (filter.search) {
const searchLower = filter.search.toLowerCase();
return (
post.title.toLowerCase().includes(searchLower) ||
post.content.toLowerCase().includes(searchLower)
);
}
return true;
});
}

// 排序
filteredPosts.sort((a, b) =>
new Date(b.publishedAt) - new Date(a.publishedAt)
);

// 分页
const totalCount = filteredPosts.length;
const cursorIndex = after ?
filteredPosts.findIndex(p => p.id === after) + 1 : 0;

const posts = filteredPosts.slice(cursorIndex, cursorIndex + first);

// 创建edges和pageInfo
const edges = posts.map(post => ({
node: post,
cursor: post.id,
}));

const hasNextPage = cursorIndex + first < totalCount;
const hasPreviousPage = cursorIndex > 0;

return {
edges,
pageInfo: {
hasNextPage,
hasPreviousPage,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount,
};
},
},
};
// 客户端分页组件
import { useQuery, gql } from '@apollo/client';

const GET_POSTS = gql`
query GetPosts($first: Int = 10, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
id
title
content
author {
id
name
}
publishedAt
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`;

const PostList = () => {
const [fetchMoreState, setFetchMoreState] = useState({
after: null,
first: 10,
hasMore: true,
});

const { loading, error, data, fetchMore } = useQuery(GET_POSTS, {
variables: {
first: fetchMoreState.first,
after: fetchMoreState.after,
},
});

const loadMorePosts = () => {
if (!fetchMoreState.hasMore) return;

fetchMore({
variables: {
after: data.posts.edges[data.posts.edges.length - 1].cursor,
first: fetchMoreState.first,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;

const newEdges = fetchMoreResult.posts.edges;
const pageInfo = fetchMoreResult.posts.pageInfo;

return {
posts: {
...prev.posts,
edges: [...prev.posts.edges, ...newEdges],
pageInfo,
totalCount: fetchMoreResult.posts.totalCount,
},
};
},
});
};

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

const posts = data.posts.edges.map(edge => edge.node);
const hasMore = data.posts.pageInfo.hasNextPage;

return (
<div>
<h2>Posts</h2>
<div>
{posts.map(post => (
<div key={post.id} style={{ marginBottom: '20px', padding: '10px', border: '1px solid #ccc' }}>
<h3>{post.title}</h3>
<p>{post.content}</p>
<p>By: {post.author.name}</p>
<p>Published: {new Date(post.publishedAt).toLocaleDateString()}</p>
</div>
))}
</div>
<button
onClick={loadMorePosts}
disabled={!hasMore}
style={{ marginTop: '20px' }}
>
{hasMore ? 'Load More' : 'No More Posts'}
</button>
</div>
);
};

7. 实际应用场景

7.1 电商应用

# 电商GraphQL Schema
type Query {
products(
first: Int = 10
after: String
filter: ProductFilter
sort: ProductSort
): ProductConnection!

categories: [Category!]!

category(id: ID!): Category

searchProducts(term: String!): [Product!]!
}

type Mutation {
addToCart(productId: ID!, quantity: Int!): CartItem!
removeFromCart(productId: ID!): CartItem!
updateCartItem(productId: ID!, quantity: Int!): CartItem!
createOrder(input: OrderInput!): Order!
}

type Product {
id: ID!
name: String!
description: String!
price: Float!
images: [ProductImage!]!
category: Category!
variants: [ProductVariant!]!
reviews: [ProductReview!]!
inventory: Inventory!
}

type Category {
id: ID!
name: String!
slug: String!
description: String?
products: [Product!]!
}

type ProductImage {
id: ID!
url: String!
altText: String
}

type ProductVariant {
id: ID!
name: String!
sku: String!
price: Float!
options: [ProductOptionValue!]!
}

type ProductOptionValue {
option: String!
value: String!
}

type ProductReview {
id: ID!
rating: Int!
comment: String!
author: User!
createdAt: String!
}

type Inventory {
id: ID!
quantity: Int!
lowStockThreshold: Int!
trackInventory: Boolean!
}

type CartItem {
id: ID!
product: Product!
variant: ProductVariant?
quantity: Int!
price: Float!
}

type Order {
id: ID!
status: OrderStatus!
items: [OrderItem!]!
subtotal: Float!
tax: Float!
shipping: Float!
total: Float!
createdAt: String!
}

enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}

type OrderItem {
id: ID!
product: Product!
variant: ProductVariant?
quantity: Int!
price: Float!
}

7.2 社交媒体应用

# 社交媒体GraphQL Schema
type Query {
user(id: ID!): User
users: [User!]!

post(id: ID!): Post
posts(
first: Int = 10
after: String
filter: PostFilter
): PostConnection!

feed(first: Int = 10, after: String): PostConnection!

search(term: String!, type: SearchType): SearchResults!
}

type Mutation {
createUser(input: UserInput!): User!
updateUser(id: ID!, input: UserInput!): User!

createPost(input: PostInput!): Post!
updatePost(id: ID!, input: PostInput!): Post!
deletePost(id: ID!): Boolean!

createComment(input: CommentInput!): Comment!
likePost(postId: ID!): Like!
unlikePost(postId: ID!): Boolean!

follow(userId: ID!): Follow!
unfollow(userId: ID!): Boolean!
}

type Subscription {
newPost(userId: ID!): Post!
newComment: Comment!
newFollower: User!
}

type User {
id: ID!
username: String!
email: String!
name: String?
bio: String?
avatar: String?
coverPhoto: String?
followers: [User!]!
following: [User!]!
posts: [Post!]!
likes: [Like!]!
createdAt: String!
updatedAt: String!
}

type Post {
id: ID!
content: String!
images: [String!]!
videos: [String!]!
author: User!
likes: [Like!]!
comments: [Comment!]!
createdAt: String!
updatedAt: String!
}

type Comment {
id: ID!
content: String!
author: User!
post: Post!
likes: [Like!]!
createdAt: String!
updatedAt: String!
}

type Like {
id: ID!
user: User!
post: Post?
comment: Comment?
createdAt: String!
}

type Follow {
id: ID!
follower: User!
following: User!
createdAt: String!
}

enum SearchType {
USERS
POSTS
HASHTAGS
}

type SearchResults {
users: [User!]!
posts: [Post!]!
hashtags: [String!]!
}

7.3 博客系统

# 博客系统GraphQL Schema
type Query {
post(id: ID!): Post
posts(
first: Int = 10
after: String
filter: PostFilter
sort: PostSort
): PostConnection!

tag(id: ID!): Tag
tags: [Tag!]!

category(id: ID!): Category
categories: [Category!]!

author(id: ID!): Author
authors: [Author!]!
}

type Mutation {
createPost(input: PostInput!): Post!
updatePost(id: ID!, input: PostInput!): Post!
deletePost(id: ID!): Boolean!

createComment(input: CommentInput!): Comment!
deleteComment(id: ID!): Boolean!

createTag(input: TagInput!): Tag!
updateTag(id: ID!, input: TagInput!): Tag!
deleteTag(id: ID!): Boolean!

createCategory(input: CategoryInput!): Category!
updateCategory(id: ID!, input: CategoryInput!): Category!
deleteCategory(id: ID!): Boolean!
}

type Post {
id: ID!
title: String!
slug: String!
content: String!
excerpt: String?
featuredImage: String?
publishedAt: String?
status: PostStatus!
author: Author!
tags: [Tag!]!
categories: [Category!]!
comments: [Comment!]!
meta: PostMeta
}

type Author {
id: ID!
name: String!
email: String!
bio: String?
avatar: String?
social: SocialLinks
}

type Tag {
id: ID!
name: String!
slug: String!
description: String?
posts: [Post!]!
}

type Category {
id: ID!
name: String!
slug: String!
description: String?
parent: Category?
children: [Category!]!
posts: [Post!]!
}

type Comment {
id: ID!
content: String!
author: String?
email: String?
website: String?
status: CommentStatus!
post: Post!
createdAt: String!
}

type SocialLinks {
twitter: String?
facebook: String?
linkedin: String?
github: String?
instagram: String?
}

type PostMeta {
title: String?
description: String?
keywords: [String!]?
}

enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}

enum CommentStatus {
PENDING
APPROVED
REJECTED
}

input PostInput {
title: String!
content: String!
excerpt: String
featuredImage: String
status: PostStatus
tagIds: [ID!]!
categoryIds: [ID!]!
meta: PostMetaInput
}

input CommentInput {
content: String!
author: String?
email: String?
website: String?
postId: ID!
}

input TagInput {
name: String!
slug: String!
description: String?
}

input CategoryInput {
name: String!
slug: String!
description: String?
parentId: ID?
}

input PostMetaInput {
title: String?
description: String?
keywords: [String!]?
}

8. 最佳实践

8.1 Schema设计原则

# 好的Schema设计
type Query {
# 使用明确的字段名
user(id: ID!): User
users(filter: UserFilter): [User!]!

# 使用合理的分页
posts(first: Int = 10, after: String): PostConnection!

# 使用过滤和排序
products(
filter: ProductFilter
sort: ProductSort
first: Int = 10
after: String
): ProductConnection!
}

# 避免过度嵌套
type User {
id: ID!
name: String!
email: String!

# 使用连接类型而不是嵌套列表
posts: PostConnection!
followers: UserConnection!
following: UserConnection!
}

# 使用描述和示例
"""
用户输入类型
"""
input UserInput {
"""
用户名,2-50个字符
"""
name: String!

"""
有效的电子邮件地址
"""
email: String!
}

8.2 性能优化

// 服务器端优化
const resolvers = {
Query: {
// 使用 DataLoader减少数据库查询
user: async (parent, { id }, { dataloaders }) => {
return dataloaders.user.load(id);
},

// 批量查询
users: async (parent, { ids }, { dataloaders }) => {
return dataloaders.users.loadMany(ids);
},

// 使用缓存
popularPosts: async (parent, args, { redis }) => {
const cacheKey = 'popular-posts';
const cached = await redis.get(cacheKey);

if (cached) {
return JSON.parse(cached);
}

const posts = await getPopularPosts();
await redis.setex(cacheKey, 3600, JSON.stringify(posts));

return posts;
},
},
};

// DataLoader创建
const dataloaders = {
user: new DataLoader(async (ids) => {
const users = await User.find({ _id: { $in: ids } });
return ids.map(id => users.find(user => user.id === id));
}),

users: new DataLoader(async (ids) => {
const users = await User.find({ _id: { $in: ids } });
return ids.map(id => users.find(user => user.id === id));
}),
};

// 客户端缓存策略
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
merge(existing = [], incoming) {
// 自定义合并策略
const merged = [...incoming];
const incomingIds = incoming.map(p => p.id);
const existingToAdd = existing.filter(p => !incomingIds.includes(p.id));
return [...merged, ...existingToAdd];
},
},
},
},
},
}),
});

8.3 安全考虑

// 认证和授权中间件
const authMiddleware = async ({ req, res, next }) => {
const token = req.headers.authorization || '';

try {
const { userId } = verifyToken(token);
const user = await User.findById(userId);

if (!user) {
throw new Error('User not found');
}

req.user = user;
next();
} catch (error) {
throw new AuthenticationError('Invalid token');
}
};

// GraphQL Shield
const { rule, shield } = require('graphql-shield');

const isAuthenticated = rule()((parent, args, ctx) => {
return ctx.user !== null;
});

const isAdmin = rule()((parent, args, ctx) => {
return ctx.user && ctx.user.role === 'ADMIN';
});

const permissions = shield({
Query: {
users: isAdmin,
posts: isAuthenticated,
},
Mutation: {
createUser: isAdmin,
createPost: isAuthenticated,
deletePost: isAdmin,
},
});

// 速率限制
const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP 15分钟内最多100个请求
message: 'Too many requests from this IP',
});

// 输入验证
const { GraphQLNonNull, GraphQLString } = require('graphql');

const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};

const validatePassword = (password) => {
return password.length >= 8;
};

const UserInput = new GraphQLInputObjectType({
name: 'UserInput',
fields: {
email: {
type: new GraphQLNonNull(GraphQLString),
validate: (email) => {
if (!validateEmail(email)) {
throw new Error('Invalid email format');
}
},
},
password: {
type: new GraphQLNonNull(GraphQLString),
validate: (password) => {
if (!validatePassword(password)) {
throw new Error('Password must be at least 8 characters');
}
},
},
},
});

9. 总结

GraphQL作为一种现代API查询语言,为前后端开发提供了强大的数据查询能力。通过本文的学习,你应该掌握了:

  1. 基础概念:理解了GraphQL的核心特点和与REST的区别
  2. Schema设计:学会了如何设计合理的GraphQL Schema
  3. 查询语法:掌握了GraphQL查询语言的各种语法特性
  4. 服务器实现:了解了如何使用Apollo Server和GraphQL Yoga实现GraphQL服务
  5. 客户端集成:学会了如何使用Apollo Client和React Query进行前端开发
  6. 高级特性:掌握了订阅、缓存、分页等高级功能
  7. 实际应用:了解了电商、社交媒体、博客等场景的GraphQL实现
  8. 最佳实践:学习了Schema设计、性能优化和安全考虑的最佳实践

GraphQL的优势在于它让客户端能够精确地获取所需数据,避免了过度获取和获取不足的问题。这使得API更加灵活、高效,并减少了前后端之间的沟通成本。

在实际项目中,合理使用GraphQL可以显著提高开发效率和用户体验。但同时也要注意避免过度设计,保持Schema的简洁和可维护性。

随着GraphQL生态的不断发展,它将继续在API设计领域发挥重要作用。作为开发者,掌握GraphQL将使你在构建现代Web应用时更加得心应手。