前言
在重学 JavaScript 过程中,了解到了 Web 组件,而其中的一些知识点总感觉和 vuejs 中的某些概念很相似,比如 Web 组件中涉及的内容:
- HTML 模板,即 template 标签
- 自定义元素
- 影子 DOM 和 slot 标签
- 影子 DOM 实现样式隔离
那么下面就一起看看 Web 组件的这些内容和 vue 中的某些概念相似在哪吧!
Web Components
Web 组件到底是什么?
Web 组件其实就是一套用于增强 DOM 行为的工具,其内容包括 影子 DOM、自定义元素 和 HTML 模板 等.
目前 Web Components 没有得到广泛应用,主要是因为存在以下问题:
- 没有统一的 "Web Components" 规范
- Web 组件存在向后不兼容的版本问题
- 浏览器实现极其不一致
由于存在这些问题,因此使用 Web 组件通常需要引入一个 Web 组件库,用于模拟浏览器中缺失的 Web 组件. 比如 Polymer、LitElement,新项目更推荐使用 LitElement.
HTML 模板 —— template 标签
我们可以先思考下面的问题,并尝试给出一些解决方法。
问题:如何把对应的三个背景颜色分别为红绿蓝的
p
标签,3s 后动态渲染在指定的位置中,如<div id="root"></div>
?方案一: 使用 innerHtml
let divRoot = document.querySelector("#root"); setTimeout(()=>{ divRoot.innerHTML = ` <p class="red">Make me red!</p> <p class="blue">Make me blue!</p> <p class="green">Make me green!</p> ` },3000); 复制代码
方案二: 使用 createElement + createDocumentFragment + appendChild
let divRoot = document.querySelector("#root"); let colors = ['red','blue','green']; let fragment = document.createDocumentFragment(); for (const color of colors) { const p = document.createElement('p'); p.className = color; p.innerText = `Make me ${color}!` fragment.appendChild(p); } setTimeout(()=>{ divRoot.appendChild(fragment); },3000); 复制代码
通过 方案一 和 方案二 都能实现对应的效果,但是对于书写 HTML 结构来讲都是不友好的,首先就是它们都无法做到像直接书写 HTML 时的友好提示,其次就是它们都只适用结构非常简单的内容,一旦结构内容有多层嵌套时,单纯设计 html 结构就变得很复杂。
使用 <template>
标签
PS:
<template>
标签是 HTML 中存在的,并不是 vue 中特有的,不要混淆.
在有 Web 组件之前,一直缺少基于 HTML 解析构建 DOM 子树,然后在需要时再把这个子树渲染出来的机制.
在 Web 组件中,可以通过使用 <template>
标签提前在页面中写出特殊标记,让浏览器自动将其解析为 DOM 子树,但跳过渲染. 如下:
<body> <template id="tpl"> <p>I'm inside a custom element template!</p> </template> </body> 复制代码
在浏览器中通过开发者工具检查网页内容时,可以看到 <template>
标签中渲染的节点内容 是基于 DocumentFragment,而 DocumentFragment 也是批量向 HTML 中添加元素的高效工具,此时的 DocumentFragment 就像一个对应子树的最小化 document 对象,也就是说,如果需要操作 <template>
标签中节点,必须要先获取对应 DocumentFragment 的引用,即 document.querySelector('#tpl').content
.
下面是通过 <template>
标签实现上面问题的解决方案:
<body> <div id="root"></div> <template id="tpl"> <p class="red">Make me red!</p> <p class="blue">Make me blue!</p> <p class="green">Make me green!</p> </template> <script> let divRoot = document.querySelector("#root"); let tpl = document.querySelector("#tpl").content; setTimeout(() => { divRoot.appendChild(tpl); }, 3000); </script> </body> 复制代码
template 模板脚本
如果在 template
标签中存在对应的 js 脚本,那么脚本执行可以推迟到将 DocumentFragment 的内容实际添加到 DOM 树,即 延迟执行 js 脚本.
直接看下面的例子:
<body> <div id="foo"></div> <template id="bar"> <script> console.log('Template script executed'); </script> </template> <script> const fooElement = document.querySelector('#foo'); const barTemplate = document.querySelector('#bar'); const barFragment = barTemplate.content; console.log('About to add template');// 1. About to add template fooElement.appendChild(barFragment);//2. Template script executed console.log('Added template');// 3. Added template </script> </body> 复制代码
影子 DOM —— shadow DOM
首先来看下面的问题,然后思考一下:
如何给 HTML 中众多相似的结构去渲染不同的样式呢?通常情况下,为了给每个子树应用唯一的样式,又不使用 style 属性,就需要给每个子树添加一个唯一的类名,然后通过相应的选择符为它们添加样式。
存在的问题:
- 必须通过 唯一 的 样式选择器 决定渲染对应的样式渲染
- 样式全部作用于顶级 DOM 树中,即使当前展示的内容需要使用很少的样式
- 没有真正实现 CSS 样式的隔离,很容易因为书写问题导致 样式冲突
理想情况下,应该能够把 CSS 限制在使用它们的 DOM 上。
影子 DOM 是什么?
通过影子 DOM 就可以将一个 完整的 DOM 树 作为节点添加到 父 DOM 树。
即可以实现 DOM 封装,意味着 CSS 样式和 CSS 选择符可以限制在影子 DOM 子树中,而不是作用于整个顶级 DOM 树。
创建影子 DOM
影子 DOM 是通过 attachShadow() 方法创建并添加给有效 HTML 元素的:
- 影子宿主(shadow host),即容纳影子 DOM 的元素
- 影子根(shadow root),即影子 DOM 的根节点
- attachShadow() 方法需要一个 shadowRootInit 对象,即这个对象必须包含一个 mode 属性,值为 "open" 或 "closed"
- mode 属性值为 "open" 的影子 DOM 的引用可通过 shadowRoot 属性在 HTML 元素上获得,属性值 "closed" 影子 DOM 的引用则无法获取
document.body.innerHTML = ` <div id="foo"></div> <div id="bar"></div> `; const foo = document.querySelector('#foo'); const bar = document.querySelector('#bar'); // 创建不同 dom 元素的影子节点,一个 dom 节点只能有一个 影子 DOM const openShadowDOM = foo.attachShadow({ mode: 'open' }); const closedShadowDOM = bar.attachShadow({ mode: 'closed' }); // 直接访问影子根节点 console.log(openShadowDOM); // #shadow-root (open) console.log(closedShadowDOM); // #shadow-root (closed) // 为影子 DOM 添加内容和样式,这里的样式是完全隔离的,并不会发生样式冲突 openShadowDOM.innerHTML = ` <p>this is red</p> <style> p{ background: red; } </style> ` closedShadowDOM.innerHTML = ` <p>this is blue</p> <style> p{ background: blue; } </style> ` // 通过影子宿主访问影子根节点 console.log(foo.shadowRoot); // #shadow-root (open) console.log(bar.shadowRoot); // null 复制代码
合成与影子 DOM 槽位 slot
影子 DOM 是为自定义 Web 组件设计的,为此需要支持嵌套 DOM 片段,也就是说位于 影子宿主 中的 HTML 需要一种机制以渲染到影子 DOM中去,但这些 HTML 又不需要存在于影子 DOM 树中.
[ 影子 DOM 具有最高优先级 ]
正常情况下,影子 DOM 一添加到元素中,浏览器就会赋予它 最高优先级,优先渲染它的内容而不是原来的 dom 内容,比如下面的例子:
document.body.innerHTML = ` <div id="foo"> <h1>I'm foo's child</h1> </div> `; const foo = document.querySelector('#foo'); const openShadowDOM = foo.attachShadow({ mode: 'open' }); // 为影子 DOM 添加内容 openShadowDOM.innerHTML = ` <p>this is openShadowDOM content</p> ` 复制代码
[ <slot>
标签 ]
为了显示影子宿主中原本存在的 HTML 内容,我们需要使用 <slot>
标签指示浏览器在哪里放置原来的 HTML 内容,将上面的例子修改成如下形式:
document.body.innerHTML = ` <div id="foo"> <h1>I'm foo's child</h1> </div> `; const foo = document.querySelector('#foo'); const openShadowDOM = foo.attachShadow({ mode: 'open' }); // 为影子 DOM 添加内容 openShadowDOM.innerHTML = ` <p>this is openShadowDOM content</p> <slot></slot> ` 复制代码