Shadow DOM

本文深入探讨了ShadowDOM的概念及其在Web开发中的应用。介绍了如何通过ShadowDOM实现内容与表现的分离,增强样式的局部化,并提供了丰富的示例代码。此外,还讲解了宿主样式、主题化、分布节点等相关概念。

Shadow DOM

ShadowDOm是一个HTML的新规范,其允许开发者封装自己的HTML标签、CSS样式和JavaScript代码。这些新标签和相关的API被称为WebComponents。

当HTML转化成DOM时,每个元素都会变成一个节点(node)。而一组互相嵌套的一组节点被称为节点树(node tree)。

要创建一个影子树,首先要指定一个节点担任影子宿主(shadow host),然后给影子素数添加一个称作影子根(shadow root)新节点。影子根作为影子树的第一个节点,其他的节点都是它的子节点。

一个简单的例子
<body>
    <div class="widget">Hello, world!</div>
    <script>
        var host = document.querySelector(".widget");
        var root = host.createShadowRoot();
        var header = document.createElement('h1');
        header.textContent = '一只野生的影子标题出现了!';

        root.appendChild(header)
    </script>
</body>
content标签
<body>
    <div class="box">
        <div class="name">付鑫</div>
        <div class="sex"></div>
        <div class="hobby">电影</div>
        <p>我专注前端开发(HTML/CSS/JavaScript),还会一点 Node 和 Ruby。</p>
        <p>从事写作,偶尔也会写点博客。</p>
        <p>尽管我是南方人,但我最近都在美丽的旧金山工作和生活。</p>
    </div>

    <template class="box-template">
        <dl>
            <dt>名字</dt>
            <dd>
                <content select=".name"></content>
            </dd>
            <dt>性别</dt>
            <dd>
                <content select=".sex"></content>
            </dd>
            <dt>年龄</dt>
            <dd>
                <content select=".age">12</content>
            </dd>
            <dt>爱好</dt>
            <dd>
                <content select=".hobby"></content>
            </dd>
        </dl>
        <!--<p><content></content></p>-->
        <p><content select=""></content></p>
    </template>

    <script type="text/javascript">
        var box = document.querySelector('.box');
        var root = box.createShadowRoot();
        var boxTemplate = document.querySelector('.box-template');
        root.appendChild(boxTemplate.content);
    </script>
</body>

这种做法可以将内容和实现分离,让我们能更加灵活的处理页面的呈现。

要想引用影子宿主里的内容,我们首先需要使用一个新的标签— <content>标签,创建一个插入点(insertion point),将影子宿主的内容展示出来。
插入点允许我们在不改变源代码的情况下改变渲染顺序,这也意味着我们可以对要呈现的内容进行选择。

使用<template>标签令shadow DOM的使用过程更加简单。

<template>标签相关资料可查看:

  1. HTML5 < template >标签元素简介
  2. MDN- < template >
Select属性

上文例子中我们创建了一个非常简单的简历组件。因为每个定义的字段都需要特定的内容,我们必须告诉<content>标签有选择性的插入内容。为了做到这一点,我们使用select属性。select属性使用CSS选择器来选取想要展示的内容。

举例来说,<content select=".name">会在影子宿主里寻找任何样式名称为.name的元素。如果找到一个匹配的元素,就会将这个元素渲染到shadowDOM中对应的<content>标签中去。

贪心插入点(Greedy Insection Points)

在例子中,在.box-template模板的最后,我们有一个<content>标签。他的select属性值为空。

 <p><content select=""></content></p>

这种被称为通配符选择器(wildcard selection),其可以抓取影子宿主中剩余的全部内容。以下三种选择器是完全相等的。

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

Shadow Boundary

影子边界(shadow boundary)表示分离常规 DOM (与影子 DOM 相对立的“光明” DOM)与 shadow DOM 的壁障。影子边界的主要好处就是防止主 DOM 中的样式泄露到 shadow DOM 中。这就意味着即使你在主文档中有一个针对全部
<h3>标签的样式选择器,这个样式也不会不经你的允许便影响到 shadow DOM 的元素。

<body>
    <style>
        button {
            font-size: 18px;
            font-family: '华文行楷';
        }   
    </style>
    <button>
        我是一个普通的按钮
    </button>
    <div></div>

    <script type="text/javascript">
        var host = document.querySelector('div');
        var root = host.createShadowRoot();
        root.innerHTML = '<style>button { font-size: 24px; color: blue; } </style>' +
                 '<button>我是一个影子按钮</button>';
    </script>
</body>

由于影子边界的存在,第二个按钮忽略掉按钮的的样式并使用自己的样式。并且原文档中的按钮还保持了默认的显示样式。

这种作用域化(scoping)的特性实在是非常的“额妹子嘤”。我们折腾了这么些年样式表,它的选择范围似乎越来越大。你越来越难以向一个项目中添加新的样式,因为你担心不小心把页面中的那一块搞崩掉。Shadow DOM 提供给我们的样式边界意味着我们终于可以开始用一种更加局部的、特定组件化的方式来考虑和编写我们的 CSS。

宿主样式 (:host)

给影子宿主设置样式,可以使用:host
:host的选择器优先级被设定为低于页面选择器的优先级,所以如果有需要的话它可以轻松被页面重写样式

宿主样式中的类型选择器

由于:host是伪类选择器(Pseudo Selector),我们可以将其应用于多个标签上来改变组件的外观。

:host(p){
    color: blue
}
:host(div){
    color: green
}

可以利用:host选择器来改变组件的某一个特定标签样式,还可以根据类名、ID、属性等来进行匹配选择–任何有效的CSS选择器都可以正常工作。

使用*选择器,可以创造应用于全部:host元素的默认样式。

主题化

