react-spectrum 卡片组件 API 设计解析:从 v3 规范到 CardView 的真实实现
2026/9/14 18:13:33 网站建设 项目流程

react-spectrum 卡片组件 API 设计解析:从 v3 规范到 CardView 的真实实现

【免费下载链接】react-spectrumA collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum

本文以 react-spectrum 仓库中的组件 API 设计规范 specs/api/Card.md 为主体,完整解析 Card(卡片)组件在 v3 架构下的接口定义与 v2 → v3 的破坏性变更;并结合仓库中@react-spectrum/card@adobe/react-spectrum的源码,说明这些接口是如何落地为带虚拟化、键盘导航、加载态与空态的CardView容器组件的。读完本文,你可以理解 Card 各 Props 的设计意图、掌握 v2 到 v3 的迁移要点,并能结合源码判断CardCardView中的实际渲染行为。

一、v3 规范的出发点:Card 接口定义

specs/api/Card.md 是 react-spectrum 为 Card 组件制定的 v3 API 规范。规范给出的核心接口如下(原文完整继承):

interface Card { variant?: 'quiet' | 'gallery', // Change to layout names instead? size?: 'L' | 'S', allowsSelection?: boolean, isSelected?: boolean, onSelectionChange?: (isSelected: boolean) => void, quickActions?: ReactElement<QuickActions>, actionMenu?: ReactElement<ActionMenu> } interface CardCoverPhoto { src: string, children?: ReactNode } interface CardPreview { children: ReactNode } interface CardBody { title?: ReactNode, subtitle?: ReactNode, description?: ReactNode } interface CardFooter { children: ReactNode }

从接口设计可以看出 v3 时期的几个关键决策:

  • variant采用语义化取值'quiet' | 'gallery'用于区分卡片的视觉风格。规范中保留了注释Change to layout names instead?,说明当时团队正在讨论是否将variant改为按布局命名——这一点与仓库当前的实现是吻合的,下文会看到实际的布局命名正是GalleryLayoutGridLayoutWaterfallLayout
  • 卡片是一个“可组合”的组件族Card本身只描述卡片主体的选择行为与操作区(quickActionsactionMenu),而封面图、预览区、正文、页脚被拆分为CardCoverPhotoCardPreviewCardBodyCardFooter四个子接口,各司其职。
  • 选择状态是受控的allowsSelection/isSelected/onSelectionChange三件套表明卡片可选性是显式声明的,选中态由外部状态驱动,符合 React 受控组件的惯例。

各子接口的职责可以整理为:

子接口必填字段可选字段用途
Card-variantsizeallowsSelectionisSelectedonSelectionChangequickActionsactionMenu卡片主体,承载选择行为与操作区
CardCoverPhotosrcchildren卡片封面图,src为图片地址
CardPreviewchildren-卡片预览区域(任意内容)
CardBody-titlesubtitledescription标题/副标题/描述,均为ReactNode
CardFooterchildren-卡片页脚区域(任意内容)

值得注意的是size?: 'L' | 'S'与 Spectrum 设计系统一贯的尺寸分级(large/medium/small 等)呼应,规范在此为卡片保留了大/小两档尺寸。

二、v2 → v3 破坏性变更清单

规范文档的下半部分是两个迁移对照表,是 v2 用户升级到 v3 时必须核对的内容,这里完整继承:

Card Changes

v2v3Notes
variant="standard"-standard is the default
selectedisSelected
  • variant="standard"被移除:v3 中 standard 成为默认值,不再需要显式声明,代码可以直接删除该属性。
  • selected改名为isSelected:v3 统一使用is前缀命名布尔状态(如isSelectedisDisabledisLoading),命名风格与仓库中其他组件保持一致。

CardBody Changes

v2v3Notes
titlestringtitleReactNode
subtitlestringsubtitleReactNode
descriptionstringdescriptionReactNode

CardBodytitlesubtitledescription三个字段从string放宽为ReactNode。这是一个兼容性放宽变更:v2 只能传纯字符串,v3 允许传入任意节点(如内嵌图标、格式化数字、可交互元素),迁移时旧代码无需修改,但新代码可以充分利用这一能力。

三、规范如何在仓库中落地:@react-spectrum/card 的导出结构

在 react-spectrum 仓库中,Card 相关实现分布在三层:

