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 有两种:
- Autonomous custom elements (自主自定义标签) —— “全新的” 元素, 继承自
HTMLElement
抽象类.
- 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 子树:
- Light tree(光明树) —— 一个常规 DOM 子树,由 HTML 子元素组成。我们在之前章节看到的所有子树都是「光明的」。
- Shadow tree(影子树) —— 一个隐藏的 DOM 子树,不在 HTML 中反映,无法被察觉。
如果一个元素同时有以上两种子树,那么浏览器只渲染 shadow tree。但是我们同样可以设置两种树的组合。
调用
elem.attachShadow({mode: …})
可以创建一个 shadow tree。有两个限制:
- 在每个元素中,我们只能创建一个 shadow root。
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 被非常明显地和主文档分开:
- Shadow DOM 元素对于 light DOM 中的
querySelector
不可见。实际上,Shadow DOM 中的元素可能与 light DOM 中某些元素的 id 冲突。这些元素必须在 shadow tree 中独一无二。
- 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="..."
。这些元素在插槽内被渲染:结果被叫做 扁平化(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)
伪类。它根据两个条件来匹配元素 - 这是一个占槽元素,来自于 light DOM。插槽名并不重要,任何占槽元素都可以,但只能是元素本身,而不是它的子元素 。
- 该元素与
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,就会出现以下信息:
- Inner target:
BUTTON
—— 内部事件处理程序获取了正确的目标,即 shadow DOM 中的元素。
- 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()
并返回一个数组:[span
, slot
, div
, shadow-root
, user-card
, body
, html
, document
, window
]。event.composed
大多数事件能成功冒泡到 shadow DOM 边界。很少有事件不能冒泡到 shadow DOM 边界。
这由
composed
事件对象属性控制。如果 composed
是 true
,那么事件就能穿过边界。否则它仅能在 shadow DOM 内部捕获。参考链接:
- 作者:JinSo
- 链接:https://jinso365.top/article/modern-javascript-others-web-components
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。