当前位置: 首页 > news >正文

GraphQL从入门到精通完整指南

目录

  1. 什么是GraphQL
  2. GraphQL核心概念
  3. GraphQL Schema定义语言
  4. 查询(Queries)
  5. 变更(Mutations)
  6. 订阅(Subscriptions)
  7. Schema设计最佳实践
  8. 服务端实现
  9. 客户端使用
  10. 高级特性
  11. 性能优化
  12. 实战项目

什么是GraphQL

GraphQL是由Facebook开发的一种API查询语言和运行时。它为API提供了完整且易于理解的数据描述,客户端能够准确获得所需的数据,没有多余信息。

GraphQL vs REST API

特性REST APIGraphQL
数据获取多个端点,可能过度获取单一端点,精确获取
版本控制需要版本管理无需版本控制
缓存HTTP缓存查询级缓存
实时更新轮询或WebSocket内置订阅

GraphQL的优势

  • 精确数据获取: 客户端指定需要的字段,避免过度获取
  • 强类型系统: 完整的类型定义和验证
  • 单一端点: 所有操作通过一个URL处理
  • 内省功能: API自我描述,便于开发工具
  • 实时订阅: 内置实时数据推送功能

GraphQL核心概念

1. Schema(模式)

Schema定义了API的结构,包括可用的操作和数据类型。

type Query {user(id: ID!): Userposts: [Post!]!
}type User {id: ID!name: String!email: String!posts: [Post!]!
}type Post {id: ID!title: String!content: String!author: User!
}

2. Types(类型)

标量类型(Scalar Types)
  • Int: 32位有符号整数
  • Float: 双精度浮点数
  • String: UTF-8字符串
  • Boolean: true或false
  • ID: 唯一标识符
对象类型(Object Types)
type User {id: ID!name: String!email: Stringage: IntisActive: Boolean!
}
枚举类型(Enum Types)
enum Status {ACTIVEINACTIVEPENDING
}type User {id: ID!name: String!status: Status!
}
接口类型(Interface Types)
interface Node {id: ID!
}type User implements Node {id: ID!name: String!email: String!
}type Post implements Node {id: ID!title: String!content: String!
}
联合类型(Union Types)
union SearchResult = User | Post | Commenttype Query {search(query: String!): [SearchResult!]!
}

3. 字段参数和修饰符

type Query {# 必需参数user(id: ID!): User# 可选参数users(limit: Int, offset: Int): [User!]!# 默认值posts(status: Status = ACTIVE): [Post!]!
}type User {# 必需字段id: ID!name: String!# 可选字段email: String# 数组字段posts: [Post!]! # 非空数组,包含非空Post对象tags: [String] # 可为空的数组,包含可为空的字符串
}

GraphQL Schema定义语言

完整Schema示例

# 标量类型定义
scalar Date
scalar Upload# 枚举定义
enum UserRole {USERADMINMODERATOR
}enum PostStatus {DRAFTPUBLISHEDARCHIVED
}# 输入类型
input CreateUserInput {name: String!email: String!password: String!role: UserRole = USER
}input UpdatePostInput {title: Stringcontent: Stringstatus: PostStatustags: [String!]
}# 接口定义
interface Node {id: ID!createdAt: Date!updatedAt: Date!
}# 对象类型
type User implements Node {id: ID!createdAt: Date!updatedAt: Date!name: String!email: String!role: UserRole!posts: [Post!]!profile: UserProfile
}type UserProfile {bio: Stringavatar: Stringwebsite: Stringlocation: String
}type Post implements Node {id: ID!createdAt: Date!updatedAt: Date!title: String!content: String!status: PostStatus!author: User!tags: [String!]!comments: [Comment!]!likesCount: Int!
}type Comment implements Node {id: ID!createdAt: Date!updatedAt: Date!content: String!author: User!post: Post!replies: [Comment!]!parentComment: Comment
}# 查询根类型
type Query {# 用户查询me: Useruser(id: ID!): Userusers(first: Int = 10after: Stringrole: UserRolesearch: String): UserConnection!# 文章查询post(id: ID!): Postposts(first: Int = 10after: Stringstatus: PostStatusauthorId: IDtags: [String!]): PostConnection!# 搜索search(query: String!, type: SearchType): SearchResult!
}# 变更根类型
type Mutation {# 用户操作createUser(input: CreateUserInput!): CreateUserPayload!updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!deleteUser(id: ID!): DeleteUserPayload!# 文章操作createPost(input: CreatePostInput!): CreatePostPayload!updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload!deletePost(id: ID!): DeletePostPayload!publishPost(id: ID!): PublishPostPayload!# 评论操作createComment(input: CreateCommentInput!): CreateCommentPayload!updateComment(id: ID!, input: UpdateCommentInput!): UpdateCommentPayload!deleteComment(id: ID!): DeleteCommentPayload!
}# 订阅根类型
type Subscription {postAdded: Post!postUpdated(id: ID!): Post!commentAdded(postId: ID!): Comment!userOnlineStatus(userId: ID!): UserOnlineStatus!
}# 连接类型(分页)
type UserConnection {edges: [UserEdge!]!pageInfo: PageInfo!totalCount: Int!
}type UserEdge {node: User!cursor: String!
}type PostConnection {edges: [PostEdge!]!pageInfo: PageInfo!totalCount: Int!
}type PostEdge {node: Post!cursor: String!
}type PageInfo {hasNextPage: Boolean!hasPreviousPage: Boolean!startCursor: StringendCursor: String
}# 搜索结果
union SearchResult = User | Post | Commentenum SearchType {ALLUSERSPOSTSCOMMENTS
}# 变更结果类型
interface MutationPayload {success: Boolean!errors: [Error!]!
}type CreateUserPayload implements MutationPayload {success: Boolean!errors: [Error!]!user: User
}type Error {message: String!field: Stringcode: String
}

