Relay片段模式Skill relay-fragments-patterns

这个技能专注于使用Relay的片段模式来构建可维护的React应用程序。通过组件级数据声明、自动组合、数据掩码和优化数据获取,实现高效的数据依赖管理和组件共位,适用于数据密集型应用如社交媒体、电商平台等。关键词:Relay、React、GraphQL、片段、数据掩码、组件共位、前端开发。

前端开发 0 次安装 0 次浏览 更新于 3/25/2026

名称: relay-fragments-patterns 用户可调用: false 描述: 使用Relay片段组合、数据掩码、共位和容器模式来构建React应用程序。 允许工具:

  • 编辑
  • 搜索
  • 全局
  • 终端

Relay片段模式

掌握Relay的片段组合,用于构建可维护的React应用程序,具有正确的数据依赖和组件共位。

概述

Relay片段允许组件级数据声明,具有自动组合、数据掩码和优化数据获取。片段将数据需求与组件共位,以提高可维护性。

安装和设置

安装Relay

# 安装Relay包
npm install react-relay relay-runtime

# 安装Relay编译器
npm install --save-dev relay-compiler babel-plugin-relay

# 安装GraphQL
npm install graphql

Relay配置

// relay.config.js
module.exports = {
  src: './src',
  schema: './schema.graphql',
  exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**'],
  language: 'typescript',
  artifactDirectory: './src/__generated__'
};

// package.json
{
  "scripts": {
    "relay": "relay-compiler",
    "relay:watch": "relay-compiler --watch"
  }
}

环境设置

// RelayEnvironment.js
import {
  Environment,
  Network,
  RecordSource,
  Store
} from 'relay-runtime';

function fetchQuery(operation, variables) {
  return fetch('http://localhost:4000/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${localStorage.getItem('token')}`
    },
    body: JSON.stringify({
      query: operation.text,
      variables
    })
  }).then(response => response.json());
}

const environment = new Environment({
  network: Network.create(fetchQuery),
  store: new Store(new RecordSource())
});

export default environment;

核心模式

1. 基础片段定义

// PostCard.jsx
import { graphql, useFragment } from 'react-relay';

const PostCardFragment = graphql`
  fragment PostCard_post on Post {
    id
    title
    excerpt
    publishedAt
    author {
      name
      avatar
    }
  }
`;

function PostCard({ post }) {
  const data = useFragment(PostCardFragment, post);

  return (
    <article>
      <h2>{data.title}</h2>
      <p>{data.excerpt}</p>
      <div>
        <img src={data.author.avatar} alt={data.author.name} />
        <span>{data.author.name}</span>
      </div>
      <time>{data.publishedAt}</time>
    </article>
  );
}

export default PostCard;

2. 片段组合

// UserProfile.jsx
const UserProfileFragment = graphql`
  fragment UserProfile_user on User {
    id
    name
    bio
    ...UserAvatar_user
    ...UserStats_user
  }
`;

function UserProfile({ user }) {
  const data = useFragment(UserProfileFragment, user);

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.bio}</p>
      <UserAvatar user={data} />
      <UserStats user={data} />
    </div>
  );
}

// UserAvatar.jsx
const UserAvatarFragment = graphql`
  fragment UserAvatar_user on User {
    name
    avatar
    isOnline
  }
`;

function UserAvatar({ user }) {
  const data = useFragment(UserAvatarFragment, user);

  return (
    <div className="avatar-container">
      <img src={data.avatar} alt={data.name} />
      {data.isOnline && <span className="online-indicator" />}
    </div>
  );
}

// UserStats.jsx
const UserStatsFragment = graphql`
  fragment UserStats_user on User {
    postsCount
    followersCount
    followingCount
  }
`;

function UserStats({ user }) {
  const data = useFragment(UserStatsFragment, user);

  return (
    <div className="stats">
      <div>帖子数: {data.postsCount}</div>
      <div>粉丝数: {data.followersCount}</div>
      <div>关注数: {data.followingCount}</div>
    </div>
  );
}

3. 片段参数

// Post.jsx
const PostFragment = graphql`
  fragment Post_post on Post
  @argumentDefinitions(
    includeComments: { type: "Boolean!", defaultValue: false }
    commentsFirst: { type: "Int", defaultValue: 10 }
  ) {
    id
    title
    body
    comments(first: $commentsFirst) @include(if: $includeComments) {
      edges {
        node {
          ...Comment_comment
        }
      }
    }
  }
`;

