尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

React Spectrum v3 架构解析:基于 React Hooks 的三层组件抽象与 react-spectrum 的实现落地

React Spectrum v3 架构解析:基于 React Hooks 的三层组件抽象与 react-spectrum 的实现落地 React Spectrum v3 架构解析基于 React Hooks 的三层组件抽象与 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本文解读 React Spectrum v3 架构 RFCrfcs/2019-v3-architecture.md它是 react-spectrum 仓库从“单体组件库”演进为“状态 / 行为 / 主题三层可复用架构”的奠基性文档。读完后你将理解为什么仓库中同时存在react-stately/*、react-aria/*与adobe/react-spectrum三类包每个包各承担什么职责以及这套架构如何借助 React 16.8 引入的 Hooks把无障碍、键盘交互、国际化等能力从主题样式中剥离出来供任意平台与主题复用。背景动机为什么需要一次架构重构RFC 开篇给出了重构的核心动因这些判断至今仍是理解整个仓库结构的钥匙自建组件库的普遍困境。许多公司从零构建自己的设计系统组件库。Web 原语只提供了 div、HTML 标签、CSS 与 JavaScript而把无障碍a11y、国际化i18n、键盘交互等高级特性正确实现起来非常困难多数团队没有足够资源结果是无法访问的应用被推到生产环境加深了“Web 应用体验不如原生”的印象。渲染平台不再只有 Web。React Native、UXPTorq Native等平台让 React 可以渲染原生控件且未来还有 AR/VR 等更多渲染表面。理想状态是同一套 UI 组件至少是公开接口可跨平台复用底层实现按平台替换。需要一个“无样式基座”。RFC 提出如果组件库能从一个实现了功能、逻辑、交互、无障碍、国际化但不含任何样式的基座框架起步各公司就能用自己的方式做样式以极小成本得到功能完备的组件库应用也因为到处共享公开接口而具备更好的平台可移植性。因此 RFC 要求一套可扩展的架构把每个组件的行为与样式从核心实现中分离让设计系统开发者能够以最小成本灵活定制样式、交互与行为同时站在一个功能完备的基座之上。三层架构State Hook、Behavior Hook 与 Themed ComponentRFC 的核心设计是借助 React 16.8 的 Hooks把每个组件最多拆分为三个可独立复用的部分。并非所有组件都具备全部三层——简单组件可能没有状态有些组件只是其他组件的组合。State hook状态钩子跨平台共享的 React hook。接受组件的公共 props提供状态管理支持受控与非受控两种模式。它不渲染任何 UI只暴露可被多个平台相关 UI 实现共享的通用状态管理。Behavior hook行为钩子为特定平台如 Web 或 Native提供要传给子元素的 props 的 React hook。它实现事件处理、焦点管理、无障碍、国际化等并在需要时通过 state hook 更新组件状态可能还持有平台相关的 UI 状态例如用于加 class 的焦点状态。RFC 主张每个 ARIA widget 都应有对应的行为钩子。Themed component主题组件应用实际使用的组件负责提供实现某一特定主题如 Spectrum所需的 DOM 结构——正确的类名与元素。它消费行为钩子的 props 与状态钩子的 state。仓库中的真实落地以 ComboBox / Autocomplete 为例RFC 用 autocomplete 的 state hook 和 combo box 的 behavior hook 做示例。在当前仓库中这条演进脉络可以逐层对上1. State 层。RFC 示例中的useAutocomplete如今对应 useAutocompleteState。当前实现比 RFC 示例更聚焦通过 useControlledState 统一处理受控/非受控inputValue/defaultInputValue返回inputValue、setInputValue、focusedNodeId、setFocusedNodeId。RFC 强调的“接口需要文档化并遵循语义化版本因为 react-spectrum 之外的组件也可能使用它”在当前仓库中成立——AutocompleteProps、AutocompleteStateOptions、AutocompleteState均作为公开类型导出见 packages/react-stately/autocomplete/src/index.ts。2. Behavior 层。RFC 示例中的useComboBox如今对应 useComboBox。其函数签名useComboBox(props, state)与 RFC 描述完全一致接受组件 props 加上 state hook 返回的状态返回与主题无关的 props。RFC 示例返回wrapperProps / textfieldProps / buttonProps / menuProps / getMenuItemProps当前实现返回inputProps / buttonProps / labelProps / valueProps / listBoxProps / descriptionProps / errorMessageProps等并且内部组合了useMenuTrigger、useSelectableCollection、useKeyboard等更细粒度的行为原语键盘处理Enter/Tab 等、aria-activedescendant、焦点管理均在这一层完成。包级入口 packages/react-aria/combobox/src/index.ts 只做了对react-aria/useComboBox的再导出。3. Theme 层。RFC 示例中的ComboBox组件如今对应 ComboBox对外经 packages/react-spectrum/combobox/src/index.ts 导出。当前实现印证了 RFC 的设计预期——组件本身尽量保持“小而近无状态”状态来自useComboBoxStatereact-stately行为 props 来自useComboBoxreact-aria组件自身负责 Spectrum 主题的 DOM 结构、CSS 模块类名combobox.css、焦点环FocusRing、国际化文案intl/combobox/*.json以及移动端降级MobileComboBox。可以看到RFC 中“state hook → behavior hook → themed component”的数据流在真实代码里是精确兑现的主题组件同时调用状态与行为两个钩子把两者产出的值合并到 DOM 属性上。State Hook 示例RFC 原文RFC 给出的 autocomplete state hook 示意如下它接受使用方组件的 props返回状态、更新函数和通用动作且不渲染 UIimport { useState, useMemo } from react; export function useAutocomplete(props) { let [showMenu, setShowMenu] useState(false); let [value, setValue] useState(props.value || ); let [selectedIndex, setSelectedIndex] useState(null); let completions useMemo( () props.options.filter(option option.toLowerCase().startsWith(value.toLowerCase()) ), [props.options, value] ); return { showMenu: showMenu completions.length 0, setShowMenu, toggleMenu: () setShowMenu(!showMenu), value, setValue: value { if (value !showMenu) { setShowMenu(true); } setSelectedIndex(null); setValue(value); props.onChange(value); }, selectedIndex, setSelectedIndex, completions, selectItem: index { setValue(completions[index]); setShowMenu(false); props.onChange(completions[index]); } }; }Behavior Hook 示例RFC 原文行为钩子接受组件 props 与状态钩子的返回值返回给多个子元素使用的、与主题无关的 props实现键盘/鼠标交互与无障碍属性。RFC 中的 combobox 示例import { useRef } from react; import { useId } from ./utils; export function useComboBox(props, autocomplete) { let id useId(props.id); let listboxId useId(); let textfieldRef useRef(); let values { ...autocomplete, id, listboxId, textfieldRef }; return { wrapperProps: getWrapperProps(values), textfieldProps: getTextfieldProps(values), buttonProps: getButtonProps(values), menuProps: getMenuProps(values), getMenuItemProps: index getMenuItemProps(values, index) }; } function getWrapperProps({ listboxId, showMenu }) { return { role: combobox, aria-controls: showMenu ? listboxId : undefined, aria-owns: showMenu ? listboxId : undefined, aria-expanded: showMenu, aria-haspopup: true }; } function getTextfieldProps({ selectedIndex, setSelectedIndex, completions, value, setValue, selectItem, listboxId, showMenu, setShowMenu, textfieldRef }) { let onKeyDown e { switch (e.key) { case ArrowDown: setSelectedIndex( selectedIndex null ? 0 : (selectedIndex 1) % completions.length ); break; case ArrowUp: setSelectedIndex( selectedIndex null ? completions.length - 1 : (selectedIndex - 1 completions.length) % completions.length ); break; case Enter: selectItem(selectedIndex); break; case Escape: setShowMenu(false); break; } }; return { value, ref: textfieldRef, onChange: e setValue(e.target.value), aria-controls: showMenu ? listboxId : undefined, aria-autocomplete: list, aria-activedescendant: showMenu selectedIndex ! null ? listboxId -option- selectedIndex : undefined, role: textbox, autoComplete: off, onKeyDown: onKeyDown, onBlur: () setShowMenu(false), onFocus: () { if (value) { setShowMenu(true); } } }; } function getButtonProps({ toggleMenu, textfieldRef }) { return { tabIndex: -1, onMouseDown: e e.preventDefault(), onMouseUp: e e.preventDefault(), onClick: () { textfieldRef.current.focus(); toggleMenu(); } }; } function getMenuProps({ listboxId }) { return { id: listboxId, role: listbox }; } function getMenuItemProps( { listboxId, selectedIndex, setSelectedIndex, selectItem }, index ) { return { role: option, id: listboxId -option- index, tabIndex: selectedIndex index ? 0 : -1, aria-selected: selectedIndex index, onMouseEnter: () setSelectedIndex(index), onMouseDown: e e.preventDefault(), onClick: () selectItem(index) }; }值得对照的是RFC 示例中getTextfieldProps用aria-activedescendant指向listboxId -option- index这一 ARIA 模式在今天的 useComboBox 中依然被保留只是项 id 改由listData.set(state, {id: menuProps.id})集中登记后动态生成说明行为钩子输出的属性形态是这套架构最稳定的契约面。Themed Component 示例RFC 原文主题组件消费两个钩子的产物只负责主题所需的 DOM 结构。RFC 中的 ComboBox 示例import {useAutocomplete} from react-state/autocomplete; import {useComboBox} from react-aria/combo-box; import {Textfield} from react-spectrum/textfield; import {Button} from react-spectrum/button; import {AutocompleteMenu} from react-spectrum/autocomplete; function ComboBox(props) { let autocomplete useAutocomplete(props); let { wrapperProps, textfieldProps, buttonProps, menuProps } useComboBox(props, autocomplete); return ( div {...wrapperProps} classNamespectrum-InputGroup Textfield {...textfieldProps} classNamespectrum-InputGroup-field / Button {...buttonProps} variantfield / AutocompleteMenu {...menuProps} / /div ); }包划分与目录结构三层各自独立发布三层必须发布为独立 npm 包RFC 明确指出为了让组件的三部分都能被独立使用它们应发布为独立的 npm 包。这样其他主题组件的作者只需依赖自己用到的代码而不是把整个 Spectrum 特定的东西都拖进来react-spectrum 的包则依赖 state 与 behavior 包。RFC 提出的命名结构为react-state/combo-box—— state hookreact-aria/combo-box—— Web 平台的行为钩子实现react-spectrum/combo-box—— Spectrum 主题组件对比当前仓库可以发现RFC 的“开放问题”最终落定的方式与提议略有出入行为层命名照 RFC 采纳为react-aria/*仓库中存在 packages/react-aria/combobox、packages/react-aria/autocomplete 等包状态层没有沿用 RFC 中“仍待讨论”的react-state而是定名为react-stately/*如 packages/react-stately/autocomplete、packages/react-stately/combobox主题层除react-spectrum/*门面包外主库实现集中在 packages/adobe/react-spectrumreact-spectrum/*与react-aria/*、react-stately/*的门面包主要承担再导出与类型声明。目录结构从“两层一棵树”到当前的组织方式RFC 提议在 react-spectrum 仓库内使用两层文件夹树把每个组件的三部分归入同一目录方便检索packages └── combo-box ├── aria │ ├── package.json │ ├── src │ │ └── useComboBox.js │ └── test │ └── useComboBox.js ├── component │ ├── package.json │ ├── src │ │ └── ComboBox.js │ └── test │ └── ComboBox.js └── state ├── package.json ├── src │ └── useAutocomplete.js └── test └── useAutocomplete.js从源码结构看当前仓库最终没有采用这种“每组件一个目录、内嵌 aria/state/component”的布局而是按“技术层”组织packages/react-stately/、packages/react-aria/、packages/react-spectrum/、packages/react-types/各为扁平的按组件命名的包目录同时用packages/react-stately/、packages/react-aria/、packages/react-aria-components/等伞形包承载实际源码例如 combobox 的三层实现分别位于 react-stately/src/autocomplete、react-aria/src/combobox 与 adobe/react-spectrum/src/combobox。可以推断这一调整是权衡了包数量仓库中react-aria下有 50 余个包后的结果每组件三层各建一个目录的样板成本过高按层分域后检索同样直接。此外仓库还新增了 RFC 未设想的react-types/*层承载跨层共享的纯类型定义如SpectrumTextInputBase、AriaComboBoxProps等见 ComboBox.tsx 的 import 列表可以视为三层架构在 TypeScript 生态下的自然延伸。文档、代价与兼容性RFC 对重构的配套约束同样值得开发者留意它们解释了仓库中大量文档与 API 面存在的合理性文档要求虽然对应用消费的组件不必然产生 API 变更但这是内部构建方式的重大变化——消费者可以直接在自己的自定义组件中使用 state 或 behavior hookAPI 表面积大幅增加这些接口需要文档化同时也要有面向贡献者的架构文档说明组件应当如何结构化。这与仓库中packages/react-aria/*、packages/react-stately/*每个包都配有 README、且各行为钩子带完整 JSDoc如useComboBox的接口注释的现状一致。代价Drawbacks这是对组件构建方式的重大变更每个组件都需要大量重构耗时耗力架构复杂度显著上升贡献者容易困惑——以前出 bug 只需在组件或 spectrum-css 里找现在可能的排查点多得多新贡献者还可能不了解或不理解这套架构。向后兼容性分析该重构不一定要求组件 API 变更但实现细节变化很大可能出现非预期的破坏或行为变化且 Hooks 要求使用方升级到 React 16.8 及以上因此必须是主版本发布。替代方案Hooks 发布前曾考虑过 HOC 与 render props但它们样板代码多、组件嵌套深导致调试困难、难以组合与传递状态。Hooks 允许把共享状态与行为绑定到单一组件实例上而无需子类化或其他 hack优雅地解决了这些问题。常见问答与现状对照RFC 的 FAQ 给出了四个问题结合当前仓库可以这样理解为什么做这次重构一是让 UXP 这类平台复用跨平台代码、同时提供各自的平台特定渲染二是为开源发布铺路让 react-spectrum 的潜在用户无需采用 Spectrum 设计就能享受主题无关的行为、无障碍、国际化等沉淀三是把组件的各部分更好地抽象使其更小、更易推理。是否包含破坏性 API 变更该提案不必然要求现有组件 props 的 API 变更但因为需要 React 16.8将随主版本发布且该主版本中可能还有其他不相关的破坏性变更。应用如何升级升级到 React 16.8 与 React Spectrum 3.0。时间线这是重大重构需要大量时间与资源当时没有明确的发布时间表。从仓库现状看这一 RFC 提出的问题在 packages/react-spectrum、packages/react-aria、packages/react-stately 的并行包体系与 rfcs/2019-v3-architecture.md 所在的 RFC 目录同目录还有 v3 语义化元素、v3 测试、v3 主题 等姊妹 RFC中都已得到最终答复。小结react-spectrum v3 架构 RFC 的核心贡献是把“一个组件 一个文件”的隐式约定升级为**状态跨平台共享→ 行为平台相关、主题无关→ 主题组件Spectrum 相关**的显式分层并以独立 npm 包固化各层的复用边界。对使用者而言这意味着你可以只装react-aria/*与react-stately/*用任意主题构建无障碍组件也可以只装react-spectrum/*/adobe/react-spectrum直接使用成品对维护者而言每层的接口如useComboBox(props, state)的入参与返回 props 集合就是跨层契约任何一层都可以被单独测试与替换。理解这份 RFC就是理解整个仓库包结构与命名react-stately、react-aria、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),仅供参考
返回列表