目錄
- 什么是GraphQL
- GraphQL核心概念
- GraphQL Schema定義語言
- 查詢(Queries)
- 變更(Mutations)
- 訂閱(Subscriptions)
- Schema設計最佳實踐
- 服務端實現
- 客戶端使用
- 高級特性
- 性能優化
- 實戰項目
什么是GraphQL
GraphQL是由Facebook開發的一種API查詢語言和運行時。它為API提供了完整且易于理解的數據描述,客戶端能夠準確獲得所需的數據,沒有多余信息。
GraphQL vs REST API
特性 | REST API | GraphQL |
---|---|---|
數據獲取 | 多個端點,可能過度獲取 | 單一端點,精確獲取 |
版本控制 | 需要版本管理 | 無需版本控制 |
緩存 | 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或falseID
: 唯一標識符
對象類型(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查詢語言,它提供了:
- 精確的數據獲取: 客戶端可以指定需要的確切數據
- 強類型系統: 完整的類型定義和運行時驗證
- 單一端點: 所有操作通過一個URL處理
- 實時功能: 內置訂閱支持實時數據更新
- 開發者體驗: 優秀的工具支持和自省功能
最佳實踐總結
- 設計清晰的Schema,使用適當的類型和命名規范
- 實現適當的認證和授權機制
- 使用DataLoader解決N+1查詢問題
- 實施查詢復雜度限制和速率限制
- 合理使用緩存策略提升性能
- 遵循GraphQL規范和社區最佳實踐
GraphQL已經被越來越多的公司采用,掌握GraphQL將為你的職業發展帶來很大幫助。通過本指南的學習和實踐,你應該能夠熟練使用GraphQL構建現代化的API。