function Post({ post, showComments = false }) {
  const data = useFragment(
    PostFragment,
    post,
    {
      includeComments: showComments,
      commentsFirst: 20
    }
  );

  return (
    <article>
      <h1>{data.title}</h1>
      <div>{data.body}</div>
      {showComments && (
        <CommentsList comments={data.comments.edges.map(e => e.node)} />
      )}
    </article>
  );
}

4. 数据掩码

// 父组件
const ParentFragment = graphql`
  fragment Parent_data on Query {
    user {
      id
      ...Child_user
    }
  }
`;

function Parent({ data }) {
  const parentData = useFragment(ParentFragment, data);

  // parentData.user 仅包含 id 和片段引用
  // 这里无法访问 user.name(数据掩码)

  return (
    <div>
      <h1>用户ID: {parentData.user.id}</h1>
      <Child user={parentData.user} />
    </div>
  );
}

// 子组件
const ChildFragment = graphql`
  fragment Child_user on User {
    name
    email
    avatar
  }
`;

function Child({ user }) {
  const data = useFragment(ChildFragment, user);

  // 只有子组件可以访问 name、email、avatar
  return (
    <div>
      <h2>{data.name}</h2>
      <p>{data.email}</p>
      <img src={data.avatar} alt={data.name} />
    </div>
  );
}

5. 带有连接的片段

// PostsList.jsx
const PostsListFragment = graphql`
  fragment PostsList_query on Query
  @argumentDefinitions(
    first: { type: "Int", defaultValue: 10 }
    after: { type: "String" }
  )
  @refetchable(queryName: "PostsListRefetchQuery") {
    posts(first: $first, after: $after)
    @connection(key: "PostsList_posts") {
      edges {
        node {
          id
          ...PostCard_post
        }
      }
    }
  }
`;

function PostsList({ query }) {
  const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment(
    PostsListFragment,
    query
  );

  return (
    <div>
      {data.posts.edges.map(({ node }) => (
        <PostCard key={node.id} post={node} />
      ))}

      {hasNext && (
        <button
          onClick={() => loadNext(10)}
          disabled={isLoadingNext}
        >
          {isLoadingNext ? '加载中...' : '加载更多'}
        </button>
      )}
    </div>
  );
}

6. 可重新获取的片段

// UserProfile.jsx
const UserProfileFragment = graphql`
  fragment UserProfile_user on User
  @refetchable(queryName: "UserProfileRefetchQuery") {
    id
    name
    bio
    posts(first: 10) {
      edges {
        node {
          id
          title
        }
      }
    }
  }
`;

function UserProfile({ user }) {
  const [data, refetch] = useRefetchableFragment(
    UserProfileFragment,
    user
  );

  const handleRefresh = () => {
    refetch({}, { fetchPolicy: 'network-only' });
  };

  return (
    <div>
      <button onClick={handleRefresh}>刷新</button>
      <h1>{data.name}</h1>
      <p>{data.bio}</p>
      <PostsList posts={data.posts.edges} />
    </div>
  );
}

7. 复数片段

// PostsList.jsx
const PostsListFragment = graphql`
  fragment PostsList_posts on Post @relay(plural: true) {
    id
    title
    excerpt
  }
`;