查询(Queries)

1. 基础查询

# 简单字段查询
query {me {idnameemail}
}# 带参数的查询
query {user(id: "123") {idnameemailposts {idtitlecreatedAt}}
}# 嵌套查询
query {posts {idtitleauthor {idnameprofile {bioavatar}}comments {idcontentauthor {name}}}
}

2. 查询变量

# 定义查询变量
query GetUser($userId: ID!, $includeInactive: Boolean = false) {user(id: $userId) {idnameemailposts(includeInactive: $includeInactive) {idtitlestatus}}
}# 变量值
{"userId": "123","includeInactive": true
}

3. 字段别名

query {user(id: "123") {idfullName: nameemailAddress: emailpublishedPosts: posts(status: PUBLISHED) {idtitle}draftPosts: posts(status: DRAFT) {idtitle}}
}

4. 片段(Fragments)

# 定义片段
fragment UserInfo on User {idnameemailprofile {bioavatar}
}fragment PostInfo on Post {idtitlecontentcreatedAtlikesCount
}# 使用片段
query {me {...UserInfoposts {...PostInfoauthor {...UserInfo}}}
}# 内联片段
query {search(query: "GraphQL") {... on User {idnameemail}... on Post {idtitlecontent}... on Comment {idcontentpost {title}}}
}

5. 指令(Directives)

query GetUser($userId: ID!, $includeProfile: Boolean!, $includePosts: Boolean!) {user(id: $userId) {idnameemailprofile @include(if: $includeProfile) {bioavatar}posts @skip(if: $includePosts) {idtitle}}
}

6. 分页查询

# 游标分页
query GetPosts($first: Int!, $after: String) {posts(first: $first, after: $after) {edges {node {idtitlecontentauthor {name}}cursor}pageInfo {hasNextPagehasPreviousPagestartCursorendCursor}totalCount}
}# 偏移分页
query GetUsers($limit: Int!, $offset: Int!) {users(limit: $limit, offset: $offset) {idnameemail}usersCount
}

变更(Mutations)

1. 基础变更

# 创建用户
mutation CreateUser($input: CreateUserInput!) {createUser(input: $input) {successerrors {messagefield}user {idnameemailcreatedAt}}
}# 变量
{"input": {"name": "张三","email": "zhangsan@example.com","password": "password123"}
}

2. 更新操作

mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {updatePost(id: $id, input: $input) {successerrors {messagefield}post {idtitlecontentstatusupdatedAt}}
}# 变量
{"id": "post123","input": {"title": "新标题","content": "更新的内容","status": "PUBLISHED"}
}

3. 删除操作

mutation DeletePost($id: ID!) {deletePost(id: $id) {successerrors {message}deletedPostId: id}
}

4. 批量操作

mutation BatchUpdatePosts($updates: [PostUpdateInput!]!) {updatePosts(updates: $updates) {successerrors {messageindex}posts {idtitlestatus}}
}

5. 文件上传

mutation UploadAvatar($file: Upload!, $userId: ID!) {uploadAvatar(file: $file, userId: $userId) {successerrors {message}user {idprofile {avatar}}}
}

订阅(Subscriptions)

1. 基础订阅

subscription PostAdded {postAdded {idtitlecontentauthor {name}createdAt}
}subscription CommentAdded($postId: ID!) {commentAdded(postId: $postId) {idcontentauthor {nameavatar}createdAt}
}

2. 用户状态订阅

subscription UserOnlineStatus($userId: ID!) {userOnlineStatus(userId: $userId) {userIdisOnlinelastSeen}
}

3. 实时通知

subscription Notifications {notificationAdded {idtypemessagereadcreatedAtuser {id}}
}

Schema设计最佳实践

1. 命名规范

# 类型名使用PascalCase
type User {# 字段名使用camelCasefirstName: String!lastName: String!emailAddress: String!
}# 枚举值使用UPPER_SNAKE_CASE
enum UserStatus {ACTIVEINACTIVEPENDING_VERIFICATION
}

2. 输入输出分离

# 输入类型
input CreateUserInput {name: String!email: String!password: String!
}input UpdateUserInput {name: Stringemail: String
}# 输出类型
type User {id: ID!name: String!email: String!createdAt: Date!updatedAt: Date!
}