使用:host-context()可以基于内容元素修改组件的外观。实现.parent<.child类似父类选择器的效果,当然这仅限于使用shadow DOM。

<div class="serious">
    <p class="serious-widget">大家好我很严肃</p>
</div>

<div class="playful">
    <p class="playful-widget">漂亮的小云彩效果</p>
</div>

<template class="shadow-template">
    <style type="text/css">
        :host-context(.serious) {
            width: 250px;
            height: 50px;
            padding: 50px;
            font-weight: bold;
            font-size: 24px;
            color: black;
            background: tomato;
        }

        :host-context(.playful) {
            width: 250px;
            height: 50px;
            padding: 50px;
            font-weight: bold;
            font-size: 24px;
            color: #FFF;
            background: deepskyblue;
        }
        ::content {
            color: firebrick;
        }
    </style>
    <content select=""></content>
</template>

<script type="text/javascript">
    var root1 = document.querySelector('.serious-widget').createShadowRoot();
    var root2 = document.querySelector('.playful-widget').createShadowRoot();
    var template = document.querySelector('.shadow-template');
    root1.appendChild(document.importNode(template.content, true));
    root2.appendChild(document.importNode(template.content, true));
</script>
宿主样式状态

:host标签最好用的地方之一是设置状态的样式,如:hover:active

template模板标签的特性:

  1. 惰性:在使用前不会被渲染;
  2. 无副作用:在使用前,模板内的各种脚本不会运行、图像不会加载等;
  3. 内容不可见:模板的内容不存在于文档中,使用选择器无法获取;
  4. 可被放置于任意位置:即使是HTML解析器不允许出现的位置,例如作为<select>的子元素。

:host-context的作用:这个选择器的实际用法是用来选择影子宿主的祖先元素,最大的用处大概就是主体的设置。

分布节点

使用shadow DOM时应确保表现与内容分离。来自页面并通过<content>标签添加到shadow DOM的内容被称为分布节点

分布节点的样式渲染需要用到::content伪类选择器。可以让影子模板中的按钮与出现在<content>标签中的按钮拥有不同的样式。

::content> p {
    font-size: 20px;
    color: hotpink;
}
::shadow

使用::shadow伪类选择器可以赋予我们重写默认定义的自由,可以打破影子边界的壁垒。

/deep/

使用::shadow选择器的缺陷是他只能穿透一层影子边界。如果在一个影子树中嵌套了多个影子树,那么使用/deep/组合符更为简便。

JavaScript
事件重定向(Event Retargeting)

Shadow DOM里的js与传统js一个真正不同的点在于事件调度(event dispatching)原来绑定在shadow DOM节点中的事件被重定向了,所以他们看起来像绑定在影子宿主上一样

影子节点上的事件必须重定向,否则这将破坏封装性

分布式节点

分布节点来自原有 DOM 结构,而用户是可以操作原有 DOM 结构的。对它进行事件重定向没啥必要,而且事实上你也不想让它重定向。如果一个使用者给你提供一个按钮来对 Shadow DOM 设置样式,有时候他们可能会希望监听它的 click 事件。

被阻塞的事件(Blocked Events)

有些情况下事件绑定不进行重定向而直接被干掉。以下事件会被阻塞到根节点且不会被原有DOM结构监听到:

  • abort
  • error
  • select
  • change
  • load
  • reset
  • resize
  • scroll
  • selectstart

webcomponent.js在页面组件层面主要实现了三个内容:自定义元素、HTML引入以及Shadow DOM。

参考资料

  1. Shadow DOM:简介
  2. Shadow DOM:基础
  3. Shadow DOM:样式
  4. Shadow DOM:样式(续)
  5. Shadow DOM:JavaScript
  6. HTML template 标签
  7. Shadow DOM 201
### 如何在 Shadow DOM 中使用 Iconfont 字体 #### 创建自定义元素并附加 Shadow DOM 为了使 `iconfont` 字体能够在 Shadow DOM 内正常工作,首先需要创建一个带有 Shadow DOM 的 Web 组件。通过 JavaScript 定义一个新的类继承自 HTMLElement 并注册该组件。 ```javascript class MyElement extends HTMLElement { constructor() { super(); const shadowRoot = this.attachShadow({ mode: 'open' }); // 将外部样式表引入到 Shadow DOM 中 const linkElem = document.createElement('link'); linkElem.setAttribute('rel', 'stylesheet'); link Elem.href = '//at.alicdn.com/t/font_123456.css'; // 替换成实际的 iconfont URL shadowRoot.appendChild(linkElem); // 构建内部 HTML 结构 const wrapper = document.createElement('span'); wrapper.className = "my-icon"; wrapper.innerHTML = '<i class="iconfont icon-example"></i>'; shadowRoot.appendChild(wrapper); } } customElements.define('my-element', MyElement); ``` 上述代码片段展示了如何构建一个简单的 Web Component,在其内部加载来自阿里云平台上的 `iconfont` 资源,并将其应用至指定图标的显示上[^3]。 #### 解决潜在兼容性和样式隔离问题 由于 Shadow DOM 提供了良好的封装机制,默认情况下会阻止父级页面中的全局样式影响子树内的节点;因此,对于某些老旧版本浏览器(如 IE),可能无法直接识别 `<style>` 或者 `<link rel="stylesheet">` 标签所指向的内容。针对这种情况,可以通过动态插入内联样式的手段来规避这一限制: ```css /* 添加到 Shadow Root */ <style> .iconfont:before { speak: none; font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } </style> ``` 另外需要注意的是,尽管 `iconfont` 支持较好的跨浏览器表现力,但对于多色图标的支持仍然有限——这取决于具体实现方法以及目标环境特性。如果项目中有这方面的需求,则推荐考虑采用其他形式的技术方案,比如基于 SVG Sprites 实现[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值