commit 395154a0212602247ec152ce8b9ada50fa2102fd Author: wehub-skill-sync Date: Mon Jul 13 21:36:20 2026 +0800 chore: import zh skill graphql-operations diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..5f61e5c --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,9 @@ +# WeHub 来源说明 + +- Skill 名称:`graphql-operations` +- 中文类目:GraphQL Schema 设计 +- 上游仓库:`apollographql__skills` +- 上游路径:`skills/graphql-operations/SKILL.md` +- 上游链接:https://github.com/apollographql/skills/blob/HEAD/skills/graphql-operations/SKILL.md +- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理 +- 原作者、版权和许可证信息以上游仓库为准 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..65302f1 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,244 @@ +--- +name: graphql-operations +description: > + 编写 GraphQL 操作(查询、变更、片段)的最佳实践指南。在以下情况下使用本技能: + (1) 编写 GraphQL 查询或变更, + (2) 使用片段组织操作, + (3) 优化数据获取模式, + (4) 设置类型生成或代码检查, + (5) 审查操作效率。 +license: MIT +compatibility: 任意 GraphQL 客户端(Apollo Client、urql、Relay 等) +metadata: + author: apollographql + version: "1.0.0" +allowed-tools: Bash(npm:*) Bash(npx:*) Read Write Edit Glob Grep +--- + +# GraphQL 操作指南 + +本指南涵盖了作为客户端开发者编写 GraphQL 操作(查询、变更、订阅)的最佳实践。编写良好的操作应具备高效、类型安全且易于维护的特点。 + +## 操作基础 + +### 查询结构 + +```graphql +query GetUser($id: ID!) { + user(id: $id) { + id + name + email + } +} +``` + +### 变更结构 + +```graphql +mutation CreatePost($input: CreatePostInput!) { + createPost(input: $input) { + id + title + createdAt + } +} +``` + +### 订阅结构 + +```graphql +subscription OnMessageReceived($channelId: ID!) { + messageReceived(channelId: $channelId) { + id + content + sender { + id + name + } + } +} +``` + +## 快速参考 + +### 操作命名 + +| 模式 | 示例 | +| ------------ | --------------------------------------------- | +| 查询 | `GetUser`、`ListPosts`、`SearchProducts` | +| 变更 | `CreateUser`、`UpdatePost`、`DeleteComment` | +| 订阅 | `OnMessageReceived`、`OnUserStatusChanged` | + +### 变量语法 + +```graphql +# 必填变量 +query GetUser($id: ID!) { ... } + +# 带默认值的可选变量 +query ListPosts($first: Int = 20) { ... } + +# 多个变量 +query SearchPosts($query: String!, $status: PostStatus, $first: Int = 10) { ... } +``` + +### 片段语法 + +```graphql +# 定义片段 +fragment UserBasicInfo on User { + id + name + avatarUrl +} + +# 使用片段 +query GetUser($id: ID!) { + user(id: $id) { + ...UserBasicInfo + email + } +} +``` + +### 指令 + +```graphql +query GetUser($id: ID!, $includeEmail: Boolean!) { + user(id: $id) { + id + name + email @include(if: $includeEmail) + } +} + +query GetPosts($skipDrafts: Boolean!) { + posts { + id + title + draft @skip(if: $skipDrafts) + } +} +``` + +## 关键原则 + +### 1. 仅请求所需字段 + +```graphql +# 良好:指定字段 +query GetUserName($id: ID!) { + user(id: $id) { + id + name + } +} + +# 避免:过度获取 +query GetUser($id: ID!) { + user(id: $id) { + id + name + email + bio + posts { + id + title + content + comments { + id + } + } + followers { + id + name + } + # ... 大量未使用的字段 + } +} +``` + +### 2. 为所有操作命名 + +```graphql +# 良好:命名操作 +query GetUserPosts($userId: ID!) { + user(id: $userId) { + posts { + id + title + } + } +} + +# 避免:匿名操作 +query { + user(id: "123") { + posts { + id + title + } + } +} +``` + +### 3. 使用变量,而非内联值 + +```graphql +# 良好:变量 +query GetUser($id: ID!) { + user(id: $id) { + id + name + } +} + +# 避免:硬编码值 +query { + user(id: "123") { + id + name + } +} +``` + +### 4. 将片段与组件共存 + +```tsx +// UserAvatar.tsx +export const USER_AVATAR_FRAGMENT = gql` + fragment UserAvatar on User { + id + name + avatarUrl + } +`; + +function UserAvatar({ user }) { + return {user.name}; +} +``` + +## 参考文件 + +各主题的详细文档: + +- [查询](references/queries.md) —— 查询模式与优化 +- [变更](references/mutations.md) —— 变更模式与错误处理 +- [片段](references/fragments.md) —— 片段组织与复用 +- [变量](references/variables.md) —— 变量用法与类型 +- [工具链](references/tooling.md) —— 代码生成与代码检查 + +## 基本规则 + +- 始终为你的操作命名(禁止匿名查询/变更) +- 始终对动态值使用变量 +- 始终只请求你需要的字段 +- 始终为可缓存类型包含 `id` 字段 +- 切勿在操作中硬编码值 +- 切勿在文件间重复选择相同的字段 +- 优先使用片段来复用字段选择 +- 优先将片段与组件共存 +- 使用描述性的操作名称来体现用途 +- 对条件字段使用 `@include`/`@skip` diff --git a/references/fragments.md b/references/fragments.md new file mode 100644 index 0000000..bc24207 --- /dev/null +++ b/references/fragments.md @@ -0,0 +1,536 @@ +# Fragment Patterns(片段模式) + +本参考文档涵盖了有效组织与使用 GraphQL 片段(Fragment)的模式。 + +## 目录 + +- [Fragment 基础](#fragment-basics) +- [Fragment 同地存放(Colocation)](#fragment-colocation) +- [Fragment 复用](#fragment-reuse) +- [内联 Fragment(Inline Fragments)](#inline-fragments) +- [类型条件(Type Conditions)](#type-conditions) +- [Fragment 组合(Composition)](#fragment-composition) +- [反模式(Anti-Patterns)](#anti-patterns) + +## Fragment 基础 + +### 定义 Fragment + +```graphql +fragment UserBasicInfo on User { + id + name + avatarUrl +} +``` + +### 使用 Fragment + +```graphql +query GetUser($id: ID!) { + user(id: $id) { + ...UserBasicInfo + email + } +} + +fragment UserBasicInfo on User { + id + name + avatarUrl +} +``` + +### Fragment 展开(Spread) + +`...` 运算符用于展开 Fragment 的字段: + +```graphql +query GetPost($id: ID!) { + post(id: $id) { + id + title + author { + ...UserBasicInfo # 展开 id、name、avatarUrl + } + } +} +``` + +## Fragment 同地存放(Colocation) + +### 与组件同地存放 + +将 Fragment 放在使用它们的组件旁边: + +``` +src/ + components/ + UserAvatar/ + UserAvatar.tsx + UserAvatar.fragment.graphql + UserCard/ + UserCard.tsx + UserCard.fragment.graphql + PostList/ + PostList.tsx + PostList.query.graphql +``` + +### 组件拥有自己的数据 + +```tsx +// UserAvatar.tsx +import { gql } from "@apollo/client"; + +export const USER_AVATAR_FRAGMENT = gql` + fragment UserAvatar on User { + id + name + avatarUrl + } +`; + +interface UserAvatarProps { + user: UserAvatarFragment; +} + +export function UserAvatar({ user }: UserAvatarProps) { + return {user.name}; +} +``` + +### 父组件组合 Fragment + +```tsx +// UserCard.tsx +import { gql } from "@apollo/client"; +import { USER_AVATAR_FRAGMENT, UserAvatar } from "./UserAvatar"; + +export const USER_CARD_FRAGMENT = gql` + fragment UserCard on User { + id + name + bio + ...UserAvatar + } + ${USER_AVATAR_FRAGMENT} +`; + +export function UserCard({ user }: { user: UserCardFragment }) { + return ( +
+ +

