🎁现代 JavaScript —— 杂项(Web Components)
00 分钟
2022-9-15
2022-9-15
type
status
date
slug
summary
tags
category
icon
password
Edited
Sep 15, 2022 12:41 PM
Created
Sep 14, 2022 08:15 AM

Custom elements

Custom elements 有两种:
  1. Autonomous custom elements (自主自定义标签) —— “全新的” 元素, 继承自 HTMLElement 抽象类.
  1. Customized built-in elements (自定义内建元素) —— 继承内建的 HTML 元素,比如自定义 HTMLButtonElement 等。
在创建 custom elements 的时候,我们需要告诉浏览器一些细节,包括:如何展示它,以及在添加元素到页面和将其从页面移除的时候需要做什么,等等。

Autonomous custom elements

现在当任何带有 <my-element> 标签的元素被创建的时候,一个 MyElement 的实例也会被创建,并且前面提到的方法也会被调用。我们同样可以使用 document.createElement('my-element') 在 JavaScript 里创建元素。
ℹ️
Custom element 名称必须包括一个短横线 -

Customized built-in elements

我们创建的 <time-formatted> 这些新元素,并没有任何相关的语义。搜索引擎并不知晓它们的存在,同时无障碍设备也无法处理它们。
我们可以通过继承内建元素的类来扩展和定制它们。
 

Shadow DOM

Shadow DOM 为封装而生。它可以让一个组件拥有自己的「影子」DOM 树,这个 DOM 树不能在主文档中被任意访问,可能拥有局部样式规则,还有其他特性。

Shadow tree

一个 DOM 元素可以有以下两类 DOM 子树:
  1. Light tree(光明树) —— 一个常规 DOM 子树,由 HTML 子元素组成。我们在之前章节看到的所有子树都是「光明的」。
  1. Shadow tree(影子树) —— 一个隐藏的 DOM 子树,不在 HTML 中反映,无法被察觉。
如果一个元素同时有以上两种子树,那么浏览器只渲染 shadow tree。但是我们同样可以设置两种树的组合。
调用 elem.attachShadow({mode: …}) 可以创建一个 shadow tree。
有两个限制:
  1. 在每个元素中,我们只能创建一个 shadow root。
  1. elem 必须是自定义元素,或者是以下元素的其中一个:「article」、「aside」、「blockquote」、「body」、「div」、「footer」、「h1…h6」、「header」、「main」、「nav」、「p」、「section」或者「span」。其他元素,比如 <img>,不能容纳 shadow tree。
mode 选项可以设定封装层级。他必须是以下两个值之一:
  • 「open」 —— shadow root 可以通过 elem.shadowRoot 访问。
    • 任何代码都可以访问 elem 的 shadow tree。
  • 「closed」 —— elem.shadowRoot 永远是 null
    • 我们只能通过 attachShadow 返回的指针来访问 shadow DOM(并且可能隐藏在一个 class 中)。
attachShadow 返回的 shadow root,和任何元素一样:我们可以使用 innerHTML 或者 DOM 方法,比如 append 来扩展它。
称有 shadow root 的元素叫做「shadow tree host」,可以通过 shadow root 的 host 属性访问

封装

Shadow DOM 被非常明显地和主文档分开:
  1. Shadow DOM 元素对于 light DOM 中的 querySelector 不可见。实际上,Shadow DOM 中的元素可能与 light DOM 中某些元素的 id 冲突。这些元素必须在 shadow tree 中独一无二。
  1. Shadow DOM 有自己的样式。外部样式规则在 shadow DOM 中不产生作用。
 

模板元素

内建的 <template> 元素用来存储 HTML 模板。浏览器将忽略它的内容,仅检查语法的有效性,但是我们可以在 JavaScript 中访问和使用它来创建其他元素。
浏览器认为 <template> 的内容“不在文档中”:样式不会被应用,脚本也不会被执行,等。
当我们将内容插入文档时,该内容将变为活动状态(应用样式,运行脚本等)。
模板的 content 属性可看作DocumentFragment —— 一种特殊的 DOM 节点。
我们可以将其视为普通的DOM节点,除了它有一个特殊属性:将其插入某个位置时,会被插入的则是其子节点。
 

Shadow DOM 插槽,组成

Shadow DOM 支持 <slot> 元素,由 light DOM 中的内容自动填充。

具名插槽

在这里 <user-card> shadow DOM 提供两个插槽, 从 light DOM 填充:
在 shadow DOM 中,<slot name="X"> 定义了一个“插入点”,一个带有 slot="X" 的元素被渲染的地方。
⚠️
仅顶层子元素可以设置 slot="…" 特性(Light DOM)

插槽处理过程

编译后,不考虑组合的 DOM 结构:
我们创建了 shadow DOM,所以它当然就存在了,位于 #shadow-root 之下。现在元素同时拥有 light DOM 和 shadow DOM。
为了渲染 shadow DOM 中的每一个 <slot name="..."> 元素,浏览器在 light DOM 中寻找相同名字的 slot="..."。这些元素在插槽内被渲染:
notion image
结果被叫做 扁平化(flattened)DOM
但是 “flattened” DOM 仅仅被创建用来渲染和事件处理,是“虚拟”的。虽然是渲染出来了,但文档中的节点事实上并没有到处移动!
querySelectorAll 还是能找到它们
因此,扁平化 DOM 是通过插入插槽从 shadow DOM 派生出来的。浏览器渲染它并且用于样式继承、事件传播。但是 JavaScript 在扁平前仍按原样看到文档。

默认插槽:第一个不具名的插槽

