影子节点 shadowDOM

shadow DOM是什么

<video controls autoplay name="media">
  <source id="mp4" src="trailer.mp4" type="video/mp4">
</video>

上面这是最简单的视屏标签,里面有默认的音量等按键。在源代码中根本没有一点痕迹。那这些节点是从哪里来的?
  这就是shadow DOM,视屏的控件在浏览器中真实面目如下:


shadow DOM浏览器中体现

发现#shadow-root是灰色的,这是浏览器为了表明在shadow DOM中,代表页面其他的部分的内容不会对内部产生影响(可用特定方式穿透,后文会说到)

内容具体指css选择器和javascript代码

简而言之,Shadow DOM 是一个 HTML 的新规范,其允许开发者封装HTML组件(类似vue组件,将html,css,js独立部分提取)。

为什么要使用shadow DOM

Bootstrap的名字你一定不陌生,代码一般如下:

<ul class="media-list">
  <li class="media">
    <div class="media-left">
       <a href="#">
        ![](...)
       </a>
    </div>
    <div class="media-body">
       <h4 class="media-heading">Media heading</h4>
    </div>
  </li>
</ul>

非常的简单好用,但是这些东西你并没有深入了解,往往结构变复杂之后,都是一堆模板,修改是一个很难的问题,牵一发而动全身。
  这种情况下shadow DOM的优势十分巨大,你可以这么写模板

// 我是一个简洁的模板
<bootstrap-media-list>
  <a href="#">
    ![](...)
  </a>
  <h4 class="media-heading">Media heading</h4>
</bootstrap-media-list> 

当然想实现这么写还需要一些js,css配合才行

如何使用

先运行一个例子

<div class="widget">Hello, world!</div>
<script>
  var host = document.querySelector('.widget');
  var root = host.createShadowRoot();
  root.textContent = '我在你的 div 里!';
</script> 

运行结果

  首先我们指定一个宿主节点shadow host)然后创建影子根(shadow root)为它添加一个文本节点,结果宿主中的内容未被渲染。

如何渲染宿主节点中的内容

只渲染影子根中的内容基本没有实用的地方,但能自由的渲染宿主节点中的内容的话就可以让页面展现更灵活。我们需要content标签

<div class="pokemon">胖丁</div>
<template class="pokemon-template">
  <h1>一只野生的<content></content>出现了!</h1>
</template>
<script>
  var host = document.querySelector('.pokemon');
  var root = host.createShadowRoot();
  var template = document.querySelector('.pokemon-template');
  root.appendChild(document.importNode(template.content, true)); 
</script>


  <content>标签创建了一个**插入点 **将.pokemon里面的文本投影出来,多个内容匹配时可以实用select属性指定

<div class="host">
    <p>大慈大悲,由诸葛亮进化而来。</p>
    <span class="name">观音姐姐兽</span>
</div>
<template class="root-template">
    <dl>
      <dt>名字</dt>
      <dd><content select=".name"></content></dd>
    </dl>
    <p><content select=""></content></p>
</template>
<script>
    var host = document.querySelector('.host');
    var root = host.createShadowRoot();
    var template = document.querySelector('.root-template');
    root.appendChild(template.content);
</script>


  可以使用select属性类似选择器的形式渲染宿主节点中匹配元素的投影。这种形式不但可以改变DOM流的顺序也可以让布局变得灵活。
  在模板的最后<content select=""></content>是一种贪心匹配,把宿主节点中所有未被匹配的内容全部投影。需要注意的是把贪心匹配放在最前面会把所有的节点投影并且之后的select不会再获取到被其投影的内容。
  以下都是等效的:

  • <content></content>
  • <conent select=""></conent>
  • <content select="*"></content>

样式渲染与封装

先看一个简单的例子

<style>
    button {
        font-size: 18px;
        font-family: '华文行楷';
    }
</style>
<button>普通按钮</button>
<div></div>
<script>
    var host = document.querySelector('div');
    var root = host.createShadowRoot();
    root.innerHTML = 
      '<style>button { font-size: 24px; color: blue; } </style>'+
      '<button>影子按钮</button>';
</script>


  在影子节点中存在边界使shadow DOM样式和正常DOM流中的样式不相互干扰。这是一种作用域化的体现,不用再担心样式的相互冲突。

(:host)选择器

:host是伪类选择器选择宿主节点,我们可以扩展一下上面的例子

<style>
    p {
        font-size: 12px;
    }
</style>
<p>我的文本</p>
<button>我的按钮</button>
<template class="shadow-template">
    <style>
        :host(p) {
            color: green;
        }
        :host(button) {
            color: red;
        }
        :host(*) {
            font-size: 24px;
        }
    </style>
    <content select=""></content>
</template>
<script>
    var root1 = document.querySelector('p').createShadowRoot();
    var root2 = document.querySelector('button').createShadowRoot();

    var template = document.querySelector('.shadow-template');

    root1.appendChild(document.importNode(template.content, true));
    root2.appendChild(document.importNode(template.content, true));