  1. 类型包@react-types/card:入口 src/index.d.ts 仅做类型转发,将AriaCardViewPropsSpectrumCardPropsSpectrumCardViewProps@react-spectrum/card再导出,说明最终 Props 类型定义收敛在 Spectrum 层。
  2. 组件包@react-spectrum/card:入口 src/index.ts 从@adobe/react-spectrum私有目录转发导出核心组件与布局类。
  3. 实现层@adobe/react-spectrum:真实的组件代码位于 packages/@adobe/react-spectrum/src/card/ 目录。

@react-spectrum/card对外导出的内容(见 src/index.ts):

export {CardView} from '@adobe/react-spectrum/private/card/CardView'; export {GalleryLayout} from '@adobe/react-spectrum/private/card/GalleryLayout'; export {GridLayout} from '@adobe/react-spectrum/private/card/GridLayout'; export {WaterfallLayout} from '@adobe/react-spectrum/private/card/WaterfallLayout'; export {Card} from '@adobe/react-spectrum/private/card/Card'; export type {GalleryLayoutOptions} from '@adobe/react-spectrum/private/card/GalleryLayout'; export type {GridLayoutOptions} from '@adobe/react-spectrum/private/card/GridLayout'; export type {WaterfallLayoutOptions} from '@adobe/react-spectrum/private/card/WaterfallLayout'; export type { SpectrumCardProps, AriaCardViewProps, SpectrumCardViewProps } from '@adobe/react-spectrum/private/card/types';

这份导出表印证了规范中Change to layout names instead?的讨论结果:布局以独立的类命名并导出——GalleryLayout(画廊式)、GridLayout(网格式)、WaterfallLayout(瀑布流式),每个布局类还配有对应的*LayoutOptions配置类型。也就是说,v3 规范中variant的取值讨论最终演化成了“CardView通过layout属性挂载布局类”的形态:布局成为可注入的对象,而不是简单的字符串枚举。

四、源码级解析:Card 与 CardView 的协作机制

Card 在 CardView 内部“让位”给 Collection

packages/@adobe/react-spectrum/src/card/Card.tsx 的实现非常短,但包含两个关键设计:

let Card = forwardRef((props: SpectrumCardProps, ref: DOMRef<HTMLDivElement>) => { let context = useCardViewContext(); if (context !== null) { return null; } else { return <CardBase {...props} ref={ref} />; } }); // @ts-ignore Card.getCollectionNode = function* getCollectionNode<T>(props: any): Generator<PartialNode<T>> { let {children, textValue} = props; yield { type: 'item', props: props, rendered: children, 'aria-label': props['aria-label'], hasChildNodes: false, textValue }; };
  • 通过 Context 判重Card先查询useCardViewContext()。当它出现在CardView内部时(context 非空),Card渲染为null——因为此时它已经作为 collection item 被CardView消费,真正的 DOM 由CardView内部的InternalCardCardBase渲染;只有独立使用(不在CardView中)时,Card才自己渲染CardBase。从源码结构看,这是一种“同一组件、两种宿主”的复用策略:独立卡片与列表内卡片共享CardBase的全部视觉与行为。
  • getCollectionNode静态方法:这是 react-spectrum 将组件接入集合(collection)体系的标准约定——以 Generator 形式产出一个type: 'item'的节点,携带rendered: childrentextValue(供搜索/排序使用)。CardView正是通过该约定把每张卡片收集成可导航、可选择的数据行。

CardView:虚拟化 + 网格语义 + 状态机

packages/@adobe/react-spectrum/src/card/CardView.tsx 是规范中“卡片”概念在容器层的完整实现,其内部结构可以拆成五个环节:

  1. 布局实例化(对应 CardView.tsx#L61-L65):

    let cardViewLayout = useMemo( () => (typeof layout === 'function' ? new layout({collator, cardOrientation, scale}) : layout), [layout, collator, cardOrientation, scale] );

    layout既可以是布局类(构造函数),也可以是已实例化的布局对象;实例化时注入collator(用于排序)、cardOrientation与 Provider 的scale,因此布局本身是可定制的扩展点

  2. 集合包装为单列网格(CardView.tsx#L70-L93):CardView把 item collection 包进GridCollection,每张卡片是一行、行内含一个以cell-${item.key}命名的 cell。源码注释说明其动机:“Makes the Grid row use the keys the user provides to the cards so that selection change via interactions returns the card keys”——即用用户提供的 key 保证选中变更回调返回的就是卡片的 key。

  3. 网格状态与键盘导航(CardView.tsx#L95-L116):useGridState+useGrid提供方向键漫游、focusMode: 'cell';布局对象被同时设置为keyboardDelegate,即由布局决定键盘导航如何在视觉网格上移动(网格/瀑布流布局各有多列导航规则)。

  4. 虚拟化渲染(CardView.tsx#L147-L173):容器是VirtualizerisVirtualized: true意味着只有可视区域内的卡片会真实挂载;persistedKeys持久化当前聚焦项,避免其因滚动被卸载后焦点丢失。渲染函数按类型分派:itemInternalCardloaderLoadingStateplaceholderEmptyState

  5. 加载态与空态(CardView.tsx#L178-L203):loadingState'loading''loadingMore'时显示ProgressCircle,且 aria-label 会依据集合是否为空在loading/loadingMore两条本地化文案间切换(来自intl/card/*.json);空集合时若提供了renderEmptyState回调,则在其产出的内容外包一层居中的role="row"/role="gridcell"语义。

InternalCard:行/格语义与布局联动

InternalCard(CardView.tsx#L217-L283)是每张卡片在CardView中的真实渲染体,有两处值得注意的联动逻辑:

  • 布局决定视觉形态:当布局为gridgallery时强制isQuiet = true;当布局不是grid时强制cardOrientation = 'vertical'。即网格/画廊布局下卡片呈现为安静风格,瀑布流等布局下卡片始终纵向排列——这正是规范中variant取值与布局命名融合的体现。
  • 焦点与选择隔离useGridRow生成行属性、useGridCell生成格属性,并通过delete gridCellProps.onKeyDownCapture移除格级按键处理——源码注释解释了原因:“We don't want to focus the checkbox (or any other focusable elements) within the Card when pressing the arrow keys”,方向键导航统一交给useGriduseSelectableCollection处理。此外当卡片被禁用或selectionMode="none"时,空格键按下会被preventDefault,防止 CardView 被误滚动。

五、迁移与实践要点

结合规范与源码,可以总结出面向开发者的实践结论:

  1. v2 → v3 迁移只做三件事:删掉variant="standard";把selected重命名为isSelectedCardBodytitle/subtitle/description现在可传ReactNode(旧代码不受影响)。
  2. 单独使用卡片时直接使用Card(可选配allowsSelection/isSelected/onSelectionChangequickActionsactionMenu等规范中定义的 Props);集合展示时改用CardView,并将Card作为其子项——从Card.tsx的 context 判重逻辑看,这是框架设计预期的标准用法。
  3. 布局选择:通过layout传入 GalleryLayout /GridLayout/WaterfallLayout(或其实例),并可参考导出的*LayoutOptions类型做定制;cardOrientation属性控制卡片方向,且会在非 grid 布局下被内部归一为vertical
  4. 大数据量场景CardView内建Virtualizer虚拟化与persistedKeys焦点保持,并支持loadingStateloading/loadingMore)、onLoadMore增量加载与renderEmptyState空态渲染,长列表无需自行实现窗口化。
  5. 样式来源CardView的样式类取自 spectrum-css-temp 的 card 变量(@adobe/spectrum-css-temp/components/card/vars.css),与 Spectrum 设计系统的 CSS 变量体系对齐;仓库中还有 CardView 的 Storybook 故事 与 chromatic 视觉快照 可供查看三种布局的实际效果。

六、小结

specs/api/Card.md 虽然篇幅不长,但它完整定义了 Card 组件族的接口边界(CardCardCoverPhotoCardPreviewCardBodyCardFooter)与 v2 → v3 的迁移规则(移除variant="standard"selectedisSelected、CardBody 字段放宽为ReactNode)。仓库实现则展示了这套规范的落点:@react-spectrum/card导出CardCardView与三种布局类;Card借助getCollectionNode与 context 判重在“独立卡片”与“集合卡片”两种角色间复用CardBaseCardView以“GridCollection + useGrid + Virtualizer”的组合提供了键盘导航、虚拟化、加载/空态与布局注入的完整能力。理解这条“规范 → 导出 → 实现”的链路,就能在 react-spectrum 中正确地设计并迁移卡片类界面。

【免费下载链接】react-spectrumA collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询