首页
开发技巧正文内容

在React中使用 Headless 组件解耦UI和逻辑

2023年11月14日
阅读时长 2 分钟
阅读量 116
在React中使用 Headless 组件解耦UI和逻辑

在前端开发领域,术语和范例有时可能令人费解,“Headless UI”或“Headless 组件”很可能属于这个范畴。如果你对这些术语感到困惑,你并不孤单。实际上,尽管这些概念的名称令人困惑,但它们是一种有吸引力的策略,可以极大地简化复杂用户界面的管理。

2023年11月10日更新:我在Martin Fowler的博客上发表了一篇长文(https://martinfowler.com/articles/headless-component.html)。如果你喜欢更详细的版本,那篇文章应该很合适。

无头组件可能看起来很奇特,但它们的真正威力在于它们的灵活性、可重用性和改善代码库组织和清晰度的能力。在本文中,我们将揭示这种模式的奥秘,阐明它到底是什么,为什么它有益,以及它如何彻底改变你对界面设计的方法。

为了说明这一点,我们将从一个简单而有效的无头组件的应用开始:从两个相似的组件中提取一个“useToggle”钩子来减少代码重复。虽然这个例子可能看起来微不足道,但它为理解无头组件的核心原则铺平了道路。通过识别常见模式并将它们提取为可重用的部分,我们可以简化我们的代码库,并为更高效的开发过程铺平道路。

但这只是冰山一角!随着我们深入探讨,我们将遇到一个更复杂的实例:利用Downshift,一个用于创建增强型输入组件的强大库。

通过本文的阅读,我希望不仅能让你理解无头组件,还能让你有信心将这种强大的模式整合到你自己的项目中。所以,让我们摒弃困惑,拥抱无头组件的变革潜力。

切换组件

切换是许多应用程序的重要组成部分。它们是“在此设备上记住我”、“激活通知”或非常流行的“暗黑模式”等功能背后的默默工作者。

一个ToggleButton组件

在React中创建这样一个切换组件的过程非常简单。让我们深入了解如何构建一个。

const ToggleButton = () => {
  const [isToggled, setIsToggled] = useState(false);

  const toggle = useCallback(() => {
    setIsToggled((prevState) => !prevState);
  }, []);

  return (
    <div className="toggleContainer">
      <p>请勿打扰</p>
      <button onClick={toggle} className={isToggled ? "on" : "off"}>
        {isToggled ? "开" : "关"}
      </button>
    </div>
  );
};

useState钩子设置了一个名为isToggled的状态变量,初始值为false。使用useCallback创建的toggle函数在每次调用时(点击按钮时)在isToggled值之间切换truefalse。按钮的外观和文本("开"或"关")动态反映了isToggled状态。

现在假设我们需要构建另一个完全不同的组件ExpandableSection,它将显示或隐藏一个部分的详细信息。标题旁边有一个按钮,您可以点击展开或折叠详细信息。

一个ExpandableSection组件

实现也不太困难,你可以轻松地这样做:

const ExpandableSection = ({ title, children }: ExpandableSectionType) => {
  const [isOpen, setIsOpen] = useState(false);

  const toggleOpen = useCallback(() => {
    setIsOpen((prevState) => !prevState);
  }, []);

  return (
    <div>
      <h2 onClick={toggleOpen}>{title}</h2>
      {isOpen && <div>{children}</div>}
    </div>
  );
};

这里有一个明显的相似之处 - ToggleButton中的“开”和“关”状态与ExpandableSection中的“展开”和“折叠”操作相似。认识到这种共同点,我们可以将这个共享功能抽象成一个单独的函数。在React生态系统中,我们通过创建自定义钩子来实现这一点。

const useToggle = (init = false) => {
  const [state, setState] = useState(init);
  
  const toggle = useCallback(() => {
    setState((prevState) => !prevState);
  }, []);

  return [state, toggle];
};

这个重构可能看起来相当简单,但它突出了一个重要的概念:将行为与呈现分离。在这种情况下,我们的自定义钩子作为一个独立于JSX的状态机。ToggleButtonExpandableSection都利用了这个相同的基础逻辑。

任何在中等规模的前端项目上花费了相当时间的人都会意识到,大多数更新或错误与UI视觉无关,而是与管理UI的状态相关 - 本质上是与逻辑相关。钩子为集中处理这些逻辑方面提供了强大的工具,使其更容易进行审查、优化和维护。

Headless 组件

实际上,已经有许多优秀的库使用这种模式来分离行为(或状态管理)和呈现。在这些组件库中,最著名的应该是Downshift

Downshift应用了无头组件的概念,这些组件管理行为和状态,而不渲染任何UI。它们在其渲染属性函数中提供了一个状态和一组操作,允许你将其连接到你的UI上。这样,Downshift允许你控制你的UI,同时它负责复杂的状态和可访问性管理。

例如,我想构建一个下拉列表,显然我需要列表数据、一个触发器和一些关于如何突出显示选定项、应该渲染多少行的自定义设置。但我不想从头开始构建可访问性,因为有许多边缘情况需要考虑,包括跨浏览器和跨设备的适应性。

一个StateSelect组件

所以只需几行JSX,我就可以轻松地使用Downshift创建一个完全可访问的选择器:

const StateSelect = () => {
  const {
    isOpen,
    selectedItem,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    highlightedIndex,
    getItemProps,
  } = useSelect({items: states});

  return (
    <div>
      <label {...getLabelProps()}>发行状态:</label>
      <div {...getToggleButtonProps()} className="trigger" >
        {selectedItem ?? '选择一个状态'}
      </div>
      <ul {...getMenuProps()} className="menu">
        {isOpen &&
          states.map((item, index) => (
            <li
              style={
                highlightedIndex === index ? {backgroundColor: '#bde4ff'} : {}
              }
              key={`${item}${index}`}
              {...getItemProps({item, index})}
            >
              {item}
            </li>
          ))}
      </ul>
    </div>
  )
}

这个组件是一个使用Downshift的useSelect钩子的状态选择器。它允许用户从下拉菜单中选择一个状态。

  • useSelect管理选择输入的状态和交互。

  • isOpenselectedItemhighlightedIndex是由useSelect控制的状态变量。

  • getToggleButtonPropsgetLabelPropsgetMenuPropsgetItemProps是用于提供相应元素所需属性的函数。

  • isOpen确定下拉菜单是否打开。

  • selectedItem保存当前选定状态的值。

  • highlightedIndex指示当前突出显示的列表项。

  • 如果下拉菜单打开,states.map会生成一个可选择的状态的无序列表。

  • 使用扩展运算符(...)将属性从Downshift的钩子传递给组件。这包括点击处理程序、键盘导航和ARIA属性等。

  • 如果选择了一个状态,它将显示为按钮内容。否则,它显示为“选择一个状态”。

这种方法使你完全控制渲染,因此你可以根据应用程序的外观和感觉来设计你的组件,并在必要时应用自定义行为。它也非常适合在不同的组件或项目之间共享行为逻辑。

还有一些遵循这种模式的无头组件库:

  • Reakit:它提供了一组用于构建可访问的高级UI库、工具包、设计系统等的无头组件。

  • React Table:它是一个可组合的无头实用工具,基于Hooks,可以构建各种表格。

  • react-use:它是一个包含多个无头组件的Hooks集合。

深入一点

当我们以有意识的方式继续将逻辑与UI分离时,我们逐渐创建了一个分层结构。这个结构不是跨整个应用程序的传统分层架构,而是特定于应用程序的UI部分。

无头UI模式在这种架构中,JSX(或大多数标签)在最高层定义,它专门负责显示传入的属性。紧接着,我们有一个所谓的“无头组件”。这个组件维护所有的行为,管理状态,并为JSX提供交互的接口。在这个结构的底部,我们有封装领域特定逻辑的数据模型。这些模型不关心UI或状态,而是专注于数据管理和业务逻辑。这种分层的方法提供了一种清晰分离关注点的方式,增强了代码的清晰度和可维护性。

一个平衡的观点

与任何其他类型的技术一样,无头UI也有一些需要注意的优点和缺点。让我们先讨论无头UI的好处:

  1. 可重用性:无头组件的主要优势是它们的可重用性。通过将逻辑封装到独立的组件中,您可以在多个UI元素中重用这些组件。这不仅减少了代码重复,还促进了应用程序的一致性。

  2. 关注点分离:无头组件清晰地分离了逻辑和呈现。这使得您的代码库更易于管理和理解,特别是对于具有分工的大型团队而言。

  3. 灵活性:由于无头组件不指定呈现方式,它们允许更大的设计灵活性。您可以根据需要自定义UI,而不影响底层逻辑。

  4. 可测试性:由于逻辑与呈现分离,编写业务逻辑的单元测试更容易。

另一方面,与一个全功能组件相比,它通常会更复杂一些,因此需要考虑以下几点:

  1. 初始开销:对于较简单的应用程序或组件,创建无头组件可能会显得过度工程化,导致不必要的复杂性。

  2. 学习曲线:对于不熟悉这个概念的开发人员来说,理解起来可能会有一定的挑战,导致学习曲线较陡。

  3. 可能过度使用:很容易过度使用并尝试使每个组件都成为无头组件,即使在不必要的情况下,导致代码库过于复杂。

  4. 潜在的性能问题:虽然通常不是一个重大问题,但如果不小心处理使用共享逻辑重新渲染多个组件可能会导致性能问题。

请记住,无头UI并不是像任何架构模式一样适用于所有情况的解决方案。使用它的决定应该基于您项目的特定需求和复杂性。

进一步阅读

总结

在本文中,我们深入探讨了无头用户界面的世界,这是一种处理复杂UI任务的有力方法。我们探讨了将行为与渲染分离的方式如何帮助我们创建更易于维护和重用的代码,减少冗余和潜在的错误。我们首先通过一个简单的示例来说明这一点,创建了一个自定义的React钩子useToggle,并展示了它在两个独立的组件中的应用。然后,我们将这个概念扩展到一个更复杂的场景中,使用了一个名为Downshift的优秀库,该库方便构建增强型输入组件。通过更深入地理解“无头”方法,我们希望您能够在未来的项目中利用这种模式来创建更可扩展和可维护的UI。

免责声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

相关文章

探索多种软件架构模式及其实用应用
2024年11月22日19:06
本文深入探讨了多种软件架构模式,包括有界上下文、边车模式、发布-订阅模式、应用网关、微服务、命令职责分离(CQRS)等,介绍了它们的优点、使用场景以及具体应用实例。文章强调根据具体项目需求和团队能力选择最合适的架构,以构建高效和可维护的解决方案,同时展示了各架构模式间的综合应用,提供了丰富的案例和技术细节。
15个高级Python快捷键助您更快编程
2024年11月21日07:02
本文分享了 15 个高级的 Python 编程快捷键,包括上下文管理器、行内字典合并、函数参数解包、链式比较、dataclasses、海象运算符、反转列表、备忘录缓存、splitlines、enumerate、字典推导、zip 用于并行迭代、itertools.chain 扁平化列表、functools.partial 部分函数和 os.path 文件路径管理等,帮助开发者提高编程效率和代码简洁性。
揭示网页开发的 11 个迷思:停止相信这些误区
2024年11月19日22:05
网页开发充满误解,这篇博文针对11个常见迷思进行揭秘。包括网站开发后不需更新、需掌握所有技术、AI会取代开发者等。强调持续学习、专业化、用户体验的重要性,澄清误区如多任务处理的必要性和最新技术的必需性。文章提醒开发者注重实用而非追求完美代码,以务实态度面对开发工作。
你知道 CSS 的四种 Focus 样式吗?
2024年11月18日21:41
本文介绍了四种 CSS focus 样式::focus、:focus-visible、:focus-within 以及自定义的 :focus-visible-within,帮助提升网站用户体验。:focus 样式应用于被选中元素;:focus-visible 仅在键盘导航时显示;:focus-within 用于父元素;自定义 :focus-visible-within 结合两者效果。合理运用这些样式能使网站更方便键盘用户导航。
利用 Python 实现自动化图像裁剪:简单高效的工作流程
2024年11月11日20:49
使用 Python 和 OpenCV 自动裁剪图像,轻松实现 16:9 的完美构图。这个指南介绍了如何通过代码进行灰度化、模糊处理和边缘检测,最终识别出最重要的部分进行裁剪。特别适合需要批量处理图像的情况,节省大量时间。
每位资深前端开发人员都应了解的 TypeScript 高级概念
2024年11月11日02:07
资深前端开发者应了解 TypeScript 的高级概念,如联合类型、交叉类型、类型保护、条件类型、映射类型、模板字面量类型和递归类型。这些特性可提升代码的可维护性和可扩展性,确保在开发复杂应用时实现更高的类型安全性和效率。