</script>

  这个例子有几个点:

  • p标签字体大小是12px = 影子样式的优先级不如页面样式
  • :host选择器中可以使用任意合法选择器,*应用于所有
  • 通过挂载不同宿主渲染出不同的内容,可以实现主题化
      上面的主题化并不完全,只根据挂载元素进行选择也就是说.parent > .child,但是我们还能通过:host-context实现.parent < .child如下
<div class="serious">
    <p class="serious-widget">
        serious-widget
    </p>
</div>
<div class="playful">
    <p class="playful-widget">
        playful-widget
    </p>
</div>
<template class="widget-template">
    <style>
        :host-context(.serious) {
            width: 250px;
            height: 50px;
            background: tomato;
        }
        :host-context(.playful) {
            width: 250px;
            height: 50px;
            background: deepskyblue;
        }
    </style>
    <content></content>
</template>
<script>
var root1 = document.querySelector('.serious-widget').createShadowRoot();
var root2 = document.querySelector('.playful-widget').createShadowRoot();
var template = document.querySelector('.widget-template');
root1.appendChild(document.importNode(template.content, true));
root2.appendChild(document.importNode(template.content, true));
</script>

上面的效果就非常不错了,可以进行动态组件构建

ps: 伪类,伪元素选择器也可以直接使用,效果和正常节点中一致

(::content)选择器

在使用 shadow DOM 的时候应该确保内容和表现的分离,也就是说文本应该来自页面而不是埋在 shadow DOM 的模板里。所以我们需要在模板中对分布式节点进行渲染。

<div class="widget">
    <button>分布节点碉堡啦!</button>
</div>
<template class="widget-template">
    <style>
        ::content > button {
            color: white;
            background: tomato;
            border-radius: 10px;
            border: none;
            padding: 10px;
        }
    </style>
    <content select=""></content>
</template>
<script>
var root = document.querySelector('.widget').createShadowRoot();
var template = document.querySelector('.widget-template');
root.appendChild(document.importNode(template.content, true));
</script>

打破作用域(::shadow)

我们可以在挂载节点中使用::shadow,比如

<style>
    .sign-up::shadow #username{
        font-size: 20px;
        border: 1px solid red;
    }
</style>
<div class="sign-up"></div>
<template class="sign-up-template">
    <style>
        #username{
            font-size: 12px;
        }
    </style>
    <div>
        <input type="text" id="username" placeholder="用户名">
    </div>
</template>
<script>
var root = document.querySelector('.sign-up').createShadowRoot();
var template = document.querySelector('.sign-up-template');
root.appendChild(document.importNode(template.content, true));
</script>

不过缺点是只能穿透一层,但我们还有一个神器!

多层穿透(/deep/)

<style>
    #foo /deep/ button {
        color: red;
    }
</style>
<div id="foo"></div>
<template>
    <div id="bar"></div>
</template>
<script>
    var root1 = document.querySelector('#foo').createShadowRoot();
    var template = document.querySelector('template');
    root1.appendChild(document.importNode(template.content, true));
    var root2 = root1.querySelector('#bar').createShadowRoot();
    root2.innerHTML = '<button>点我点我</button>';
</script>

javascript的区别

  1. 数据并没有块级化,仍挂载在window
  • 事件重定向(原来绑定在 shadow DOM 节点中的事件被重定向了,所以他们看起来像绑定在宿主节点上一样)
<input id="normal-text" type="text" value="I'm normal text">
<div id="host"></div>
<template>
    <input id="shadow-text" type="text" value="I'm shadow text">
</template>
<script>
    var root = document.querySelector('#host').createShadowRoot();
    var template = document.querySelector('template');
    root.appendChild(document.importNode(template.content, true));
    document.addEventListener('click', function(e) {
      console.log(e.target.id + ' clicked!');
    });
</script>

可以看到在影子节点的事件被宿主节点代理。

事件阻塞

在监听以下事件时会被阻塞在影子节点的根:

  • aborterror
  • select
  • change
  • load
  • reset
  • reset
  • resize
  • scroll
  • selectstar
<input id="normal-text" type="text" value="I'm normal text">
<div id="host">
    <input id="distributed-text" type="text" value="I'm distributed text">
</div>
<template>
    <div><content></content></div>
    <div>
        <input id="shadow-text" type="text" value="I'm shadow text">
    </div>
</template>
<script>
    var root = document.querySelector('#host').createShadowRoot();
    var template = document.querySelector('template');
    root.appendChild(document.importNode(template.content, true));
    document.addEventListener('select', function(e) {
      console.log(e.target.id + ' text selected!');
    });
</script>

事件影子节点的根上被阻止,无法冒泡到ducoment,所以无法监听。

分布节点

分布节点指之前通过<content>标签将宿主节点的内容投影,分布节点不会发生上面的阻塞情况,因为这个只是一个投影实际的内容还是挂载在宿主节点上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容