shadow DOM 中第一个没有名字的 <slot> 是一个默认插槽。它从 light DOM 中获取没有放置在其他位置的所有节点。
把默认插槽添加到 <user-card>,该位置可以收集有关用户的所有未开槽(unslotted)的信息
所有未被插入的 light DOM 内容进入 “其他信息” 字段集。
 
如果 添加/删除 了插槽元素,浏览器将监视插槽并更新渲染。
如果组件想知道插槽的更改,那么可以用 slotchange 事件。
如果修改了 slot="title" 的内容,则不会发生 slotchange 事件。

插槽 API

  • node.assignedSlot – 返回 node 分配给的 <slot> 元素。
  • slot.assignedNodes({flatten: true/false}) – 分配给插槽的 DOM 节点。默认情况下,flatten 选项为 false。如果显式地设置为 true,则它将更深入地查看扁平化 DOM ,如果嵌套了组件,则返回嵌套的插槽,如果未分配节点,则返回备用内容。
  • slot.assignedElements({flatten: true/false}) – 分配给插槽的 DOM 元素(与上面相同,但仅元素节点)。
 

给 Shadow DOM 添加样式

shadow DOM 可以包含 <style> 和 <link rel="stylesheet" href="…"> 标签。在后一种情况下,样式表是 HTTP 缓存的,因此不会为使用同一模板的多个组件重新下载样式表。
一般来说,局部样式只在 shadow 树内起作用,文档样式在 shadow 树外起作用。但也有少数例外。

:host

:host 选择器允许选择 shadow 宿主(包含 shadow 树的元素)。

级联

shadow 宿主( <custom-dialog> 本身)驻留在 light DOM 中,因此它受到文档 CSS 规则的影响。
如果在局部的 :host 和文档中都给一个属性设置样式,那么文档样式优先。
这是非常有利的,因为我们可以在其 :host 规则中设置 “默认” 组件样式,然后在文档中轻松地覆盖它们。

:host(selector)

与 :host 相同,但仅在 shadow 宿主与 selector 匹配时才应用样式。
:host([centered]) —— 仅当 元素 具有 centered 属性时才将其居中

:host-context(selector)

与 :host 相同,但仅当外部文档中的 shadow 宿主或它的任何祖先节点与 selector 匹配时才应用样式。

给占槽( slotted )内容添加样式

占槽元素来自 light DOM,所以它们使用文档样式。局部样式不会影响占槽内容。
而想要在组件中设置占槽元素的样式,有两种选择:
  • 对 <slot> 本身进行样式化,并借助 CSS 继承
    • 使用 ::slotted(selector) 伪类。它根据两个条件来匹配元素
        1. 这是一个占槽元素,来自于 light DOM。插槽名并不重要,任何占槽元素都可以,但只能是元素本身,而不是它的子元素 。
        1. 该元素与 selector 匹配。
        注意,::slotted 选择器不能用于任何插槽中更深层的内容。

    用自定义 CSS 属性作为勾子

    像 :host 这样的选择器应用规则到 <custom-dialog> 元素或 <user-card>,但是如何设置在它们内部的 shadow DOM 元素的样式呢?
    没有选择器可以从文档中直接影响 shadow DOM 样式。但是,正如我们暴露用来与组件交互的方法那样,我们也可以暴露 CSS 变量(自定义 CSS 属性)来对其进行样式设置。
    自定义 CSS 属性存在于所有层次,包括 light DOM 和 shadow DOM。
    例如,在 shadow DOM 中,我们可以使用 --user-card-field-color CSS 变量来设置字段的样式,而外部文档可以设置它的值
     

    Shadow DOM 和事件(events)

    为了保持细节简单,浏览器会重新定位(retarget)事件。
    当事件在组件外部捕获时,shadow DOM 中发生的事件将会以 host 元素作为目标。
    如果你点击了 button,就会出现以下信息:
    1. Inner target: BUTTON —— 内部事件处理程序获取了正确的目标,即 shadow DOM 中的元素。
    1. Outer target: USER-CARD —— 文档事件处理程序以 shadow host 作为目标。
    事件重定向是一件很棒的事情,因为外部文档并不需要知道组件的内部情况。从它的角度来看,事件是发生在 <user-card>
    如果事件发生在 slotted 元素上,实际存在于 light DOM 上,则不会发生重定向。
     
    例如,,如果用户点击了 <span slot="username">,那么对于 shadow 和 light 处理程序来说,事件目标就是当前这个 span 元素。
     
    如果单击事件发生在 "John Smith" 上,则对于内部和外部处理程序来说,其目标是 <span slot="username">。这是 light DOM 中的元素,所以没有重定向。
    另一方面,如果单击事件发生在源自 shadow DOM 的元素上,例如,在 <b>Name</b> 上,然后当它冒泡出 shadow DOM 后,其 event.target 将重置为 <user-card>

    冒泡(bubbling), event.composedPath()

    出于事件冒泡的目的,使用扁平 DOM(flattened DOM)。
    使用 event.composedPath() 获得原始事件目标的完整路径以及所有 shadow 元素。正如我们从方法名称中看到的那样,该路径是在组合(composition)之后获取的。
    因此,对于 <span slot="username"> 上的点击事件,会调用 event.composedPath() 并返回一个数组:[spanslotdivshadow-rootuser-cardbodyhtmldocumentwindow]。

    event.composed

    大多数事件能成功冒泡到 shadow DOM 边界。很少有事件不能冒泡到 shadow DOM 边界。
    这由 composed 事件对象属性控制。如果 composed 是 true,那么事件就能穿过边界。否则它仅能在 shadow DOM 内部捕获。
     

    参考链接:
    1. Web components (javascript.info)
     
    上一篇
    现代 JavaScript —— 正则表达式
    下一篇
    现代 JavaScript —— 杂项(动画)

    评论
    Loading...