3. 错误处理设计

type MutationResult {success: Boolean!errors: [Error!]!
}type Error {message: String!code: String!field: String
}type CreateUserPayload {success: Boolean!errors: [Error!]!user: User
}

4. 分页设计

# Relay风格的游标分页
type UserConnection {edges: [UserEdge!]!pageInfo: PageInfo!totalCount: Int!
}type UserEdge {node: User!cursor: String!
}type PageInfo {hasNextPage: Boolean!hasPreviousPage: Boolean!startCursor: StringendCursor: String
}

服务端实现

1. Node.js + GraphQL (Apollo Server)

const { ApolloServer, gql } = require('apollo-server-express');
const express = require('express');// Schema定义
const typeDefs = gql`type User {id: ID!name: String!email: String!posts: [Post!]!}type Post {id: ID!title: String!content: String!author: User!}type Query {users: [User!]!user(id: ID!): Userposts: [Post!]!post(id: ID!): Post}type Mutation {createUser(name: String!, email: String!): User!createPost(title: String!, content: String!, authorId: ID!): Post!}type Subscription {postAdded: Post!}
`;// 模拟数据
const users = [{ id: '1', name: '张三', email: 'zhangsan@example.com' },{ id: '2', name: '李四', email: 'lisi@example.com' }
];const posts = [{ id: '1', title: 'GraphQL入门', content: '学习GraphQL...', authorId: '1' },{ id: '2', title: 'Apollo Server', content: '使用Apollo Server...', authorId: '2' }
];// 解析器
const resolvers = {Query: {users: () => users,user: (parent, args) => users.find(user => user.id === args.id),posts: () => posts,post: (parent, args) => posts.find(post => post.id === args.id),},Mutation: {createUser: (parent, args) => {const user = {id: String(users.length + 1),name: args.name,email: args.email};users.push(user);return user;},createPost: (parent, args, context) => {const post = {id: String(posts.length + 1),title: args.title,content: args.content,authorId: args.authorId};posts.push(post);// 发布订阅事件context.pubsub.publish('POST_ADDED', { postAdded: post });return post;}},Subscription: {postAdded: {subscribe: (parent, args, context) => context.pubsub.asyncIterator(['POST_ADDED'])}},User: {posts: (parent) => posts.filter(post => post.authorId === parent.id)},Post: {author: (parent) => users.find(user => user.id === parent.authorId)}
};// 创建服务器
async function startServer() {const app = express();const server = new ApolloServer({typeDefs,resolvers,context: ({ req }) => ({// 添加认证、数据库连接等user: req.user,pubsub: new PubSub()})});await server.start();server.applyMiddleware({ app });const PORT = 4000;app.listen(PORT, () => {console.log(`Server running at http://localhost:${PORT}${server.graphqlPath}`);});
}startServer();

2. 数据库集成 (Prisma)

// schema.prisma
generator client {provider = "prisma-client-js"
}datasource db {provider = "postgresql"url      = env("DATABASE_URL")
}model User {id        String   @id @default(cuid())email     String   @uniquename      Stringposts     Post[]createdAt DateTime @default(now())updatedAt DateTime @updatedAt
}model Post {id        String   @id @default(cuid())title     Stringcontent   String?published Boolean  @default(false)author    User     @relation(fields: [authorId], references: [id])authorId  StringcreatedAt DateTime @default(now())updatedAt DateTime @updatedAt
}
// 使用Prisma的解析器
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();const resolvers = {Query: {users: async () => {return await prisma.user.findMany({include: { posts: true }});},user: async (parent, args) => {return await prisma.user.findUnique({where: { id: args.id },include: { posts: true }});}},Mutation: {createUser: async (parent, args) => {return await prisma.user.create({data: {name: args.name,email: args.email}});},createPost: async (parent, args) => {return await prisma.post.create({data: {title: args.title,content: args.content,author: {connect: { id: args.authorId }}},include: { author: true }});}}
};

3. 认证和授权

const jwt = require('jsonwebtoken');// 中间件
const authMiddleware = (req) => {const token = req.headers.authorization?.replace('Bearer ', '');if (token) {try {const user = jwt.verify(token, process.env.JWT_SECRET);return { user };} catch (error) {throw new AuthenticationError('Invalid token');}}return {};
};// 带权限检查的解析器
const resolvers = {Query: {me: async (parent, args, context) => {if (!context.user) {throw new ForbiddenError('Authentication required');}return await prisma.user.findUnique({where: { id: context.user.id }});}},Mutation: {createPost: async (parent, args, context) => {if (!context.user) {throw new ForbiddenError('Authentication required');}return await prisma.post.create({data: {title: args.title,content: args.content,author: {connect: { id: context.user.id }}}});}}
};

客户端使用

1. Apollo Client (React)

// 安装依赖
// npm install @apollo/client graphqlimport { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery, useMutation } from '@apollo/client';// 创建客户端
const client = new ApolloClient({uri: 'http://localhost:4000/graphql',cache: new InMemoryCache(),headers: {authorization: localStorage.getItem('token') ? `Bearer ${localStorage.getItem('token')}` : ''}
});// App组件
function App() {return (<ApolloProvider client={client}><div className="App"><UserList /><CreateUser /></div></ApolloProvider>);
}// 查询组件
const GET_USERS = gql`query GetUsers {users {idnameemailposts {idtitle}}}
`;function UserList() {const { loading, error, data, refetch } = useQuery(GET_USERS);if (loading) return <p>Loading...</p>;if (error) return <p>Error: {error.message}</p>;return (<div><h2>用户列表</h2><button onClick={() => refetch()}>刷新</button>{data.users.map(user => (<div key={user.id}><h3>{user.name}</h3><p>{user.email}</p><p>文章数量: {user.posts.length}</p></div>))}</div>);
}// 变更组件
const CREATE_USER = gql`mutation CreateUser($name: String!, $email: String!) {createUser(name: $name, email: $email) {idnameemail}}
`;function CreateUser() {const [name, setName] = useState('');const [email, setEmail] = useState('');const [createUser, { loading, error }] = useMutation(CREATE_USER, {// 更新缓存update(cache, { data: { createUser } }) {const { users } = cache.readQuery({ query: GET_USERS });cache.writeQuery({query: GET_USERS,data: {users: [...users, createUser]}});}});const handleSubmit = async (e) => {e.preventDefault();try {await createUser({variables: { name, email }});setName('');setEmail('');} catch (err) {console.error(err);}};return (<form onSubmit={handleSubmit}><h2>创建用户</h2><inputtype="text"placeholder="姓名"value={name}onChange={(e) => setName(e.target.value)}required/><inputtype="email"placeholder="邮箱"value={email}onChange={(e) => setEmail(e.target.value)}required/><button type="submit" disabled={loading}>{loading ? '创建中...' : '创建用户'}</button>{error && <p>错误: {error.message}</p>}</form>);
}

2. 订阅使用

import { useSubscription } from '@apollo/client';const POST_ADDED_SUBSCRIPTION = gql`subscription PostAdded {postAdded {idtitlecontentauthor {name}createdAt}}
`;function PostSubscription() {const { data, loading, error } = useSubscription(POST_ADDED_SUBSCRIPTION);if (loading) return <p>等待新文章...</p>;if (error) return <p>订阅错误: {error.message}</p>;return (<div><h3>新文章通知</h3>{data && (<div><h4>{data.postAdded.title}</h4><p>作者: {data.postAdded.author.name}</p><p>{data.postAdded.content}</p></div>)}</div>);
}

3. 缓存管理

// 乐观更新
const [likePost] = useMutation(LIKE_POST, {optimisticResponse: {likePost: {id: postId,likesCount: post.likesCount + 1,isLiked: true,__typename: 'Post'}},update(cache, { data: { likePost } }) {cache.modify({id: cache.identify(likePost),fields: {likesCount: (existing) => likePost.likesCount,isLiked: () => likePost.isLiked}});}
});// 缓存策略
const { loading, error, data } = useQuery(GET_POSTS, {fetchPolicy: 'cache-first', // cache-first, cache-only, network-only, cache-and-networkpollInterval: 5000, // 每5秒轮询一次notifyOnNetworkStatusChange: true
});

高级特性

1. 自定义标量类型

// 服务端定义
const { GraphQLScalarType, GraphQLError } = require('graphql');
const { Kind } = require('graphql/language');const DateType = new GraphQLScalarType({name: 'Date',description: 'Date custom scalar type',serialize(value) {return value.toISOString(); // 发送给客户端},parseValue(value) {return new Date(value); // 来自客户端变量},parseLiteral(ast) {if (ast.kind === Kind.STRING) {return new Date(ast.value); // 来自查询字面量}throw new GraphQLError(`Can only parse strings to dates but got a: ${ast.kind}`);}
});const resolvers = {Date: DateType,// ... 其他解析器
};

2. DataLoader(批量加载和缓存)

const DataLoader = require('dataloader');// 创建DataLoader
const userLoader = new DataLoader(async (userIds) => {const users = await prisma.user.findMany({where: { id: { in: userIds } }});// 保持顺序return userIds.map(id => users.find(user => user.id === id));
});const postLoader = new DataLoader(async (userIds) => {const posts = await prisma.post.findMany({where: { authorId: { in: userIds } }});return userIds.map(userId => posts.filter(post => post.authorId === userId));
});// 在解析器中使用
const resolvers = {Post: {author: async (parent, args, context) => {return context.userLoader.load(parent.authorId);}},User: {posts: async (parent, args, context) => {return context.postLoader.load(parent.id);}}
};// 在context中提供DataLoader实例
const server = new ApolloServer({typeDefs,resolvers,context: () => ({userLoader: new DataLoader(batchUsers),postLoader: new DataLoader(batchPosts)})
});

3. 字段级权限控制

const { shield, rule, and, or } = require('graphql-shield');// 定义规则
const isAuthenticated = rule({ cache: 'contextual' })(async (parent, args, context) => {return context.user !== null;}
);const isOwner = rule({ cache: 'strict' })(async (parent, args, context) => {return context.user && parent.authorId === context.user.id;}
);const isAdmin = rule({ cache: 'contextual' })(async (parent, args, context) => {return context.user && context.user.role === 'ADMIN';}
);// 创建权限盾
const permissions = shield({Query: {me: isAuthenticated,users: isAdmin,},Mutation: {createPost: isAuthenticated,updatePost: and(isAuthenticated, or(isOwner, isAdmin)),deletePost: and(isAuthenticated, or(isOwner, isAdmin)),},Post: {author: isAuthenticated,}
});// 应用到服务器
const server = new ApolloServer({typeDefs,resolvers,middlewares: [permissions]
});

4. 查询复杂度分析

const depthLimit = require('graphql-depth-limit');
const costAnalysis = require('graphql-query-complexity');const server = new ApolloServer({typeDefs,resolvers,validationRules: [depthLimit(10), // 限制查询深度costAnalysis({maximumCost: 1000,defaultCost: 1,scalarCost: 1,objectCost: 1,listFactor: 10,introspectionCost: 1000,complexityScalarMap: {DateTime: 2,Upload: 1000,},fieldConfigMap: {User: {posts: { complexity: ({ args, childComplexity }) => {return childComplexity * (args.first || 10);}}}}})]
});

5. 缓存策略

const responseCachePlugin = require('apollo-server-plugin-response-cache');// 响应缓存插件
const server = new ApolloServer({typeDefs,resolvers,plugins: [responseCachePlugin({sessionId: (requestContext) => {return requestContext.request.http.headers.get('session-id') || null;},})],cacheControl: {defaultMaxAge: 60, // 默认缓存60秒}
});// 在Schema中设置缓存提示
const typeDefs = gql`type Query {posts: [Post!]! @cacheControl(maxAge: 300) # 缓存5分钟user(id: ID!): User @cacheControl(maxAge: 60, scope: PRIVATE)}type Post @cacheControl(maxAge: 300) {id: ID!title: String!author: User! @cacheControl(maxAge: 60)}
`;

性能优化

1. N+1 查询问题解决

// 错误的方式 - 导致N+1查询
const resolvers = {Post: {author: async (parent) => {return await prisma.user.findUnique({where: { id: parent.authorId }});}}
};// 正确的方式 - 使用DataLoader
const resolvers = {Post: {author: async (parent, args, context) => {return context.userLoader.load(parent.authorId);}}
};// 或者在查询时包含关联数据
const resolvers = {Query: {posts: async () => {return await prisma.post.findMany({include: { author: true } // 一次性获取作者信息});}},Post: {author: (parent) => parent.author // 直接返回已加载的数据}
};

2. 分页优化

// 游标分页实现
const resolvers = {Query: {posts: async (parent, args) => {const { first = 10, after } = args;const posts = await prisma.post.findMany({take: first + 1, // 多取一个判断是否还有更多cursor: after ? { id: after } : undefined,skip: after ? 1 : 0,orderBy: { createdAt: 'desc' }});const hasNextPage = posts.length > first;const edges = posts.slice(0, first).map(post => ({node: post,cursor: post.id}));return {edges,pageInfo: {hasNextPage,startCursor: edges[0]?.cursor,endCursor: edges[edges.length - 1]?.cursor}};}}
};

3. 字段级缓存

const Redis = require('redis');
const redis = Redis.createClient();const resolvers = {User: {postsCount: async (parent, args, context) => {const cacheKey = `user:${parent.id}:postsCount`;// 尝试从缓存获取let count = await redis.get(cacheKey);if (count === null) {// 缓存未命中,从数据库查询count = await prisma.post.count({where: { authorId: parent.id }});// 缓存结果,5分钟过期await redis.setEx(cacheKey, 300, count.toString());}return parseInt(count);}}
};

4. 查询优化

// 使用投影减少数据传输
const resolvers = {Query: {users: async (parent, args, info) => {// 分析查询字段const selectedFields = getFieldsFromInfo(info);const select = {};if (selectedFields.includes('name')) select.name = true;if (selectedFields.includes('email')) select.email = true;if (selectedFields.includes('posts')) {select.posts = {select: {id: true,title: true}};}return await prisma.user.findMany({ select });}}
};// 辅助函数
function getFieldsFromInfo(info) {return info.fieldNodes[0].selectionSet.selections.map(selection => selection.name.value);
}

实战项目

博客系统完整实现

1. Schema设计
# 完整的博客系统Schema
type User {id: ID!username: String! @uniqueemail: String! @uniquedisplayName: String!bio: Stringavatar: Stringrole: UserRole!posts: [Post!]!comments: [Comment!]!followers: [User!]!following: [User!]!followersCount: Int!followingCount: Int!postsCount: Int!createdAt: DateTime!updatedAt: DateTime!
}type Post {id: ID!title: String!slug: String! @uniquecontent: String!excerpt: StringfeaturedImage: Stringstatus: PostStatus!viewsCount: Int!likesCount: Int!commentsCount: Int!author: User!tags: [Tag!]!categories: [Category!]!comments: [Comment!]!likes: [Like!]!isLiked: Boolean!readingTime: Int!createdAt: DateTime!updatedAt: DateTime!publishedAt: DateTime
}type Comment {id: ID!content: String!author: User!post: Post!parent: Commentreplies: [Comment!]!repliesCount: Int!likesCount: Int!isLiked: Boolean!createdAt: DateTime!updatedAt: DateTime!
}type Tag {id: ID!name: String! @uniqueslug: String! @uniquedescription: StringpostsCount: Int!posts: [Post!]!
}type Category {id: ID!name: String! @uniqueslug: String! @uniquedescription: Stringparent: Categorychildren: [Category!]!postsCount: Int!posts: [Post!]!
}enum UserRole {USERMODERATORADMIN
}enum PostStatus {DRAFTPUBLISHEDARCHIVED
}type Query {# 用户查询me: Useruser(username: String!): Userusers(first: Int = 20after: Stringsearch: Stringrole: UserRole): UserConnection!# 文章查询post(slug: String!): Postposts(first: Int = 20after: Stringstatus: PostStatus = PUBLISHEDauthorId: IDtagIds: [ID!]categoryIds: [ID!]search: StringsortBy: PostSortInput): PostConnection!popularPosts(limit: Int = 10): [Post!]!recentPosts(limit: Int = 10): [Post!]!# 标签和分类tags: [Tag!]!tag(slug: String!): Tagcategories: [Category!]!category(slug: String!): Category# 搜索search(query: String!, type: SearchType = ALL): SearchResult!# 统计stats: BlogStats!
}type Mutation {# 认证login(email: String!, password: String!): AuthPayload!register(input: RegisterInput!): AuthPayload!logout: Boolean!refreshToken: AuthPayload!# 用户操作updateProfile(input: UpdateProfileInput!): User!uploadAvatar(file: Upload!): User!followUser(userId: ID!): FollowPayload!unfollowUser(userId: ID!): FollowPayload!# 文章操作createPost(input: CreatePostInput!): Post!updatePost(id: ID!, input: UpdatePostInput!): Post!deletePost(id: ID!): Boolean!publishPost(id: ID!): Post!unpublishPost(id: ID!): Post!likePost(postId: ID!): LikePayload!unlikePost(postId: ID!): LikePayload!# 评论操作createComment(input: CreateCommentInput!): Comment!updateComment(id: ID!, input: UpdateCommentInput!): Comment!deleteComment(id: ID!): Boolean!likeComment(commentId: ID!): LikePayload!unlikeComment(commentId: ID!): LikePayload!# 标签和分类createTag(input: CreateTagInput!): Tag!updateTag(id: ID!, input: UpdateTagInput!): Tag!deleteTag(id: ID!): Boolean!createCategory(input: CreateCategoryInput!): Category!updateCategory(id: ID!, input: UpdateCategoryInput!): Category!deleteCategory(id: ID!): Boolean!
}type Subscription {# 实时通知postPublished: Post!commentAdded(postId: ID!): Comment!userFollowed(userId: ID!): FollowNotification!postLiked(postId: ID!): LikeNotification!# 在线状态userOnline(userId: ID!): UserOnlineStatus!
}
2. 服务端完整实现
const { ApolloServer } = require('apollo-server-express');
const { PrismaClient } = require('@prisma/client');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const { createWriteStream } = require('fs');
const { finished } = require('stream/promises');
const path = require('path');const prisma = new PrismaClient();// 认证中间件
const getUser = async (token) => {try {if (token) {const decoded = jwt.verify(token, process.env.JWT_SECRET);const user = await prisma.user.findUnique({where: { id: decoded.userId }});return user;}return null;} catch (error) {return null;}
};// 解析器实现
const resolvers = {Query: {me: async (parent, args, context) => {if (!context.user) {throw new AuthenticationError('Not authenticated');}return context.user;},posts: async (parent, args) => {const {first = 20,after,status = 'PUBLISHED',authorId,tagIds,categoryIds,search,sortBy} = args;const where = { status };if (authorId) where.authorId = authorId;if (search) {where.OR = [{ title: { contains: search, mode: 'insensitive' } },{ content: { contains: search, mode: 'insensitive' } }];}if (tagIds?.length) {where.tags = { some: { id: { in: tagIds } } };}if (categoryIds?.length) {where.categories = { some: { id: { in: categoryIds } } };}const orderBy = sortBy ? [sortBy] : [{ createdAt: 'desc' }];const posts = await prisma.post.findMany({where,orderBy,take: first + 1,cursor: after ? { id: after } : undefined,skip: after ? 1 : 0,include: {author: true,tags: true,categories: true,_count: {select: {comments: true,likes: true}}}});const hasNextPage = posts.length > first;const edges = posts.slice(0, first).map(post => ({node: post,cursor: post.id}));return {edges,pageInfo: {hasNextPage,startCursor: edges[0]?.cursor,endCursor: edges[edges.length - 1]?.cursor}};},popularPosts: async (parent, args) => {return await prisma.post.findMany({where: { status: 'PUBLISHED' },orderBy: [{ likesCount: 'desc' },{ viewsCount: 'desc' }],take: args.limit || 10,include: {author: true,tags: true}});}},Mutation: {register: async (parent, args) => {const { username, email, password, displayName } = args.input;// 检查用户是否已存在const existingUser = await prisma.user.findFirst({where: {OR: [{ email }, { username }]}});if (existingUser) {throw new UserInputError('User already exists');}// 加密密码const hashedPassword = await bcrypt.hash(password, 12);// 创建用户const user = await prisma.user.create({data: {username,email,password: hashedPassword,displayName,role: 'USER'}});// 生成JWTconst token = jwt.sign({ userId: user.id },process.env.JWT_SECRET,{ expiresIn: '7d' });return {token,user};},login: async (parent, args) => {const { email, password } = args;const user = await prisma.user.findUnique({where: { email }});if (!user) {throw new AuthenticationError('Invalid credentials');}const valid = await bcrypt.compare(password, user.password);if (!valid) {throw new AuthenticationError('Invalid credentials');}const token = jwt.sign({ userId: user.id },process.env.JWT_SECRET,{ expiresIn: '7d' });return {token,user};},createPost: async (parent, args, context) => {if (!context.user) {throw new AuthenticationError('Not authenticated');}const { title, content, excerpt, tagIds, categoryIds } = args.input;// 生成slugconst slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');const post = await prisma.post.create({data: {title,slug,content,excerpt,author: { connect: { id: context.user.id } },tags: tagIds ? { connect: tagIds.map(id => ({ id })) } : undefined,categories: categoryIds ? { connect: categoryIds.map(id => ({ id })) } : undefined,status: 'DRAFT'},include: {author: true,tags: true,categories: true}});return post;},publishPost: async (parent, args, context) => {if (!context.user) {throw new AuthenticationError('Not authenticated');}const post = await prisma.post.findUnique({where: { id: args.id },include: { author: true }});if (!post) {throw new UserInputError('Post not found');}if (post.author.id !== context.user.id && context.user.role !== 'ADMIN') {throw new ForbiddenError('Not authorized');}const updatedPost = await prisma.post.update({where: { id: args.id },data: {status: 'PUBLISHED',publishedAt: new Date()},include: {author: true,tags: true,categories: true}});// 发布订阅事件context.pubsub.publish('POST_PUBLISHED', { postPublished: updatedPost });return updatedPost;},likePost: async (parent, args, context) => {if (!context.user) {throw new AuthenticationError('Not authenticated');}const existingLike = await prisma.like.findFirst({where: {userId: context.user.id,postId: args.postId}});if (existingLike) {throw new UserInputError('Already liked');}await prisma.$transaction([prisma.like.create({data: {userId: context.user.id,postId: args.postId}}),prisma.post.update({where: { id: args.postId },data: {likesCount: { increment: 1 }}})]);return {success: true,post: await prisma.post.findUnique({where: { id: args.postId },include: { author: true }})};}},// 字段解析器Post: {isLiked: async (parent, args, context) => {if (!context.user) return false;const like = await prisma.like.findFirst({where: {userId: context.user.id,postId: parent.id}});return !!like;},readingTime: (parent) => {const wordsPerMinute = 200;const wordCount = parent.content.split(/\s+/).length;return Math.ceil(wordCount / wordsPerMinute);},commentsCount: (parent) => {return parent._count?.comments || 0;},likesCount: (parent) => {return parent._count?.likes || parent.likesCount || 0;}},User: {postsCount: async (parent) => {return await prisma.post.count({where: {authorId: parent.id,status: 'PUBLISHED'}});},followersCount: async (parent) => {return await prisma.follow.count({where: { followingId: parent.id }});},followingCount: async (parent) => {return await prisma.follow.count({where: { followerId: parent.id }});}}
};// 创建服务器
const server = new ApolloServer({typeDefs,resolvers,context: async ({ req }) => {const token = req.headers.authorization?.replace('Bearer ', '');const user = await getUser(token);return {user,prisma,pubsub: new PubSub()};}
});
3. 客户端实现 (React)
// hooks/useAuth.js
import { useState, useEffect, createContext, useContext } from 'react';
import { useApolloClient } from '@apollo/client';const AuthContext = createContext();export const useAuth = () => useContext(AuthContext);export const AuthProvider = ({ children }) => {const [user, setUser] = useState(null);const [token, setToken] = useState(localStorage.getItem('token'));const client = useApolloClient();useEffect(() => {if (token) {// 验证token并获取用户信息fetchUser();}}, [token]);const login = async (email, password) => {const { data } = await client.mutate({mutation: LOGIN_MUTATION,variables: { email, password }});const { token: newToken, user: newUser } = data.login;setToken(newToken);setUser(newUser);localStorage.setItem('token', newToken);};const logout = () => {setToken(null);setUser(null);localStorage.removeItem('token');client.resetStore();};return (<AuthContext.Provider value={{ user, token, login, logout }}>{children}</AuthContext.Provider>);
};// components/PostList.js
import { useQuery } from '@apollo/client';
import { GET_POSTS } from '../graphql/queries';export const PostList = () => {const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {variables: { first: 10 }});const loadMore = () => {fetchMore({variables: {after: data.posts.pageInfo.endCursor}});};if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error.message}</div>;return (<div>{data.posts.edges.map(({ node: post }) => (<article key={post.id}><h2>{post.title}</h2><p>{post.excerpt}</p><div><span>By {post.author.displayName}</span><span>{post.likesCount} likes</span><span>{post.commentsCount} comments</span></div></article>))}{data.posts.pageInfo.hasNextPage && (<button onClick={loadMore}>Load More</button>)}</div>);
};// components/CreatePost.js
import { useState } from 'react';
import { useMutation } from '@apollo/client';
import { CREATE_POST_MUTATION } from '../graphql/mutations';export const CreatePost = () => {const [formData, setFormData] = useState({title: '',content: '',excerpt: ''});const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, {onCompleted: (data) => {console.log('Post created:', data.createPost);// 重定向到文章页面}});const handleSubmit = async (e) => {e.preventDefault();await createPost({variables: { input: formData }});};return (<form onSubmit={handleSubmit}><inputtype="text"placeholder="Title"value={formData.title}onChange={(e) => setFormData({ ...formData, title: e.target.value })}required/><textareaplaceholder="Excerpt"value={formData.excerpt}onChange={(e) => setFormData({ ...formData, excerpt: e.target.value })}/><textareaplaceholder="Content"value={formData.content}onChange={(e) => setFormData({ ...formData, content: e.target.value })}required/><button type="submit" disabled={loading}>{loading ? 'Creating...' : 'Create Post'}</button>{error && <p>Error: {error.message}</p>}</form>);
};

总结

GraphQL是一个功能强大的API查询语言,它提供了:

  1. 精确的数据获取: 客户端可以指定需要的确切数据
  2. 强类型系统: 完整的类型定义和运行时验证
  3. 单一端点: 所有操作通过一个URL处理
  4. 实时功能: 内置订阅支持实时数据更新
  5. 开发者体验: 优秀的工具支持和自省功能

最佳实践总结

  • 设计清晰的Schema,使用适当的类型和命名规范
  • 实现适当的认证和授权机制
  • 使用DataLoader解决N+1查询问题
  • 实施查询复杂度限制和速率限制
  • 合理使用缓存策略提升性能
  • 遵循GraphQL规范和社区最佳实践

GraphQL已经被越来越多的公司采用,掌握GraphQL将为你的职业发展带来很大帮助。通过本指南的学习和实践,你应该能够熟练使用GraphQL构建现代化的API。

http://www.dtcms.com/a/331626.html

相关文章:

  • Scrapy 基础框架搭建教程:从环境配置到爬虫实现(附实例)
  • 开源数据发现平台:Amundsen 第1部分:基础入门与本地环境设置
  • 数据结构:用两个栈模拟队列(Queue Using 2 Stacks)
  • 2025年商协会新运营模式,正在破局
  • NokoPrint:安卓平台上的全能打印解决方案
  • 软件测试之接口测试,接口自动化测试, request
  • 【FreeRTOS】刨根问底4: 优先级反转是啥?咋解决?
  • 系统升级部署中的常见问题与解决方案
  • 京东比价项目开发实录:京东API接口(2025)
  • AI Agent 为什么需要记忆?
  • 我的 LeetCode 日记:Day 37 - 解锁动态规划:完全背包问题
  • 深度解析 Vue 高阶技巧:提升工程化能力的实用方案
  • 使用EvalScope对GPT-OSS-20B进行推理性能压测实战
  • Flink中的水位线
  • STL容器详解:Vector高效使用指南
  • 高效Unicode字符表示:一种创新的词表构建策略分析
  • MCP智能化问答系统实现方案
  • K8S企业级应用与DaemonSet实战解析
  • 【车联网kafka】用钟表齿轮理解 Kafka 时间轮​(第七篇)
  • Java应用快速部署Tomcat指南
  • # 2025全球AI游戏市场研究报告:行业洞察与未来趋势
  • OpenAI 的浏览器将使用 ChatGPT Agent 来控制浏览器
  • 亚马逊FCF计划:破解高单价产品转化困局的金融杠杆
  • RH134 管理基本存储知识点
  • 考研408《计算机组成原理》复习笔记,第四章(1)——指令系统概念(指令字长、N地址指令、定长和变长操作码)
  • H.264编码格式详解:Annex-B vs AVCC
  • 14、Docker Compose 安装 Redis 集群(三主三从)
  • 嵌入式学习笔记--MCU阶段--DAY12实时操作系统rt_thread1
  • Cypher注入详解:原理、类型与测试方法
  • 使用免费API开发口播数字人