基于Slot的Shadow DOM API

这是webkitBlog上一篇文章的翻译,原文链接

很高兴宣布我们已经对slot-base Shadow DOM API提供基本的支持。最新支持它的浏览器是nightly builds of WebKit,Shadow DOM草案最早由Google推荐,意在支持可重用的网络组件,具体的说:在某个DOM节点的同一级插入一些特殊的DOM节点,这些插入的DOM节点被叫做Shadow DOM,它们可以被渲染,且不改变原有DOM的结构(Shadow DOM, in particular, provides a lightweight encapsulation for DOM trees by allowing a creation of a parallel tree on an element called a “shadow tree” that replaces the rendering of the element without modifying the underlying DOM tree.)。因为一个Shadow DOM并不是同一个传统意义上的DOM节点,所以,你用querySelector等API无法访问到它,也就不需要担心不小心改变了它内部的结构。Shadow DOM中的style样式也是有作用域的,不要当心SD中的样式污染了外部样式,反之亦然。

隔离样式

运用Shadow DOM的一个首要作用就是进行样式隔离。我们首先来看一个进度条的例子。

<style>
.progress { position: relative; border: solid 1px #000; padding: 1px; width: 100px; height: 1rem; }
.progress > .bar { background: #9cf; height: 100%; }
.progress > .label { position: absolute; top: 0; left: 0; width: 100%;
    text-align: center; font-size: 0.8rem; line-height: 1.1rem; }
</style>
<template id="progress-bar-template">
    <div class="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
        <div class="bar"></div>
        <div class="label">0%</div>
    </div>
</template>
<script>
function createProgressBar() {
    var fragment = document.getElementById('progress-bar-template').content.cloneNode(true);
    var progressBar = fragment.querySelector('div');
    progressBar.updateProgress = function (newPercentage) {
        this.setAttribute('aria-valuenow', newPercentage);
        this.querySelector('.label').textContent = newPercentage + '%';
        this.querySelector('.bar').style.width = newPercentage + '%';
    }
    return progressBar;
}
</script>

请注意template标签的使用,它允许使用者定义并且引入模板,这是在WebKit中实现的一个Web组件,已经写进了HTML5标准。一个template标签能够出现在html文档中的任何位置(比如table标签和tr标签之间)。template中的内容是惰性的,里面的script标签、img标签是不会自动加载的。以上的代码中,我们定义了一个组件:progressBar,想要在页面中插入这个组件,只需要简单的几句js:

var progressBar = createProgressBar();
container.appendChild(progressBar);
...
progressBar.updateProgress(10);

然后你将得到:


这样引入一个控件是很方便的,但是有个问题:如果文档的其他地方也有class="progress"的节点,那么我们控件的样式将污染它。

<section class="project">
    <p class="progress">Pending an approval</p>
</section>

类似的,外部样式也会影响到控件的样式:

<style>
.label { color: red; }
</style>

一般情况下,我们可以为控件定义一个特殊的类型(比如.custom-progressbar)来避免样式冲突,而Shadow DOM提供了一个更加优雅的解决方案。
想法是为外部div引入一个“包含层(encapsulation layer)”,包含层之类的样式不会影响外部的样式,外部的使用者也看不见包含层之类的实现。要做到这些,我们首先在外部div上调用attachShadow({model:'close'})创建一个ShadowRoot,然后在这个ShadowRoot下层添加任何我想要的内容。我们说这个外部div“掌管(host)”了它的ShadowRoot,看看下面的代码:

<template id="progress-bar-template">
    <style>
        .progress { position: relative; border: solid 1px #000; padding: 1px; width: 100px; height: 1rem; }
        .progress > .bar { background: #9cf; height: 100%; }
        .progress > .label { position: absolute; top: 0; left: 0; width: 100%;
            text-align: center; font-size: 0.8rem; line-height: 1.1rem; }
    </style>
    <div class="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
        <div class="bar"></div>
        <div class="label">0%</div>
    </div>
</template>
<script>
function createProgressBar() {
    var progressBar = document.createElement('div');
    var shadowRoot = progressBar.attachShadow({mode: 'closed'});
    shadowRoot.appendChild(document.getElementById('progress-bar-template').content.cloneNode(true));
    progressBar.updateProgress = function (newPercentage) {
        shadowRoot.querySelector('.progress').setAttribute('aria-valuenow', newPercentage);
        shadowRoot.querySelector('.label').textContent = newPercentage + '%';
        shadowRoot.querySelector('.bar').style.width = newPercentage + '%';
    }
    return progressBar;
}
</script>

我们创建一个空的div用于包含我们想要的控件。然后progressBar.attachShadow({mode: 'closed'});创建了一个与div同级的ShadowRoot。然后再利用shadowRoot.appendChild(...)添加想要的控件模板。可以看到内外部样式相互之间并不污染。另外,为了利于调试,我们可以在创建Shadow DOM时配置{mode: DEBUG ? 'open' : 'closed'},这样,就可以通过progressBar.shadowRoot属性获取到ShadowRoot。

控件插槽

现在,你可能非常疑惑为什么要去建立Shadow DOM而不是直接操作CSS?事实上,在第一版的CSS作用域模型中已经对css的作用域问题@scope做出了相关支持。那么我们为什么要另起炉灶又引入一个新的机制来实现样式的作用域隔离呢?其中一个动机是我们需要将Web控件的实现内容隐藏起来,不让一个遍历API诸如querySelectorAllgetElementByTagName操作它。因为在Shadow Dom内部的节点是不能通过通用API操作到的,用户不担心自己使用的控件被自己破坏了。注意到Shadow DOM不会像iframe那样使用跨源安全机制,我们可以轻易的通过特定的API操作Shadow DOM内部的内容。另一个原因是我们需要一个基于DOM的组件解决方案,来看下面的例子:

<ul id="contacts">
    <li>
        Commit Queue
        (<a href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a>)<br>
        One Infinite Loop, Cupertino, CA 95014
    </li>
    <li>
        Niwa, Ryosuke
        (<a href="mailto:rniwa@webkit.org">rniwa@webkit.org</a>)<br>
        Two Infinite Loop, Cupertino, CA 95014
    </li>
</ul>

我们需要为联系人列表加上一些样式(译者:一般方法非常好实现。但是,注意联系人列表是控件,控件有自己的样式,用特定的class或id标注控件不是本文的目的。),像下面这样:



我们不用在js中往Shadow DOM中插入每个联系人的信息,我可以使用插槽(solt)。首先定义一个这样的模板:

<template id="contact-template">
    <style>
        :host { border: solid 1px #ccc; border-radius: 0.5rem; padding: 0.5rem; margin: 0.5rem; }
        b { display: inline-block; width: 5rem; }
    </style>
    <b>Name</b>: <slot name="fullName"><slot name="firstName"></slot> <slot name="lastName"></slot></slot><br>
    <b>Email</b>: <slot name="email">Unknown</slot><br>
    <b>Address</b>: <slot name="address">Unknown</slot>
</template>
<script>
window.addEventListener('DOMContentLoaded', function () {
    var contacts = document.getElementById('contacts').children;
    var template = document.getElementById('contact-template').content;
    for (var i = 0; i < contacts.length; i++)
        contacts[i].attachShadow({mode: 'closed'}).appendChild(template.cloneNode(true));
});
</script>

模板中运用了slot标签,外部dom中对应内容会自动插入到插槽中。我们需要改写一下联系人列表:

<ul id="contacts">
    <li>
        <span slot="fullName">Commit Queue</span>
        (<a slot="email" href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a>)<br>
        <span slot="address">One Infinite Loop, Cupertino, CA 95014</span>
    </li>
</ul>

js代码中,我们为每一个<li>创建了一个Shadow DOM,Shadow DOM的模板中定义了一些插槽如<slot name="fullName">,外部li会将对应信息插入模板的插槽中,最终生成的模板就像这样:

<ul id="contacts">
    <li>
        <!--shadow-root-start-->
            <b>Name</b>:
            <slot name="fullName">
                <!--slot-content-start-->
                    <span slot="fullName">Commit Queue</span>
                <!--slot-content-end-->
            </slot><br>
            <b>Email</b>:
            <slot name="email">
                <!--slot-content-start-->
                    <a slot="email" href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a>
                <!--slot-content-end-->
            </slot><br>
            <b>Address</b>:
            <slot name="address">
                <!--slot-content-start-->
                    <span slot="address">One Infinite Loop, Cupertino, CA 95014</span>
                <!--slot-content-end-->
            </slot>
        <!--shadow-root-end-->
    </li>
</ul>

然后该模板拥有自己的样式。最终效果如下:


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,816评论 1 92
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 173,699评论 25 708
  • 姐姐要去援疆了,心里舍不得。毕竟要远离,不能经常见面了。人还没走,心里已经是空落落的……只能躲在一隅,默默...
    蒹葭essay阅读 121评论 0 0
  • 故事前言 作者还小,所以很多不好的地方多多包涵谢谢!文章主要讲女主进入娱乐圈后的各种磨难,至于主角名字不要太在意,...
    雨下的雪i阅读 236评论 0 1
  • 自新入职来,除了上周学习适应阶段,真正开始进入工作状态的就是制作这次标书了。上周看同事做的时候还没有很强烈的参与感...
    潇湘淋毓阅读 240评论 0 1