专栏名称: 创宇前端
目录
相关文章推荐
51好读  ›  专栏  ›  创宇前端

深度介绍:💾 你听说过原生 HTML 组件吗?

创宇前端  · 掘金  · 前端  · 2018-10-18 02:18

正文

请到「今天看啥」查看全文


我们来看一看浏览器利用 Shadow DOM 实现的一个示例吧,那就是 video 标签:

<video controls src="./video.mp4" width="400" height="300"></video>
复制代码

我们来看一下浏览器渲染的结果:

等一下!不是说 Shadow DOM 吗?这和普通 DOM 有啥区别???

在 Chrome 中,Elements 默认是不显示内部实现的 Shadow DOM 节点的,需要在设置中启用:

注:浏览器默认隐藏自身的 Shadow DOM 实现,但如果是用户通过脚本创造的 Shadow DOM,是不会被隐藏的。

然后,我们就可以看到 video 标签的真面目了:

在这里,你可完全像调试普通 DOM 一样随意调整 Shadow DOM 中的内容(反正和普通 DOM 一样,刷新一下就恢复了)。

我们可以看到上面这些 shadow DOM 中的节点大多都有 pseudo 属性,根据这个属性,你就可以在外面编写 CSS 样式来控制对应的节点样式了。比如,将上面这个 pseudo="-webkit-media-controls-overlay-play-button" 的 input 按钮的背景色改为橙色:

video::-webkit-media-controls-overlay-play-button {
  background-color: orange;
}
复制代码

由于 Shadow DOM 实际上也是 DOM 的一种,所以在 Shadow DOM 中还可以继续嵌套 Shadow DOM,就像上面那样。

浏览器中还有很多 Element 都使用了 Shadow DOM 的形式进行封装,比如 <input> <select> <audio> 等,这里就不一一展示了。

由于 Shadow DOM 的隔离性,所以即便是你在外面写了个样式: div { background-color: red !important; } ,Shadow DOM 内部的 div 也不会受到任何影响。

也就是说,写样式的时候,该用 id 的时候就用 id,该用 class 的时候就用 class,一个按钮的 class 应该写成 .button 就写成 .button 。完全不用考虑当前组件中的 id、class 可能会与其他组件冲突,你只要确保一个组件内部不冲突就好——这很容易做到。

这解决了现在绝大多数的组件化框架都面临的问题:Element 的 class(className) 到底怎么写?用前缀命名空间的形式会导致 class 名太长,像这样: .header-nav-list-sublist-button-icon ;而使用一些 CSS-in-JS 工具,可以创造一些唯一的 class 名称,像这样: .Nav__welcomeWrapper___lKXTg ,这样的名称仍旧有点长,还带了冗余信息。

ShadowRoot

ShadowRoot 是 Shadow DOM 下面的根,你可以把它当做 DOM 中的 <body> 一样看待,但是它不是 <body> ,所以你不能使用 <body> 上的一些属性,甚至它不是一个节点。

你可以通过 ShadowRoot 下面的 appendChild querySelectorAll 之类的属性或方法去操作整个 Shadow DOM 树。

对于一个普通的 Element,比如 <div> ,你可以通过调用它上面的 attachShadow 方法来创建一个 ShadowRoot(还有一个 createShadowRoot 方法,已经过时不推荐使用), attachShadow 接受一个对象进行初始化: { mode: 'open' } ,这个对象有一个 mode 属性,它有两个取值: 'open' 'closed' ,这个属性是在创造 ShadowRoot 的时候需要初始化提供的,并在创建 ShadowRoot 之后成为一个只读属性。

mode: 'open' mode: 'closed' 有什么区别呢?在调用 attachShadow 创建 ShadowRoot 之后, attachShdow 方法会返回 ShadowRoot 对象实例,你可以通过这个返回值去构造整个 Shadow DOM。当 mode 为 'open' 时,在用于创建 ShadowRoot 的外部普通节点(比如 <div> )上,会有一个 shadowRoot 属性,这个属性也就是创造出来的那个 ShadowRoot,也就是说,在创建 ShadowRoot 之后,还是可以在任何地方通过这个属性再得到 ShadowRoot,继续对其进行改造;而当 mode 为 'closed' 时,你将不能再得到这个属性,这个属性会被设置为 null ,也就是说,你只能在 attachShadow 之后得到 ShadowRoot 对象,用于构造整个 Shadow DOM,一旦你失去对这个对象的引用,你就无法再对 Shadow DOM 进行改造了。

可以从上面 Shadow DOM 的截图中看到 #shadow-root (user-agent) 的字样,这就是 ShadowRoot 对象了,而括号中的 user-agent 表示这是浏览器内部实现的 Shadow DOM,如果使用通过脚本自己创建的 ShadowRoot,括号中会显示为 open closed 表示 Shadow DOM 的 mode。

浏览器内部实现的 user-agent 的 mode 为 closed ,所以你不能通过节点的 ShadowRoot 属性去获得其 ShadowRoot 对象,也就意味着你不能通过脚本对这些浏览器内部实现的 Shadow DOM 进行改造。

HTML Template

有了 ShadowRoot 对象,我们可以通过代码来创建内部结构了,对于简单的结构,也许我们可以直接通过 document.createElement 来创建,但是稍微复杂一些的结构,如果全部都这样来创建不仅麻烦,而且代码可读性也很差。当然也可以通过 ES6 提供的反引号字符串( const template = `......`; )配合 innerHTML 来构造结构,利用反引号字符串中可以任意换行,并且 HTML 对缩进并不敏感的特性来实现模版,但是这样也是不够优雅,毕竟代码里大段大段的 HTML 字符串并不美观,即便是单独抽出一个常量文件也是一样。

这个时候就可以请 HTML Template 出场了。我们可以在 html 文档中编写 DOM 结构,然后在 ShadowRoot 中加载过来即可。

HTML Template 实际上就是在 html 中的一个 <template> 标签,正常情况下,这个标签下的内容是不会被渲染的,包括标签下的 img、style、script 等都是不会被加载或执行的。你可以在脚本中使用 getElementById 之类的方法得到 <template> 标签对应的节点,但是却无法直接访问到其内部的节点,因为默认他们只是模版,在浏览器中表现为 #document-fragment ,字面意思就是“文档片段”,可以通过节点对象的 content 属性来访问到这个 document-fragment 对象。

通过 document-fragment 对象,就可以访问到 template 内部的节点了,通过 document.importNode 方法,可以将 document-fragment 对象创建一份副本,然后可以使用一切 DOM 属性方法替换副本中的模版内容,最终将其插入到 DOM 或是 Shadow DOM 中。







请到「今天看啥」查看全文