chore: import zh skill graphql-operations

This commit is contained in:
wehub-skill-sync
2026-07-13 21:36:20 +08:00
commit 395154a021
7 changed files with 2578 additions and 0 deletions
+9
View File
@@ -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 清单整理
- 原作者、版权和许可证信息以上游仓库为准
+244
View File
@@ -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 <img src={user.avatarUrl} alt={user.name} />;
}
```
## 参考文件
各主题的详细文档:
- [查询](references/queries.md) —— 查询模式与优化
- [变更](references/mutations.md) —— 变更模式与错误处理
- [片段](references/fragments.md) —— 片段组织与复用
- [变量](references/variables.md) —— 变量用法与类型
- [工具链](references/tooling.md) —— 代码生成与代码检查
## 基本规则
- 始终为你的操作命名(禁止匿名查询/变更)
- 始终对动态值使用变量
- 始终只请求你需要的字段
- 始终为可缓存类型包含 `id` 字段
- 切勿在操作中硬编码值
- 切勿在文件间重复选择相同的字段
- 优先使用片段来复用字段选择
- 优先将片段与组件共存
- 使用描述性的操作名称来体现用途
- 对条件字段使用 `@include`/`@skip`
+536
View File
@@ -0,0 +1,536 @@
# Fragment Patterns(片段模式)
本参考文档涵盖了有效组织与使用 GraphQL 片段(Fragment)的模式。
## 目录
- [Fragment 基础](#fragment-basics)
- [Fragment 同地存放(Colocation](#fragment-colocation)
- [Fragment 复用](#fragment-reuse)
- [内联 FragmentInline 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 <img src={user.avatarUrl} alt={user.name} className="avatar" />;
}
```
### 父组件组合 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 (
<div className="user-card">
<UserAvatar user={user} />
<h3>{user.name}</h3>
<p>{user.bio}</p>
</div>
);
}
```
### 查询使用组件 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 (
<div>
<UserCard user={data.user} />
<p>{data.user.email}</p>
</div>
);
}
```
## 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
}
}
}
```
## 内联 FragmentInline 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 <UserResult user={result} />;
case 'Post':
return <PostResult post={result} />;
case 'Comment':
return <CommentResult comment={result} />;
}
}
```
## 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,没有循环
}
}
```
+435
View File
@@ -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
}
}
```
+504
View File
@@ -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
}
```
+410
View File
@@ -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: "<URL_OF_YOUR_GRAPHQL_API>",
// 假设所有源文件都在顶层的 `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<GetUserData>(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 <img src={user.avatarUrl} alt={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!) { ... }"
}
+440
View File
@@ -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: 20status: 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<GetUserQuery, GetUserQueryVariables>(GET_USER, {
variables: { id: userId }, // 类型已检查
});
```