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 的迁移要点,并能结合源码判断Card在CardView中的实际渲染行为。
一、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改为按布局命名——这一点与仓库当前的实现是吻合的,下文会看到实际的布局命名正是GalleryLayout、GridLayout、WaterfallLayout。- 卡片是一个“可组合”的组件族:
Card本身只描述卡片主体的选择行为与操作区(quickActions、actionMenu),而封面图、预览区、正文、页脚被拆分为CardCoverPhoto、CardPreview、CardBody、CardFooter四个子接口,各司其职。 - 选择状态是受控的:
allowsSelection/isSelected/onSelectionChange三件套表明卡片可选性是显式声明的,选中态由外部状态驱动,符合 React 受控组件的惯例。
各子接口的职责可以整理为:
| 子接口 | 必填字段 | 可选字段 | 用途 |
|---|---|---|---|
Card | - | variant、size、allowsSelection、isSelected、onSelectionChange、quickActions、actionMenu | 卡片主体,承载选择行为与操作区 |
CardCoverPhoto | src | children | 卡片封面图,src为图片地址 |
CardPreview | children | - | 卡片预览区域(任意内容) |
CardBody | - | title、subtitle、description | 标题/副标题/描述,均为ReactNode |
CardFooter | children | - | 卡片页脚区域(任意内容) |
值得注意的是size?: 'L' | 'S'与 Spectrum 设计系统一贯的尺寸分级(large/medium/small 等)呼应,规范在此为卡片保留了大/小两档尺寸。
二、v2 → v3 破坏性变更清单
规范文档的下半部分是两个迁移对照表,是 v2 用户升级到 v3 时必须核对的内容,这里完整继承:
Card Changes
| v2 | v3 | Notes |
|---|---|---|
variant="standard" | - | standard is the default |
selected | isSelected |
variant="standard"被移除:v3 中 standard 成为默认值,不再需要显式声明,代码可以直接删除该属性。selected改名为isSelected:v3 统一使用is前缀命名布尔状态(如isSelected、isDisabled、isLoading),命名风格与仓库中其他组件保持一致。
CardBody Changes
| v2 | v3 | Notes |
|---|---|---|
title→string | title→ReactNode | |
subtitle→string | subtitle→ReactNode | |
description→string | description→ReactNode |
CardBody的title、subtitle、description三个字段从string放宽为ReactNode。这是一个兼容性放宽变更:v2 只能传纯字符串,v3 允许传入任意节点(如内嵌图标、格式化数字、可交互元素),迁移时旧代码无需修改,但新代码可以充分利用这一能力。
三、规范如何在仓库中落地:@react-spectrum/card 的导出结构
在 react-spectrum 仓库中,Card 相关实现分布在三层:
- 类型包
@react-types/card:入口 src/index.d.ts 仅做类型转发,将AriaCardViewProps、SpectrumCardProps、SpectrumCardViewProps从@react-spectrum/card再导出,说明最终 Props 类型定义收敛在 Spectrum 层。 - 组件包
@react-spectrum/card:入口 src/index.ts 从@adobe/react-spectrum私有目录转发导出核心组件与布局类。 - 实现层
@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内部的InternalCard用CardBase渲染;只有独立使用(不在CardView中)时,Card才自己渲染CardBase。从源码结构看,这是一种“同一组件、两种宿主”的复用策略:独立卡片与列表内卡片共享CardBase的全部视觉与行为。 getCollectionNode静态方法:这是 react-spectrum 将组件接入集合(collection)体系的标准约定——以 Generator 形式产出一个type: 'item'的节点,携带rendered: children与textValue(供搜索/排序使用)。CardView正是通过该约定把每张卡片收集成可导航、可选择的数据行。
CardView:虚拟化 + 网格语义 + 状态机
packages/@adobe/react-spectrum/src/card/CardView.tsx 是规范中“卡片”概念在容器层的完整实现,其内部结构可以拆成五个环节:
布局实例化(对应 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,因此布局本身是可定制的扩展点。集合包装为单列网格(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。网格状态与键盘导航(CardView.tsx#L95-L116):
useGridState+useGrid提供方向键漫游、focusMode: 'cell';布局对象被同时设置为keyboardDelegate,即由布局决定键盘导航如何在视觉网格上移动(网格/瀑布流布局各有多列导航规则)。虚拟化渲染(CardView.tsx#L147-L173):容器是
Virtualizer,isVirtualized: true意味着只有可视区域内的卡片会真实挂载;persistedKeys持久化当前聚焦项,避免其因滚动被卸载后焦点丢失。渲染函数按类型分派:item→InternalCard,loader→LoadingState,placeholder→EmptyState。加载态与空态(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中的真实渲染体,有两处值得注意的联动逻辑:
- 布局决定视觉形态:当布局为
grid或gallery时强制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”,方向键导航统一交给useGrid→useSelectableCollection处理。此外当卡片被禁用或selectionMode="none"时,空格键按下会被preventDefault,防止 CardView 被误滚动。
五、迁移与实践要点
结合规范与源码,可以总结出面向开发者的实践结论:
- v2 → v3 迁移只做三件事:删掉
variant="standard";把selected重命名为isSelected;CardBody的title/subtitle/description现在可传ReactNode(旧代码不受影响)。 - 单独使用卡片时直接使用
Card(可选配allowsSelection/isSelected/onSelectionChange、quickActions、actionMenu等规范中定义的 Props);集合展示时改用CardView,并将Card作为其子项——从Card.tsx的 context 判重逻辑看,这是框架设计预期的标准用法。 - 布局选择:通过
layout传入 GalleryLayout /GridLayout/WaterfallLayout(或其实例),并可参考导出的*LayoutOptions类型做定制;cardOrientation属性控制卡片方向,且会在非 grid 布局下被内部归一为vertical。 - 大数据量场景:
CardView内建Virtualizer虚拟化与persistedKeys焦点保持,并支持loadingState(loading/loadingMore)、onLoadMore增量加载与renderEmptyState空态渲染,长列表无需自行实现窗口化。 - 样式来源:
CardView的样式类取自 spectrum-css-temp 的 card 变量(@adobe/spectrum-css-temp/components/card/vars.css),与 Spectrum 设计系统的 CSS 变量体系对齐;仓库中还有 CardView 的 Storybook 故事 与 chromatic 视觉快照 可供查看三种布局的实际效果。
六、小结
specs/api/Card.md 虽然篇幅不长,但它完整定义了 Card 组件族的接口边界(Card、CardCoverPhoto、CardPreview、CardBody、CardFooter)与 v2 → v3 的迁移规则(移除variant="standard"、selected→isSelected、CardBody 字段放宽为ReactNode)。仓库实现则展示了这套规范的落点:@react-spectrum/card导出Card、CardView与三种布局类;Card借助getCollectionNode与 context 判重在“独立卡片”与“集合卡片”两种角色间复用CardBase;CardView以“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),仅供参考