组件组合

处理复杂性和将 Lit 代码分解成独立单元的最常见方法是 *组件组合*:即,将一个大型、复杂组件从更小、更简单的组件构建出来的过程。假设你被要求实现一个 UI 界面

Screenshot of an application that displays a set of animal photos. The application has a top bar with a title ("Fuzzy") and a menu button. A left menu drawer is open, showing a set of options.

你可能可以识别出需要一定复杂性来实现的区域。很有可能,这些区域可以是组件。

通过将复杂性隔离到特定的组件中,你使任务变得更加简单,然后你可以将这些组件组合起来以创建整体设计。

例如,上面的相当简单的屏幕截图包含了许多可能的组件:顶部栏、菜单按钮、带有菜单项的抽屉以导航当前部分;以及主内容区域。每个部分都可以用一个组件来表示。一个复杂的组件,比如带有导航菜单的抽屉,可能会分解成许多更小的组件:抽屉本身、一个用于打开和关闭抽屉的按钮、菜单、单个菜单项。

Lit 允许你通过将元素添加到你的模板中来进行组合——无论这些元素是内置的 HTML 元素还是自定义元素。

在决定如何分解功能时,有一些因素可以帮助确定何时创建新的组件。如果满足以下一项或多项,则 UI 的一部分可能适合作为组件

  • 它有自己的状态。
  • 它有自己的模板。
  • 它在多个地方使用,无论是在这个组件中还是在多个组件中。
  • 它专注于做好一件事。
  • 它具有明确定义的 API。

可重复使用的控件,如按钮、复选框和输入字段可以构成很好的组件。但是,更复杂的 UI 部分,如抽屉和轮播,也是组件化的理想选择。

在与子组件交换数据时,一般的规则是遵循 DOM 模型:*属性向下*,*事件向上*。

  • 属性向下。在子组件上设置属性通常比在子组件上调用方法更可取。在 Lit 模板和其他声明式模板系统中,设置属性很容易。

  • 事件向上。在 Web 平台中,触发事件是元素向上发送信息的默认方法,通常是对用户交互的响应。这使主机组件能够对事件做出响应,或者转换或重新触发事件以供树中的更上层祖先处理。

此模型的一些含义

  • 组件应该是其 Shadow DOM 中子组件的真相来源。子组件不应在其主机组件上设置属性或调用方法。

  • 如果组件更改了其自身的公共属性,则它应该触发一个事件以通知树中的更上层组件。通常,这些更改将是用户操作的结果——比如按下按钮或选择菜单项。想想原生的 input 元素,它会在用户更改输入值时触发事件。

考虑一个菜单组件,它包含一组菜单项并公开 itemsselectedItem 属性作为其公共 API 的一部分。它的 DOM 结构可能如下所示

A hierarchy of DOM nodes representing a menu. The top node, my-menu, has a ShadowRoot, which contains three my-item elements.

当用户选择一个项目时,my-menu 元素应该更新其 selectedItem 属性。它还应该触发一个事件以通知任何拥有组件,选择已更改。完整的序列类似于以下内容

  • 用户与一个项目交互,导致事件触发(要么是一个标准事件,如 click,要么是 my-item 组件特有的事件)。
  • my-menu 元素接收事件,并更新其 selectedItem 属性。它也可能会更改一些状态,以便选择项目突出显示。
  • my-menu 元素触发一个语义事件,指示选择已更改。例如,此事件可能被称为 selected-item-changed。由于此事件是 my-menu API 的一部分,因此它应该在该上下文中具有语义意义。

有关调度和监听事件的更多信息,请参阅 事件

属性向下,事件向上是一个很好的开始规则。但是,如果你需要在两个没有直接父子关系的组件之间交换数据怎么办?例如,在 Shadow 树中是兄弟的两个组件?

解决此问题的一种方法是使用 *中介模式*。在中介模式中,对等组件不会直接相互通信。相反,交互由第三方 *中介*。

实现中介模式的一种简单方法是让拥有组件处理来自其子组件的事件,并相应地更新其子组件的状态,通过将更改后的数据向下传递回树中。通过添加一个中介,你可以使用熟悉的事件向上、属性向下原则在树状结构中传递数据。

在以下示例中,中介元素监听其 Shadow DOM 中输入和按钮元素的事件。它控制按钮的启用状态,因此用户只有在输入框中有文本时才能点击 *提交*。

其他中介模式包括 flux/Redux 风格的模式,其中存储库通过订阅来中介更改和更新组件。让组件直接订阅更改可以帮助避免需要每个父组件传递其所有子组件所需的所有数据。

除了你 Shadow DOM 中的节点外,你还可以渲染由组件用户提供的子节点,就像标准的 <select> 元素可以接受一组 <option> 元素作为子节点并将它们渲染为菜单项一样。

子节点有时被称为“Light DOM”,以将其与组件的 Shadow DOM 区分开来。例如

这里,top-bar 元素有两个由用户提供的 Light DOM 子节点:一个导航按钮和一个标题。

与 Light DOM 子节点的交互方式不同于与 Shadow DOM 中节点的交互方式。组件 Shadow DOM 中的节点由组件管理,不应从组件外部访问。Light DOM 子节点由组件外部管理,但也可以由组件访问。组件的用户可以随时添加或删除 Light DOM 子节点,因此组件无法假设一组静态的子节点。

组件可以使用其 Shadow DOM 中的 <slot> 元素来控制是否以及在何处渲染子节点。当添加和删除子节点时,它还可以通过监听 slotchange 事件来接收通知。

有关更多信息,请参阅有关 使用插槽渲染子节点访问插槽子节点 的部分。

Meerkat 照片由 Anggit RizkiantoUnsplash 上提供。