function PostsList({ posts }) {
  const data = useFragment(PostsListFragment, posts);

  return (
    <div>
      {data.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

// 使用
const query = graphql`
  query PostsQuery {
    posts {
      ...PostsList_posts
    }
  }
`;

8. 条件片段

// Content.jsx
const ContentFragment = graphql`
  fragment Content_content on Content {
    __typename
    ... on Post {
      title
      body
      author {
        name
      }
    }
    ... on Video {
      title
      duration
      thumbnailUrl
      creator {
        name
      }
    }
    ... on Image {
      title
      imageUrl
      photographer {
        name
      }
    }
  }
`;

function Content({ content }) {
  const data = useFragment(ContentFragment, content);

  switch (data.__typename) {
    case 'Post':
      return (
        <article>
          <h2>{data.title}</h2>
          <p>{data.body}</p>
          <span>作者:{data.author.name}</span>
        </article>
      );

    case 'Video':
      return (
        <div>
          <video src={data.thumbnailUrl} />
          <h2>{data.title}</h2>
          <span>时长:{data.duration}秒</span>
          <span>创作者:{data.creator.name}</span>
        </div>
      );

    case 'Image':
      return (
        <figure>
          <img src={data.imageUrl} alt={data.title} />
          <figcaption>
            {data.title} 摄影师:{data.photographer.name}
          </figcaption>
        </figure>
      );

    default:
      return null;
  }
}

9. 片段容器传统模式

// 传统容器模式(v1-11)
import { createFragmentContainer, graphql } from 'react-relay';

class PostCard extends React.Component {
  render() {
    const { post } = this.props;
    return (
      <article>
        <h2>{post.title}</h2>
        <p>{post.excerpt}</p>
      </article>
    );
  }
}

export default createFragmentContainer(PostCard, {
  post: graphql`
    fragment PostCard_post on Post {
      id
      title
      excerpt
    }
  `
});

// 现代钩子模式(v12+)
function PostCard({ post }) {
  const data = useFragment(
    graphql`
      fragment PostCard_post on Post {
        id
        title
        excerpt
      }
    `,
    post
  );

  return (
    <article>
      <h2>{data.title}</h2>
      <p>{data.excerpt}</p>
    </article>
  );
}

10. 高级片段模式

// 递归片段
const CommentFragment = graphql`
  fragment Comment_comment on Comment {
    id
    body
    author {
      name
    }
    replies(first: 5) {
      edges {
        node {
          ...Comment_comment
        }
      }
    }
  }
`;

function Comment({ comment, depth = 0 }) {
  const data = useFragment(CommentFragment, comment);

  if (depth > 3) return null; // 防止无限递归

  return (
    <div style={{ marginLeft: depth * 20 }}>
      <p>{data.body}</p>
      <span>{data.author.name}</span>

      {data.replies?.edges.map(({ node }) => (
        <Comment key={node.id} comment={node} depth={depth + 1} />
      ))}
    </div>
  );
}

// 带内联数据的片段
const PostWithInlineFragment = graphql`
  fragment PostWithInline_post on Post {
    id
    title
    author {
      ... on User {
        id
        name
        isVerified
      }
      ... on Organization {
        id
        name
        type
      }
    }
  }
`;

// 带有指令的片段
const ConditionalFragment = graphql`
  fragment Conditional_post on Post
  @argumentDefinitions(
    includeLikes: { type: "Boolean!", defaultValue: false }
    includeComments: { type: "Boolean!", defaultValue: true }
  ) {
    id
    title
    likesCount @include(if: $includeLikes)
    comments(first: 10) @include(if: $includeComments) {
      edges {
        node {
          id
          body
        }
      }
    }
  }
`;

// 带必需字段的片段
const RequiredFieldsFragment = graphql`
  fragment RequiredFields_user on User {
    id
    name @required(action: LOG)
    email @required(action: THROW)
    avatar @required(action: NONE)
  }
`;

最佳实践

  1. 将片段与组件共位 - 保持数据需求在一起
  2. 使用片段组合 - 从简单片段构建复杂查询
  3. 利用数据掩码 - 防止组件间紧耦合
  4. 定义最小片段 - 仅请求必要字段
  5. 使用参数提高灵活性 - 使片段可重用
  6. 遵循命名约定 - 组件名称_属性名称模式
  7. 避免循环依赖 - 仔细设计片段层次结构
  8. 使用可重新获取的片段 - 启用组件级重新获取
  9. 处理加载状态 - 在数据获取期间提供反馈
  10. 正确类型化片段 - 通过TypeScript确保类型安全

常见陷阱

  1. 片段过度获取 - 请求不必要字段
  2. 缺少数据掩码 - 访问未声明的字段
  3. 循环片段引用 - 创建依赖循环
  4. 不当片段组合 - 未展开子片段
  5. 硬编码值 - 不使用片段参数
  6. 破坏数据契约 - 随意更改片段字段
  7. 缺少片段键 - 在列表中未提供唯一键
  8. 忽略类型条件 - 未正确处理联合类型
  9. 过度嵌套 - 创建过深的片段层次结构
  10. 错误处理不足 - 未优雅处理缺失数据

使用场景

  • 使用GraphQL构建React应用程序
  • 实现组件驱动架构
  • 创建可重用UI组件
  • 开发数据密集型应用程序
  • 构建社交媒体平台
  • 创建电子商务应用程序
  • 实现协作工具
  • 开发内容管理系统
  • 构建管理仪表板
  • 使用React Native创建移动应用程序

资源