{user.name}

+

{user.bio}

+
+ ); +} +``` + +### 查询使用组件 Fragment + +```tsx +// UserProfile.tsx +import { gql, useQuery } from "@apollo/client"; +import { USER_CARD_FRAGMENT, UserCard } from "./UserCard"; + +const GET_USER = gql` + query GetUserProfile($id: ID!) { + user(id: $id) { + ...UserCard + email + createdAt + } + } + ${USER_CARD_FRAGMENT} +`; + +export function UserProfile({ userId }: { userId: string }) { + const { data } = useQuery(GET_USER, { variables: { id: userId } }); + + if (!data) return null; + + return ( +
+ +

邮箱:{data.user.email}

+
+ ); +} +``` + +## Fragment 复用 + +### 共享 Fragment + +用于跨多个组件使用的通用模式: + +```graphql +# fragments/common.graphql + +fragment Timestamps on Node { + createdAt + updatedAt +} + +fragment PageInfoFields on PageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor +} +``` + +### 领域特定 Fragment + +```graphql +# fragments/user.graphql + +fragment UserSummary on User { + id + name + avatarUrl +} + +fragment UserProfile on User { + ...UserSummary + bio + location + website + socialLinks { + platform + url + } +} + +fragment UserWithStats on User { + ...UserSummary + followerCount + followingCount + postCount +} +``` + +### 使用共享 Fragment + +```graphql +query GetPost($id: ID!) { + post(id: $id) { + id + title + ...Timestamps + author { + ...UserSummary + } + } +} +``` + +## 内联 Fragment(Inline Fragments) + +### 匿名内联 Fragment + +用于与指令(Directives)一起对字段进行分组: + +```graphql +query GetUser($id: ID!, $includeDetails: Boolean!) { + user(id: $id) { + id + name + ... @include(if: $includeDetails) { + email + bio + website + } + } +} +``` + +### 接口上的内联 Fragment + +```graphql +query GetNodes($ids: [ID!]!) { + nodes(ids: $ids) { + id + ... on User { + name + email + } + ... on Post { + title + content + } + } +} +``` + +## 类型条件(Type Conditions) + +### 联合类型上的 Fragment + +```graphql +query Search($query: String!) { + search(query: $query) { + ... on User { + id + name + avatarUrl + } + ... on Post { + id + title + excerpt + } + ... on Comment { + id + body + post { + id + title + } + } + } +} +``` + +### 针对联合类型的命名 Fragment + +```graphql +query Search($query: String!) { + search(query: $query) { + ...SearchResultUser + ...SearchResultPost + ...SearchResultComment + } +} + +fragment SearchResultUser on User { + id + name + avatarUrl +} + +fragment SearchResultPost on Post { + id + title + excerpt + author { + name + } +} + +fragment SearchResultComment on Comment { + id + body + post { + id + title + } +} +``` + +### 处理 \_\_typename + +```typescript +function SearchResult({ result }) { + switch (result.__typename) { + case 'User': + return ; + case 'Post': + return ; + case 'Comment': + return ; + } +} +``` + +## Fragment 组合(Composition) + +### 构建 Fragment + +```graphql +# 基础 Fragment +fragment PostCore on Post { + id + title + slug +} + +# 扩展 Fragment +fragment PostPreview on Post { + ...PostCore + excerpt + featuredImage { + url + } +} + +# 完整 Fragment +fragment PostFull on Post { + ...PostPreview + content + publishedAt + author { + ...UserSummary + } + tags { + id + name + } +} +``` + +### Fragment 中嵌套 Fragment + +```graphql +fragment CommentWithAuthor on Comment { + id + body + createdAt + author { + ...UserSummary + } +} + +fragment PostWithComments on Post { + id + title + comments(first: 10) { + edges { + node { + ...CommentWithAuthor + } + } + } +} +``` + +### Fragment 展开顺序 + +顺序无关紧要——字段会被合并: + +```graphql +query GetUser($id: ID!) { + user(id: $id) { + ...UserProfile + ...UserStats + # 两个 Fragment 的字段都会被包含 + } +} +``` + +## 反模式(Anti-Patterns) + +### 避免巨型 Fragment + +```graphql +# 错误:字段过多,并非所有地方都需要 +fragment UserEverything on User { + id + name + email + bio + avatarUrl + coverImage + website + location + socialLinks { ... } + posts { ... } + followers { ... } + following { ... } + # ……还有 50 多个字段 +} + +# 正确:针对特定用途的精简 Fragment +fragment UserAvatar on User { + id + name + avatarUrl +} + +fragment UserProfile on User { + id + name + bio + avatarUrl + website + location +} +``` + +### 避免未使用的 Fragment 字段 + +```graphql +# 错误:组件只使用了 name 和 avatarUrl +fragment UserInfo on User { + id + name + email # 未使用 + avatarUrl + bio # 未使用 + website # 未使用 +} + +# 正确:只请求所需字段 +fragment UserInfo on User { + id + name + avatarUrl +} +``` + +### 避免深度嵌套的 Fragment + +```graphql +# 错误:难以理解正在获取什么内容 +fragment Level1 on User { + ...Level2 +} +fragment Level2 on User { + ...Level3 +} +fragment Level3 on User { + ...Level4 +} +# ……继续嵌套 + +# 正确:保持嵌套层级较浅 +fragment UserWithPosts on User { + id + name + posts { + ...PostPreview + } +} +``` + +### 避免循环 Fragment 依赖 + +```graphql +# 错误:循环引用(无法工作) +fragment UserWithPosts on User { + posts { + ...PostWithAuthor + } +} + +fragment PostWithAuthor on Post { + author { + ...UserWithPosts # 循环引用! + } +} + +# 正确:打破循环 +fragment UserWithPosts on User { + posts { + ...PostPreview + } +} + +fragment PostWithAuthor on Post { + author { + ...UserSummary # 不同的 Fragment,没有循环 + } +} +``` diff --git a/references/mutations.md b/references/mutations.md new file mode 100644 index 0000000..8eb5dcc --- /dev/null +++ b/references/mutations.md @@ -0,0 +1,435 @@ +# 变异模式 + +本参考文档涵盖了编写高效 GraphQL 变异的模式。 + +## 目录 + +- [变异结构](#mutation-structure) +- [输入模式](#input-patterns) +- [响应选择](#response-selection) +- [错误处理](#error-handling) +- [乐观更新](#optimistic-updates) +- [变异命名](#mutation-naming) + +## 变异结构 + +### 基础变异 + +```graphql +mutation CreatePost($input: CreatePostInput!) { + createPost(input: $input) { + id + title + createdAt + } +} +``` + +变量: + +```json +{ + "input": { + "title": "My Post", + "content": "Post content..." + } +} +``` + +### 带多个参数的变异 + +```graphql +mutation UpdatePost($id: ID!, $input: UpdatePostInput!) { + updatePost(id: $id, input: $input) { + id + title + updatedAt + } +} +``` + +### 多个变异 + +在单个请求中执行多个变异(顺序执行): + +```graphql +mutation SetupUserProfile($userId: ID!, $profileInput: ProfileInput!, $settingsInput: SettingsInput!) { + updateProfile(userId: $userId, input: $profileInput) { + id + bio + } + updateSettings(userId: $userId, input: $settingsInput) { + id + theme + notifications + } +} +``` + +## 输入模式 + +### 单一输入对象 + +推荐模式——单一输入参数: + +```graphql +mutation CreateUser($input: CreateUserInput!) { + createUser(input: $input) { + id + email + } +} +``` + +```json +{ + "input": { + "email": "user@example.com", + "name": "John Doe", + "password": "secret123" + } +} +``` + +### 嵌套输入对象 + +```graphql +mutation CreateOrder($input: CreateOrderInput!) { + createOrder(input: $input) { + id + total + } +} +``` + +```json +{ + "input": { + "items": [ + { "productId": "prod_1", "quantity": 2 }, + { "productId": "prod_2", "quantity": 1 } + ], + "shippingAddress": { + "street": "123 Main St", + "city": "New York", + "country": "US" + } + } +} +``` + +### 可选字段 + +```graphql +mutation UpdateUser($id: ID!, $input: UpdateUserInput!) { + updateUser(id: $id, input: $input) { + id + name + bio + } +} +``` + +```json +{ + "id": "user_123", + "input": { + "name": "New Name" + // bio 未包含——不会被更改 + } +} +``` + +## 响应选择 + +### 返回修改后的对象 + +始终返回带有更新字段的变异对象: + +```graphql +mutation UpdatePost($id: ID!, $input: UpdatePostInput!) { + updatePost(id: $id, input: $input) { + id + title + content + updatedAt # 服务端设置的字段 + } +} +``` + +### 返回相关对象 + +如果变异影响了关联数据,请包含它: + +```graphql +mutation AddComment($input: AddCommentInput!) { + addComment(input: $input) { + id + body + post { + id + commentCount # 更新后的计数 + } + author { + id + name + } + } +} +``` + +### 为缓存更新返回 + +选择更新缓存所需的字段: + +```graphql +mutation DeletePost($id: ID!) { + deletePost(id: $id) { + id # 需要从缓存中移除 + author { + id + postCount # 可能需要递减 + } + } +} +``` + +### 为列表更新返回连接 + +```graphql +mutation CreatePost($input: CreatePostInput!) { + createPost(input: $input) { + id + title + createdAt + author { + id + posts(first: 1) { + edges { + node { + id + } + } + totalCount + } + } + } +} +``` + +## 错误处理 + +### 查询结果联合类型 + +当 schema 使用联合类型来表示错误时: + +```graphql +mutation CreateUser($input: CreateUserInput!) { + createUser(input: $input) { + ... on CreateUserSuccess { + user { + id + email + } + } + ... on ValidationError { + message + field + } + ... on EmailAlreadyExists { + message + existingUserId + } + } +} +``` + +### 处理所有情况 + +```typescript +const result = await client.mutate({ + mutation: CREATE_USER, + variables: { input }, +}); + +const { createUser } = result.data; + +switch (createUser.__typename) { + case "CreateUserSuccess": + // 处理成功 + return createUser.user; + case "ValidationError": + // 处理验证错误 + throw new ValidationError(createUser.field, createUser.message); + case "EmailAlreadyExists": + // 处理特定的业务错误 + throw new EmailExistsError(createUser.existingUserId); +} +``` + +### GraphQL 错误 + +处理网络错误和 GraphQL 错误: + +```typescript +try { + const result = await client.mutate({ + mutation: CREATE_POST, + variables: { input }, + }); + return result.data.createPost; +} catch (error) { + if (error.graphQLErrors?.length) { + // 处理 GraphQL 错误 + const gqlError = error.graphQLErrors[0]; + if (gqlError.extensions?.code === "UNAUTHENTICATED") { + // 重定向到登录页 + } + } + if (error.networkError) { + // 处理网络错误 + } + throw error; +} +``` + +## 乐观更新 + +### 为乐观响应选择字段 + +包含所有将立即显示的字段: + +```graphql +mutation LikePost($postId: ID!) { + likePost(postId: $postId) { + id + likeCount + isLikedByViewer + } +} +``` + +```typescript +client.mutate({ + mutation: LIKE_POST, + variables: { postId: "post_123" }, + optimisticResponse: { + likePost: { + __typename: "Post", + id: "post_123", + likeCount: currentCount + 1, + isLikedByViewer: true, + }, + }, +}); +``` + +### 包含创建的 ID + +对于创建型变异,使用临时 ID: + +```graphql +mutation AddComment($input: AddCommentInput!) { + addComment(input: $input) { + id + body + createdAt + author { + id + name + avatarUrl + } + } +} +``` + +```typescript +client.mutate({ + mutation: ADD_COMMENT, + variables: { input: { postId, body } }, + optimisticResponse: { + addComment: { + __typename: "Comment", + id: `temp-${Date.now()}`, // 临时 ID + body, + createdAt: new Date().toISOString(), + author: { + __typename: "User", + id: currentUser.id, + name: currentUser.name, + avatarUrl: currentUser.avatarUrl, + }, + }, + }, +}); +``` + +## 变异命名 + +### 命名约定 + +| 操作 | 模式 | 示例 | +| ------------ | ------------------- | ----------------------------------- | +| 创建(Create) | `Create{Type}` | `CreateUser`, `CreatePost` | +| 更新(Update) | `Update{Type}` | `UpdateUser`, `UpdatePost` | +| 删除(Delete) | `Delete{Type}` | `DeleteUser`, `DeletePost` | +| 操作(Action) | `{Verb}{Type}` | `PublishPost`, `ArchiveProject` | +| 关系(Relationship) | `{Add/Remove}{Type}` | `AddTeamMember`, `RemoveTag` | + +### 好的命名示例 + +```graphql +mutation CreateUser($input: CreateUserInput!) { ... } +mutation UpdateUserProfile($userId: ID!, $input: ProfileInput!) { ... } +mutation DeletePost($id: ID!) { ... } +mutation PublishArticle($id: ID!) { ... } +mutation ArchiveProject($id: ID!) { ... } +mutation AddItemToCart($input: AddItemInput!) { ... } +mutation RemoveTeamMember($teamId: ID!, $userId: ID!) { ... } +mutation FollowUser($userId: ID!) { ... } +mutation MarkNotificationAsRead($id: ID!) { ... } +``` + +### 操作名称与服务端一致 + +使客户端操作名称与服务端变异名称一致: + +```graphql +# 服务端 schema +type Mutation { + createPost(input: CreatePostInput!): Post! +} + +# 客户端操作——名称反映其行为 +mutation CreatePost($input: CreatePostInput!) { + createPost(input: $input) { + id + title + } +} +``` + +### 上下文特定的名称 + +当同一个变异用于不同场景时,添加上下文: + +```graphql +# 用于创建草稿 +mutation CreateDraftPost($input: CreatePostInput!) { + createPost(input: $input) { + id + title + status + } +} + +# 用于创建并立即发布 +mutation CreateAndPublishPost($input: CreatePostInput!) { + createPost(input: $input) { + id + title + status + publishedAt + } +} +``` diff --git a/references/queries.md b/references/queries.md new file mode 100644 index 0000000..034f589 --- /dev/null +++ b/references/queries.md @@ -0,0 +1,504 @@ +# 查询模式 + +本参考文档涵盖了编写高效 GraphQL 查询的模式。 + +## 目录 + +- [查询结构](#查询结构) +- [字段选择](#字段选择) +- [别名](#别名) +- [指令](#指令) +- [查询命名](#查询命名) +- [查询组织](#查询组织) +- [性能优化](#性能优化) + +## 查询结构 + +### 基本查询 + +```graphql +query GetUser($id: ID!) { + user(id: $id) { + id + name + email + } +} +``` + +组成部分: + +- `query` — 操作类型 +- `GetUser` — 操作名称 +- `($id: ID!)` — 变量定义 +- `user(id: $id)` — 带参数的字段 +- `{ id name email }` — 选择集 + +### 多根字段查询 + +```graphql +query GetDashboardData($userId: ID!) { + user(id: $userId) { + id + name + } + notifications(first: 5) { + id + message + } + stats { + totalPosts + totalComments + } +} +``` + +### 嵌套查询 + +```graphql +query GetUserWithPosts($userId: ID!) { + user(id: $userId) { + id + name + posts(first: 10) { + edges { + node { + id + title + comments(first: 3) { + edges { + node { + id + body + } + } + } + } + } + } + } +} +``` + +## 字段选择 + +### 只请求需要的字段 + +```graphql +# 用户卡片组件 +query GetUserCard($id: ID!) { + user(id: $id) { + id + name + avatarUrl + # 如果不展示,不要请求 email、bio 等字段 + } +} +``` + +### 始终包含 ID 字段 + +为可能缓存或重新获取的类型包含 `id`: + +```graphql +query GetPost($id: ID!) { + post(id: $id) { + id # 始终包含,用于缓存 + title + author { + id # 包含,用于作者缓存条目 + name + } + } +} +``` + +### 选择连接(Connection) + +对于分页数据,按需请求: + +```graphql +query GetUserPosts($userId: ID!, $first: Int!, $after: String) { + user(id: $userId) { + id + posts(first: $first, after: $after) { + edges { + node { + id + title + excerpt + } + cursor # 仅在实现无限滚动时需要 + } + pageInfo { + hasNextPage + endCursor + } + totalCount # 仅在展示总数时需要 + } + } +} +``` + +## 别名 + +### 基本别名 + +在响应中重命名字段: + +```graphql +query GetUserNames($id: ID!) { + user(id: $id) { + userId: id + displayName: name + } +} + +# 响应:{ user: { userId: "123", displayName: "John" } } +``` + +### 多次查询同一字段 + +```graphql +query GetMultipleUsers { + admin: user(id: "1") { + id + name + } + moderator: user(id: "2") { + id + name + } + currentUser: user(id: "3") { + id + name + } +} +``` + +### 使用不同参数的别名 + +```graphql +query GetPostsByStatus($userId: ID!) { + user(id: $userId) { + id + publishedPosts: posts(status: PUBLISHED, first: 5) { + edges { + node { + id + title + } + } + } + draftPosts: posts(status: DRAFT, first: 5) { + edges { + node { + id + title + } + } + } + } +} +``` + +## 指令 + +### @include 指令 + +仅当条件为 true 时包含字段: + +```graphql +query GetUser($id: ID!, $includeEmail: Boolean!) { + user(id: $id) { + id + name + email @include(if: $includeEmail) + } +} + +# 变量:{ id: "123", includeEmail: true } +# 返回 email 字段 + +# 变量:{ id: "123", includeEmail: false } +# 不返回 email 字段 +``` + +### @skip 指令 + +当条件为 true 时跳过字段: + +```graphql +query GetPost($id: ID!, $isPreview: Boolean!) { + post(id: $id) { + id + title + content @skip(if: $isPreview) + excerpt + } +} +``` + +### 在片段上使用指令 + +```graphql +query GetUser($id: ID!, $expanded: Boolean!) { + user(id: $id) { + id + name + ...UserDetails @include(if: $expanded) + } +} + +fragment UserDetails on User { + bio + website + socialLinks { + platform + url + } +} +``` + +### 组合使用指令 + +```graphql +query GetPost($id: ID!, $showComments: Boolean!, $hideAuthor: Boolean!) { + post(id: $id) { + id + title + author @skip(if: $hideAuthor) { + id + name + } + comments(first: 10) @include(if: $showComments) { + edges { + node { + id + body + } + } + } + } +} +``` + +## 查询命名 + +### 命名约定 + +| 用途 | 模式 | 示例 | +| --------------- | --------------- | ----------------------------------- | +| 获取单个条目 | `Get{Type}` | `GetUser`,`GetPost` | +| 获取列表 | `List{Types}` | `ListUsers`,`ListPosts` | +| 搜索 | `Search{Types}` | `SearchUsers`,`SearchProducts` | +| 为特定 UI 获取 | `Get{Feature}Data` | `GetDashboardData`,`GetProfilePage` | + +### 好的命名 + +```graphql +query GetUserProfile($id: ID!) { ... } +query ListRecentPosts($first: Int!) { ... } +query SearchProducts($query: String!) { ... } +query GetOrderDetails($orderId: ID!) { ... } +query GetHomeFeed($userId: ID!) { ... } +``` + +### 避免泛化命名 + +```graphql +# 避免 +query Data { ... } +query Query1 { ... } +query FetchStuff { ... } + +# 推荐 +query GetCurrentUser { ... } +query ListActiveProjects { ... } +query SearchCustomers($query: String!) { ... } +``` + +## 查询组织 + +### 每个文件一个查询 + +``` +src/ + graphql/ + queries/ + GetUser.graphql + ListPosts.graphql + SearchProducts.graphql +``` + +```graphql +# GetUser.graphql +query GetUser($id: ID!) { + user(id: $id) { + id + name + email + } +} +``` + +### 与组件同目录放置 + +``` +src/ + components/ + UserProfile/ + UserProfile.tsx + UserProfile.graphql + UserProfile.test.tsx +``` + +### 导入并使用 + +```typescript +// 使用 graphql-tag +import { gql } from "@apollo/client"; + +export const GET_USER = gql` + query GetUser($id: ID!) { + user(id: $id) { + id + name + } + } +`; + +// 使用 .graphql 文件(需要加载器) +import { GetUserDocument } from "./UserProfile.generated"; +``` + +## 性能优化 + +### 避免过度获取 + +只请求组件会用到的字段: + +```graphql +# 列表视图——最少字段 +query ListPostsForIndex { + posts(first: 20) { + edges { + node { + id + title + excerpt + author { name } + } + } + } +} + +# 详情视图——更多字段 +query GetPostDetail($id: ID!) { + post(id: $id) { + id + title + content + publishedAt + author { + id + name + bio + avatarUrl + } + comments(first: 10) { ... } + } +} +``` + +### 使用分页 + +切勿获取无界列表: + +```graphql +# 避免 +query GetAllPosts { + posts { + # 可能返回数千条 + id + title + } +} + +# 推荐 +query GetPosts($first: Int = 20, $after: String) { + posts(first: $first, after: $after) { + edges { + node { + id + title + } + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +### 批量合并相关查询 + +在一次请求中获取相关数据: + +```graphql +# 替代多次查询 +query GetDashboard($userId: ID!) { + user(id: $userId) { + id + name + } + recentPosts: posts(first: 5, orderBy: { field: CREATED_AT, direction: DESC }) { + edges { + node { + id + title + } + } + } + notifications(first: 10, unreadOnly: true) { + edges { + node { + id + message + } + } + } +} +``` + +### 对重复选择集使用片段 + +```graphql +query GetPostsWithAuthors { + posts(first: 10) { + edges { + node { + id + title + author { + ...AuthorInfo + } + } + } + } + featuredPost { + id + title + author { + ...AuthorInfo + } + } +} + +fragment AuthorInfo on User { + id + name + avatarUrl +} +``` diff --git a/references/tooling.md b/references/tooling.md new file mode 100644 index 0000000..4e55a1b --- /dev/null +++ b/references/tooling.md @@ -0,0 +1,410 @@ +--- +name: tooling +description: 用于处理 GraphQL 操作的工具参考,包括代码生成与代码检查 +metadata: + type: reference +--- + +# 工具 + +本参考文档涵盖了用于处理 GraphQL 操作的工具,包括代码生成和代码检查。 + +## 目录 + +- [GraphQL 代码生成器](#graphql-code-generator) +- [ESLint GraphQL](#eslint-graphql) +- [IDE 扩展](#ide-extensions) +- [操作验证](#operation-validation) + +## GraphQL 代码生成器 + +### 概述 + +GraphQL 代码生成器可从你的 schema 和操作中生成 TypeScript 类型,确保整个应用程序的类型安全。 + +### 安装 + +```bash +npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node +``` + +### 基本配置 + +创建 `codegen.ts`: + +```typescript +// codegen.ts +import { CodegenConfig } from "@graphql-codegen/cli"; + +const config: CodegenConfig = { + overwrite: true, + schema: "", + // 假设所有源文件都在顶层的 `src/` 目录下——你可能需要根据你的文件结构调整此项 + documents: ["src/**/*.{ts,tsx}"], + // 当没有文档时不以非零状态退出 + ignoreNoDocuments: true, + generates: { + // 使用最适合你应用程序结构的路径 + "./src/types/__generated__/graphql.ts": { + plugins: ["typescript", "typescript-operations", "typed-document-node"], + config: { + avoidOptionals: { + // 对可空字段使用 `null` 而非可选字段 + field: true, + // 允许可空输入字段保持未指定状态 + inputValue: false, + }, + // 对未配置的标量使用 `unknown` 而非 `any` + defaultScalarType: "unknown", + // Apollo Client 始终包含 `__typename` 字段 + nonOptionalTypename: true, + // Apollo Client 不会为根类型添加 `__typename` 字段,因此 + // 不要为根操作类型生成 `__typename` 的类型。 + skipTypeNameForRoot: true, + }, + }, + }, +}; + +export default config; +``` + +### 运行生成 + +```bash +# 一次性生成 +npx graphql-codegen + +# 开发时的监听模式 +npx graphql-codegen --watch +``` + +### Package 脚本 + +```json +{ + "scripts": { + "codegen": "graphql-codegen", + "codegen:watch": "graphql-codegen --watch" + } +} +``` + +### 生成类型的使用 + +```tsx +// 之前:手动类型定义 +const GET_USER = gql` + query GetUser($id: ID!) { + user(id: $id) { + id + name + } + } +`; + +// 手动类型定义 +interface GetUserData { + user: { + id: string; + name: string; + } | null; +} + +const { data } = useQuery(GET_USER, { variables: { id } }); + +// 之后:使用生成类型 +import { useGetUserQuery } from "./generated/graphql"; + +const { data } = useGetUserQuery({ variables: { id } }); +// data.user 现在是完整类型化的! +``` + +### 近操作文件生成 + +在操作文件旁边生成类型: + +```typescript +const config: CodegenConfig = { + schema: "http://localhost:4000/graphql", + documents: ["src/**/*.graphql"], + generates: { + "src/": { + preset: "near-operation-file", + presetConfig: { + extension: ".generated.ts", + baseTypesPath: "generated/graphql.ts", + }, + plugins: ["typescript-operations", "typescript-react-apollo"], + }, + "src/generated/graphql.ts": { + plugins: ["typescript"], + }, + }, +}; +``` + +结果如下: + +``` +src/ + components/ + UserCard/ + UserCard.graphql + UserCard.generated.ts # 该文件的生成类型 +``` + +### 片段类型 + +```tsx +// UserAvatar.graphql +// fragment UserAvatar on User { +// id +// name +// avatarUrl +// } + +import { UserAvatarFragment } from "./UserAvatar.generated"; + +interface UserAvatarProps { + user: UserAvatarFragment; +} + +export function UserAvatar({ user }: UserAvatarProps) { + return {user.name}; +} +``` + +## ESLint GraphQL + +### 安装 + +```bash +npm install -D @graphql-eslint/eslint-plugin +``` + +### 配置 + +```javascript +// eslint.config.js(扁平配置) +import graphqlPlugin from "@graphql-eslint/eslint-plugin"; + +export default [ + { + files: ["**/*.graphql"], + languageOptions: { + parser: graphqlPlugin.parser, + }, + plugins: { + "@graphql-eslint": graphqlPlugin, + }, + rules: { + "@graphql-eslint/known-type-names": "error", + "@graphql-eslint/no-anonymous-operations": "error", + "@graphql-eslint/no-duplicate-fields": "error", + "@graphql-eslint/naming-convention": [ + "error", + { + OperationDefinition: { + style: "PascalCase", + forbiddenPrefixes: ["Query", "Mutation", "Subscription"], + }, + FragmentDefinition: { + style: "PascalCase", + }, + }, + ], + }, + }, +]; +``` + +### 推荐规则 + +```javascript +rules: { + // 语法与合法性 + '@graphql-eslint/known-type-names': 'error', + '@graphql-eslint/known-fragment-names': 'error', + '@graphql-eslint/no-undefined-variables': 'error', + '@graphql-eslint/no-unused-variables': 'error', + '@graphql-eslint/no-unused-fragments': 'error', + '@graphql-eslint/unique-operation-name': 'error', + '@graphql-eslint/unique-fragment-name': 'error', + + // 最佳实践 + '@graphql-eslint/no-anonymous-operations': 'error', + '@graphql-eslint/no-duplicate-fields': 'error', + '@graphql-eslint/require-id-when-available': 'warn', + + // 命名规范 + '@graphql-eslint/naming-convention': ['error', { ... }], +} +``` + +### Schema 感知规则 + +提供 schema 以实现高级验证: + +```javascript +{ + files: ['**/*.graphql'], + languageOptions: { + parser: graphqlPlugin.parser, + parserOptions: { + schema: './schema.graphql', + // 或者 + schema: 'http://localhost:4000/graphql', + }, + }, +} +``` + +## IDE 扩展 + +### VS Code + +**GraphQL:语言功能支持**(GraphQL 基金会) + +- 语法高亮 +- Schema 类型的自动补全 +- 跳转到定义 +- 悬停文档 +- 针对 schema 的验证 + +配置(`.graphqlrc.yml`): + +```yaml +schema: "http://localhost:4000/graphql" +documents: "src/**/*.{graphql,ts,tsx}" +``` + +**Apollo GraphQL**(Apollo) + +- Apollo 特有功能 +- Schema 注册表集成 +- 性能洞察 + +### JetBrains IDE + +**GraphQL** 插件: + +- 语法高亮 +- Schema 感知的补全 +- 验证 +- 导航到定义 + +配置(`.graphqlconfig`): + +```json +{ + "schemaPath": "./schema.graphql", + "includes": ["src/**/*.graphql"] +} +``` + +### 配置文件 + +常见的配置文件名: + +- `.graphqlrc`(JSON) +- `.graphqlrc.yml`(YAML) +- `.graphqlrc.json`(JSON) +- `graphql.config.js`(JavaScript) + +```yaml +# .graphqlrc.yml +schema: "http://localhost:4000/graphql" +documents: "src/**/*.graphql" +extensions: + codegen: + generates: + ./src/generated/graphql.ts: + plugins: + - typescript + - typescript-operations +``` + +## 操作验证 + +### 针对 Schema 进行验证 + +```bash +# 使用 graphql-inspector +npx graphql-inspector validate ./src/**/*.graphql ./schema.graphql +``` + +### CI 集成 + +```yaml +# .github/workflows/graphql.yml +name: GraphQL 验证 + +on: [push, pull_request] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: 安装依赖 + run: npm ci + + - name: 下载 schema + run: npx graphql-inspector introspect http://localhost:4000/graphql --write schema.graphql + + - name: 验证操作 + run: npx graphql-inspector validate './src/**/*.graphql' schema.graphql + + - name: 检查破坏性变更 + run: npx graphql-inspector diff schema.graphql http://localhost:4000/graphql +``` + +### Pre-commit 钩子 + +```json +// package.json +{ + "lint-staged": { + "*.graphql": ["eslint --fix", "graphql-inspector validate ./schema.graphql"] + } +} +``` + +### 操作复杂度检查 + +```bash +# 检查查询复杂度 +npx graphql-query-complexity-checker \ + --schema ./schema.graphql \ + --query ./src/queries/GetUser.graphql \ + --max-complexity 100 +``` + +### 持久化查询提取 + +生成用于生产环境的持久化查询: + +```typescript +// codegen.ts +const config: CodegenConfig = { + generates: { + "./persisted-queries.json": { + plugins: ["graphql-codegen-persisted-query-ids"], + config: { + output: "client", + algorithm: "sha256", + }, + }, + }, +}; +``` + +输出: + +```json +{ + "abc123...": "query GetUser($id: ID!) { user(id: $id) { id name } }", + "def456...": "mutation CreatePost($input: CreatePostInput!) { ... }" +} diff --git a/references/variables.md b/references/variables.md new file mode 100644 index 0000000..3f87eb5 --- /dev/null +++ b/references/variables.md @@ -0,0 +1,440 @@ +# GraphQL 变量模式 + +本参考文档涵盖在 GraphQL 操作中使用变量的模式。 + +## 目录 + +- [变量基础](#variable-basics) +- [变量类型](#variable-types) +- [默认值](#default-values) +- [复杂输入](#complex-inputs) +- [最佳实践](#best-practices) + +## 变量基础 + +### 声明变量 + +变量在操作定义中声明: + +```graphql +query GetUser($id: ID!) { + user(id: $id) { + id + name + } +} +``` + +### 使用变量 + +变量以 `$` 前缀引用: + +```graphql +query GetUser($id: ID!) { + user(id: $id) { + # 此处使用 $id + id + name + } +} +``` + +### 传递变量 + +变量以独立的 JSON 对象形式传递: + +```typescript +const { data } = await client.query({ + query: GET_USER, + variables: { + id: "user_123", + }, +}); +``` + +### 多个变量 + +```graphql +query SearchPosts($query: String!, $status: PostStatus, $first: Int!, $after: String) { + searchPosts(query: $query, status: $status, first: $first, after: $after) { + edges { + node { + id + title + } + } + } +} +``` + +```json +{ + "query": "graphql", + "status": "PUBLISHED", + "first": 10, + "after": "cursor_abc" +} +``` + +## 变量类型 + +### 标量类型 + +```graphql +query Example( + $id: ID! + $name: String! + $count: Int! + $price: Float! + $active: Boolean! +) { + # ... +} +``` + +### 自定义标量类型 + +```graphql +query Example( + $date: DateTime! + $email: Email! + $url: URL! +) { + # ... +} +``` + +### 枚举类型 + +```graphql +query GetPosts($status: PostStatus!) { + posts(status: $status) { + id + title + } +} +``` + +```json +{ + "status": "PUBLISHED" +} +``` + +### 列表类型 + +```graphql +query GetUsers($ids: [ID!]!) { + users(ids: $ids) { + id + name + } +} +``` + +```json +{ + "ids": ["user_1", "user_2", "user_3"] +} +``` + +### 输入对象类型 + +```graphql +mutation CreatePost($input: CreatePostInput!) { + createPost(input: $input) { + id + } +} +``` + +```json +{ + "input": { + "title": "My Post", + "content": "Post content...", + "tags": ["graphql", "api"] + } +} +``` + +### 必需 vs 可选 + +```graphql +query Example( + $required: String! # 必须提供,不能为 null + $optional: String # 可以省略或为 null + $requiredList: [String!]! # 列表必填,元素必填 + $optionalList: [String] # 列表可选,元素可选 +) { + # ... +} +``` + +## 默认值 + +### 简单默认值 + +```graphql +query GetPosts($first: Int = 10, $status: PostStatus = PUBLISHED) { + posts(first: $first, status: $status) { + id + title + } +} +``` + +如果未提供,则使用默认值: + +```json +{} +// 等效于:{ "first": 10, "status": "PUBLISHED" } +``` + +覆盖默认值: + +```json +{ + "first": 20 +} +// 使用 first: 20,status: PUBLISHED(默认值) +``` + +### 可选变量的默认值 + +```graphql +# 变量为可选(无 !),但有默认值 +query GetPosts($first: Int = 10) { + posts(first: $first) { + id + } +} +``` + +### 复杂类型的默认值 + +```graphql +query GetPosts($orderBy: PostOrderInput = { field: CREATED_AT, direction: DESC }) { + posts(orderBy: $orderBy) { + id + title + } +} +``` + +### 何时使用默认值 + +在以下场景使用默认值: + +- 分页限制(`first: Int = 20`) +- 排序顺序(`direction: SortDirection = DESC`) +- 常见筛选值(`status: Status = ACTIVE`) +- 功能开关(`includeArchived: Boolean = false`) + +## 复杂输入 + +### 嵌套输入对象 + +```graphql +mutation CreateOrder($input: CreateOrderInput!) { + createOrder(input: $input) { + id + total + } +} +``` + +```json +{ + "input": { + "customer": { + "email": "customer@example.com", + "name": "John Doe" + }, + "items": [ + { "productId": "prod_1", "quantity": 2 }, + { "productId": "prod_2", "quantity": 1 } + ], + "shippingAddress": { + "street": "123 Main St", + "city": "New York", + "state": "NY", + "zipCode": "10001", + "country": "US" + } + } +} +``` + +### 输入对象列表 + +```graphql +mutation BulkCreateUsers($inputs: [CreateUserInput!]!) { + bulkCreateUsers(inputs: $inputs) { + id + email + } +} +``` + +```json +{ + "inputs": [ + { "email": "user1@example.com", "name": "User 1" }, + { "email": "user2@example.com", "name": "User 2" }, + { "email": "user3@example.com", "name": "User 3" } + ] +} +``` + +### 筛选输入 + +```graphql +query SearchProducts($filter: ProductFilter!) { + products(filter: $filter) { + id + name + price + } +} +``` + +```json +{ + "filter": { + "category": "ELECTRONICS", + "priceRange": { + "min": 100, + "max": 500 + }, + "inStock": true, + "tags": ["featured", "sale"] + } +} +``` + +## 最佳实践 + +### 始终对动态值使用变量 + +```graphql +# 好:使用变量 +query GetUser($id: ID!) { + user(id: $id) { + id + name + } +} + +# 差:硬编码值 +query GetUser { + user(id: "123") { + id + name + } +} +``` + +### 变量名与参数名保持一致 + +```graphql +# 好:关系清晰 +query GetUser($userId: ID!) { + user(id: $userId) { + id + } +} + +# 也不错:同名 +query GetUser($id: ID!) { + user(id: $id) { + id + } +} + +# 差:命名混乱 +query GetUser($x: ID!) { + user(id: $x) { + id + } +} +``` + +### 使用描述性变量名 + +```graphql +# 好 +query SearchPosts( + $searchQuery: String! + $authorId: ID + $publishedAfter: DateTime + $maxResults: Int = 20 +) { + searchPosts( + query: $searchQuery + author: $authorId + after: $publishedAfter + first: $maxResults + ) { + # ... + } +} + +# 差 +query SearchPosts($q: String!, $a: ID, $d: DateTime, $n: Int) { + # ... +} +``` + +### 对相关变量进行分组 + +```typescript +// 好:variables 对象反映输入结构 +const variables = { + input: { + title: formData.title, + content: formData.content, + tags: formData.tags, + }, +}; + +// 不够清晰:扁平变量 +const variables = { + title: formData.title, + content: formData.content, + tags: formData.tags, +}; +``` + +### 在客户端校验变量 + +```typescript +function createPost(input: CreatePostInput) { + // 发送前校验 + if (!input.title?.trim()) { + throw new Error("Title is required"); + } + if (input.title.length > 200) { + throw new Error("Title too long"); + } + + return client.mutate({ + mutation: CREATE_POST, + variables: { input }, + }); +} +``` + +### 使用 TypeScript 为变量提供类型 + +```typescript +// 从 schema 生成的类型 +interface GetUserQueryVariables { + id: string; +} + +// 配合 Apollo Client 使用 +const { data } = useQuery(GET_USER, { + variables: { id: userId }, // 类型已检查 +}); +```