Web组件之HTML模板+DocumentFragment+影子DOM

问题: 有没有一种高效的方法,给一个Dom元素添加子Dom树?

方法1: 使用document.createElement() -----> dom.appendChild() 一个一个添加,这种方法主要是效率低下,并且每添加一次都要发生回流和重绘,十分影响性能。
方法2: dom.innerHTML() 可以将代表dom子树的一个字符串直接添加的dom上,但是又一个很严重的安全问题,这为XSS跨站脚本攻击提供了便利。

新的解决办法:

1. HTML模板

使用template标签

<template id="foo">
    <p> This is p tag in foo div</p>
</template>

这时template标签内的内容不会真的渲染到dom树上,所以也无法通过选择器获取template标签里面的子标签,template里面的内容会自动变成DocumentFragment对象。

<template id="foo">
	#documetn-fragment
    	<p> This is p tag in foo div</p>
</template>

可以通过一下dom.content属性拿到该对象:

const fooEl = document.getElementById('foo')
console.log(fooEl.content);

/* 打印输出:
	#documetn-fragment
    	<p> This is p tag in foo div</p>
*/

可以将通过**.content**拿到的对象进行dom的操作。

奇淫巧技:
可以将script脚本推迟到DocumentFragment的内容被实际添加到DOM树中的时候再执行。方法就是将script标签放到template标签内部。

2. 使用DocumentFragment对象

一下两种方式都创建添加DocumentFragment片段,需要注意的是,为片段添加子元素不会导致回流和重绘。在进行多次dom操作的时候可以有效提升性能。

//const fragment = new DocumentFragment()
const fragment = document.createDocumentFragment()    //创建片段的两种方式

for(let color of ['red', 'green', 'pink']){
   const pEl = document.createElement('p')
   pEl.innerText =  `this is ${color}`
   fragment.appendChild(pEl)     //可以将创建的片段看做是dom元素,但它并不是真正的dom
 }
 const fooEl = document.getElementById('foo')
 fooEl.appendChild(fragment)    //将片段挂载到真正的dom上,此时的片段将会被渲染出来

3. 影子DOM

a. 理解影子DOM

通过影子DOM可以将一棵完整的Dom树添加作为节点添加到父DOM树,这样可以实现DOM封装,这意味着css选择符和css样式可以限制在影子DOM子树而不是整个顶级DOM书中。影子DOM与模板template的区别是,影子DOM会实际渲染到DOM树中,而模板不会,但是他们都实现了与主DOM树的一定程度的分离。

<div>
  <p>Make me red</p>
</div>
<div>
  <p>Make me green</p>
</div>
<div>
  <p>Make me pink</p>
</div>

试想一下,有以上三组标签,我们想要将三个不同的p标签渲染成三个不同的颜色,我们应该怎么来做。为每个p标签的style属性写入样式,或者给每个标签定义一个类?这些方法完全可以,但是利用类来定义可能会造成污染,因为这个定义是全局的。这里可以利用影子DOM将对应的属性现在在使用他们的DOM上

b. 创建影子DOM

并非所有元素都可以创建影子DOM,尝试给无效元素或者已经有了影子DOM的元素添加影子DOM会导致错误。
以下是可以容纳影子DOM的元素:

<article>
<aside>
<blockquote>
<body>
<div>
<footer>
<h1 - h6>
<header>
<main>
<nav>
<p>
<section>
<span>

相关概念:
影子宿主: 容纳影子DOM的元素
影子根: 影子DOM的根节点。
主要方法:

const containerEl = document.getElementById('container')
const conShadow = containerEl.attachShadow({mode: 'open'})

//conShadow 返回的影子实例
//attachShadow({mode: 'open'}) 创建影子实例的方法,参数为一个对象
//这个对象必须具有一个mode属性,属性值为open 或者closed(创建保密影子dom)

c. 使用影子DOM

可以像使用常规DOM一样使用影子DOM。

<body>
  <div id="container">
    this is container div
  </div>
  <h1>this is not shadow dom</h1>
</body>
<script>
  const containerEl = document.getElementById('container')
  for(let color of ['red', 'green', 'pink']){
    const divEl = document.createElement('div'),    //首先创建一个div标签
        shadowEl = divEl.attachShadow({mode: 'open'})   //给创建的div标签添加影子DOM
    shadowEl.innerHTML = `     //修改影子DOM里面的内容
        <h1>this is ${color}</h1>
        <style>      //为影子DOM指定局部的样式,不会污染全局
          h1{
            color: ${color}
          }
        </style>
      `
    containerEl.appendChild(divEl)   //必须将作为影子宿主的div标签添加到真是的dom元素中去
  }
</script>

在这里插入图片描述
从结果上看,影子DOM具有局部样式。

d. 合成与影子槽位:

问题:如下,如果影子宿主dom本身内部还有标签时,会出现问题,由于影子DOM优先级要高,所以影子宿主内部的元素不会进行渲染,有时候这可能不是我们想要的结果。

<body>
  <div id="container">
    this is container div
  </div>
</body>
<script>
  const containerEl = document.getElementById('container')
  const conShadow = containerEl.attachShadow({mode: 'open'})
  conShadow.innerHTML = '<h1>This is content in shadow dom</h1>'
</script>

浏览器只渲染了影子DOM内部的内容
解决办法:
在影子DOM中使用slot标签来指示浏览器在哪里放置原来的HTML。对以上代码做出如下修改

<body>
  <div id="container">
    this is container div
  </div>
</body>
<script>
  const containerEl = document.getElementById('container')
  const conShadow = containerEl.attachShadow({mode: 'open'})
  conShadow.innerHTML = `
    <h1>This is content in shadow dom</h1>
    <slot></slot>     //这里是修改内容,增加了一个slot插槽
  `
</script>

浏览器渲染结果:
在这里插入图片描述

需要注意的是: 使用插槽相当于做了投射,将影子宿主自己的HTML投射到了影子dom内部,但是被投射的HTML实际上还是影子宿主的字dom树。

e. 使用命名插槽实现多个投射

<body>
  <div id="container">
  	<!--给p标签指定了一个slot属性,并且赋值 -->
    <p slot="first">This is the first p tag</p>    <!-- -->
    <p slot="second">This is the second p tag</p>
  </div>
</body>
<script>
  const containerEl = document.getElementById('container')
  const conShadow = containerEl.attachShadow({mode: 'open'})
  conShadow.innerHTML = `
    <slot name="first"></slot>     //使用name属性告诉应该放哪个p标签
    <h1>This is content in shadow dom</h1>
    <slot name="second"></slot>
  `
</script>

渲染结果:
在这里插入图片描述

f 事件重定向

影子DOM上发生的事件会逃出影子DOM并经过事件重定向在影子宿主上被处理,就好像事件就发生在影子宿主本身上一样。通过slot插槽从外部投射进影子DOM的元素上的事件不会发生事件重定向。因为他仅仅就是做了投射,元素本身还是在原来的位置上。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值