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.pswp.cn/bicheng/93251.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/93251.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/93251.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

使用 Dockerfile 與 Docker Compose 結合+Docker-compose.yml 文件詳解

使用 Dockerfile 與 Docker Compose 結合的完整流程 Dockerfile 用于定義單個容器的構建過程&#xff0c;而 Docker Compose 則用于編排多個容器。以下是結合使用兩者的完整方法&#xff1a; 1. 創建 Dockerfile 在項目目錄中創建 Dockerfile 定義應用鏡像的構建過程&#xff1…

15 ABP Framework 開發工具

ABP Framework 開發工具 概述 該頁面詳細介紹了 ABP Framework 提供的開發工具和命令行界面&#xff08;CLI&#xff09;&#xff0c;用于創建、管理和定制 ABP 項目。ABP CLI 是主要開發工具&#xff0c;支持項目腳手架、模塊添加、數據庫遷移管理及常見開發任務自動化。 ABP …

力扣top100(day02-01)--鏈表01

160. 相交鏈表 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode(int x) {* val x;* next null;* }* }*/ public class Solution {/*** 查找兩個鏈表的相交節點* param headA 第一個…

LLM 中 語音編碼與文本embeding的本質區別

直接使用語音編碼,是什么形式,和文本的區別 直接使用語音編碼的形式 語音編碼是將模擬語音信號轉換為數字信號的技術,其核心是對語音的聲學特征進行數字化表征,直接承載語音的物理聲學信息。其形式可分為以下幾類: 1. 基于波形的編碼(保留原始波形特征) 脈沖編碼調制…

模型選擇與調優

一、模型選擇與調優在機器學習中&#xff0c;模型的選擇和調優是一個重要的步驟&#xff0c;它直接影響到最終模型的性能1、交叉驗證在任何有監督機器學習項目的模型構建階段&#xff0c;我們訓練模型的目的是從標記的示例中學習所有權重和偏差的最佳值如果我們使用相同的標記示…

vue+Django農產品推薦與價格預測系統、雙推薦+機器學習預測+知識圖譜

vueflask農產品推薦與價格預測系統、雙推薦機器學習價格預測知識圖譜文章結尾部分有CSDN官方提供的學長 聯系方式名片 文章結尾部分有CSDN官方提供的學長 聯系方式名片 關注B站&#xff0c;有好處&#xff01;編號: D010 技術架構: vueflaskmysqlneo4j 核心技術&#xff1a; 基…

數據分析小白訓練營:基于python編程語言的Numpy庫介紹(第三方庫)(下篇)

銜接上篇文章&#xff1a;數據分析小白訓練營&#xff1a;基于python編程語言的Numpy庫介紹&#xff08;第三方庫&#xff09;&#xff08;上篇&#xff09;&#xff08;十一&#xff09;數組的組合核心功能&#xff1a;一、生成基數組np.arange().reshape() 基礎運算功能&…

負載因子(Load Factor) :哈希表(Hash Table)中的一個關鍵性能指標

負載因子&#xff08;Load Factor&#xff09; 是哈希表&#xff08;Hash Table&#xff09;中的一個關鍵性能指標&#xff0c;用于衡量哈希表的空間利用率和發生哈希沖突的可能性。一&#xff1a;定義負載因子&#xff08;通常用希臘字母 λ 表示&#xff09;的計算公式為&…

監控插件SkyWalking(一)原理

一、介紹 1、簡介 SkyWalking 是一個 開源的 APM&#xff08;Application Performance Monitoring&#xff0c;應用性能監控&#xff09;和分布式追蹤系統&#xff0c;主要用于監控、追蹤、分析分布式系統中的調用鏈路、性能指標和日志。 它由 Apache 基金會托管&#xff0c;…

【接口自動化測試】---自動化框架pytest

目錄 1、用例運行規則 2、pytest命令參數 3、pytest配置文件 4、前后置 5、斷言 6、參數化---對函數的參數&#xff08;重要&#xff09; 7、fixture 7.1、基本用法 7.2、fixture嵌套&#xff1a; 7.3、請求多個fixture&#xff1a; 7.4、yield fixture 7.5、帶參數…

Flink Stream API 源碼走讀 - socketTextStream

概述 本文深入分析了 Flink 中 socketTextStream() 方法的源碼實現&#xff0c;從用戶API調用到最終返回 DataStream 的完整流程。 核心知識點 1. socketTextStream 方法重載鏈 // 用戶調用入口 env.socketTextStream("hostname", 9999)↓ 補充分隔符參數 env.socket…

待辦事項小程序開發

1. 項目規劃功能需求&#xff1a;添加待辦事項標記完成/未完成刪除待辦事項分類或標簽管理&#xff08;可選&#xff09;數據持久化&#xff08;本地存儲&#xff09;2. 實現功能添加待辦事項&#xff1a;監聽輸入框和按鈕事件&#xff0c;將輸入內容添加到列表。 標記完成/未完…

【C#】Region、Exclude的用法

在 C# 中&#xff0c;Region 和 Exclude 是與圖形編程相關的概念&#xff0c;通常在使用 System.Drawing 命名空間進行 GDI 繪圖時出現。它們主要用于定義和操作二維空間中的區域&#xff08;幾何區域&#xff09;&#xff0c;常用于窗體裁剪、控件重繪、圖形繪制優化等場景。 …

機器學習 - Kaggle項目實踐(3)Digit Recognizer 手寫數字識別

Digit Recognizer | Kaggle 題面 Digit Recognizer-CNN | Kaggle 下面代碼的kaggle版本 使用CNN進行手寫數字識別 學習到了網絡搭建手法學習率退火數據增廣 提高訓練效果。 使用混淆矩陣 以及對分類出錯概率最大的例子單獨拎出來分析。 最終以99.546%正確率 排在 86/1035 …

新手如何高效運營亞馬遜跨境電商:從傳統SP廣告到DeepBI智能策略

"為什么我的廣告點擊量很高但訂單轉化率卻很低&#xff1f;""如何避免新品期廣告預算被大詞消耗殆盡&#xff1f;""為什么手動調整關鍵詞和出價總是慢市場半拍&#xff1f;""競品ASIN投放到底該怎么做才有效&#xff1f;""有沒有…

【論文閱讀 | CVPR 2024 | UniRGB-IR:通過適配器調優實現可見光-紅外語義任務的統一框架】

論文閱讀 | CVPR 2024 | UniRGB-IR&#xff1a;通過適配器調優實現可見光-紅外語義任務的統一框架?1&&2. 摘要&&引言3.方法3.1 整體架構3.2 多模態特征池3.3 補充特征注入器3.4 適配器調優范式4 實驗4.1 RGB-IR 目標檢測4.2 RGB-IR 語義分割4.3 RGB-IR 顯著目…

Hyperf 百度翻譯接口實現方案

保留 HTML/XML 標簽結構&#xff0c;僅翻譯文本內容&#xff0c;避免破壞富文本格式。采用「HTML 解析 → 文本提取 → 批量翻譯 → 回填」的流程。百度翻譯集成方案&#xff1a;富文本內容翻譯系統 HTML 解析 百度翻譯 API 集成 文件結構 app/ ├── Controller/ │ └──…

字節跳動 VeOmni 框架開源:統一多模態訓練效率飛躍!

資料來源&#xff1a;火山引擎-開發者社區 多模態時代的訓練痛點&#xff0c;終于有了“特效藥” 當大模型從單一語言向文本 圖像 視頻的多模態進化時&#xff0c;算法工程師們的訓練流程卻陷入了 “碎片化困境”&#xff1a; 當業務要同時迭代 DiT、LLM 與 VLM時&#xff0…

配置docker pull走http代理

之前寫了一篇自建Docker鏡像加速器服務的博客&#xff0c;需要用到境外服務器作為代理&#xff0c;但是一般可能沒有境外服務器&#xff0c;只有http代理&#xff0c;所以如果本地使用想走代理可以用以下方式 臨時生效&#xff08;只對當前終端有效&#xff09; 設置環境變量…

OpenAI 開源模型 gpt-oss 本地部署詳細教程

OpenAI 最近發布了其首個開源的開放權重模型gpt-oss&#xff0c;這在AI圈引起了巨大的轟動。對于廣大開發者和AI愛好者來說&#xff0c;這意味著我們終于可以在自己的機器上&#xff0c;完全本地化地運行和探索這款強大的模型了。 本教程將一步一步指導你如何在Windows和Linux…