什么是Shadow DOM?

Shadow DOM(影子 DOM),类似iframe 一个独立DOM容器,可以封装子元素,里面的样式与全局样式不会相互污染. 与iframe不同的是共用一个windows空间. JS事件又能互通的.ShadowDOM是WebCompnent组件重要的一部分.

ShadowDOM起到样式和脚本隔离.

先看例子,继续之前的button组件.

<style>
    button{
        color:red;
        font-size: 18px;
    }
</style>
<button>原生按扭</button>
<my-button>这是一个按扭</my-button>
<script>
    class MyButton extends HTMLElement{
        constructor(){
            super();
        }
        connectedCallback() {
            this.innerHTML=`
            <style>
                button{
                    background-color: #000;
                    color: #fff;
                    border: 0;
                    padding: 10px 20px;
                    border-radius: 5px;
                }
            </style>
            <button>${this.innerHTML}</button>
            `;
        }
    }
    customElements.define('my-button', MyButton);
</script>

image-20230814175451472

可以看到,自定义button和原生button 样式都相互污染.

在来看一样JS,外面有段按扭监听事件,会怎么样?

let btns = document.querySelectorAll('button');
// 监听事件
btns.forEach(btn => {
  btn.addEventListener('click', function(e){
      console.log("点击了",e.target.textContent);
  });
})

结果

image-20230814180627809

组件的核心就是封装,复用和隔离.现在样式和JS都没做到隔离.怎么办?

那么引出了ShadowDOM. 上面结果稍微修改一下.

class MyButton extends HTMLElement{
  constructor(){
      super();
      // 启动用shadow
      this.attachShadow({mode: 'open'});
  }
  connectedCallback() {
      // 在影子DOM中插入内容
      this.shadowRoot.innerHTML=`
      <style>
          button{
              background-color: #000;
              color: #fff;
              border: 0;
              padding: 10px 20px;
              border-radius: 5px;
          }
      </style>
      <button>${this.innerHTML}</button>
      `;
  }
}

上面的样式和JS问题都解决了. 再看一下生成的HTML结构.

image-20230814181332802

核心,两句代码:

//  启用shadow,连续到customElement自定义组件中
this.attachShadow({mode: 'open'});
// 操作shadow 根节点
this.shadowRoot...

ShadowDOM结构?

ShaowDOM和常规DOM一样.也包含DOM树.

img

这是官方的图.

意思:

  1. shadow dom 树与 dom 树一样
  2. shadow dom 有独立的空间
  3. dom 树中的 shodow host === shadow root,就是shadow 容器.
  4. shadow 挂载(关联) 在 shodow host 上.

比如上面的my-button 的结构如下.

image-20230815004615933

几个关键点:

  1. 挂载点
  2. 根节点
  3. slot插槽

可以看到外面的样式可以影响到里面,但shadow里面不能反向修改外面的样式.shadow DOM 对组件起到很大封装,隔离的作用.

内置shadow元素

其实shadow DOM不是什么新鲜的玩意,我们经常用到. 一些浏览器默认的元素就是通过shadow DOM 实现的封装.

比如: <input>,<select>,<textarea>,<video>,<progress>,<details>等就是封装了Shadow DOM.

chrome 打开设置,"user-agent shadow DOM" 配置开关就能看到内置shadow DOM里面的结构.

image-20230815010228523

input结构

image-20230815010612165

select,textarea

image-20230815010756119

video

video 比较复杂,嵌套了input range 进度条.

image-20230815011135761

details

展开和隐藏更多, 多个命名slot. 内嵌样式.:host选择器

image-20230815011843137

创建Shadow DOM

参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_shadow_DOM

基本用法:

把一个shadow 绑定到elementRef 元素上. 使其拥有shadow 特性.

let shadow = elementRef.attachShadow({ mode: "open" });
let shadow = elementRef.attachShadow({ mode: "closed" });

mode参数: openclosed 差别不大,不建议用closed.

  • open: 外面可以直接访问shadow里面的内容.
  • closed: 外面不可以使用shadow,获取里面的元素.

获取shadow 内容,通常使用:

elementRef.shadowRoot

当mode 设为"closed" ,elementRef.shadowRootnull.

为什么不建议用closed呢? 因为js 没有有私有属性.其他人可以通过其他方式绕过限制 ,访问root或修改mode. 不如直接设置open方便调试.

类似浏览器自带的input 为什么不能访问到shadow DOM呢?因为他使用的useragent 代理访问. 属性第三情况.

这就涉及到elementRef,哪些元素不支持attachShadow. 系统不自带的shadow,并且不能直接访问.也不能重新关联.

哪些能用shadow DOM?

  • 任何带有有效的名称且可独立存在的(autonomous)自定义元素
  • <article>
  • <aside>
  • <blockquote>
  • <body>
  • <div>
  • <footer>
  • <h1>
  • <h2>
  • <h3>
  • <h4>
  • <h5>
  • <h6>
  • <header>
  • <main>
  • <nav>
  • <p>
  • <section>
  • <span>

例子:

const div=document.createElement("div");
// 创建shadow 并关联div,成功返回shadowRoot
const shadowRoot=div.attachShadow({ mode: "closed" });
// 不能重复创建shadow
// div.attachShadow({ mode: "closed" });
// mode:closed 时,div.shadowRoot 返回空
shadowRoot.innerHTML=`
<style>
    :host{
        color:red;
    }
</style>
<h1>Hello World</h1>
`
document.body.appendChild(div);
// 内部可以访问
console.log(shadowRoot?.querySelector("h1")); 
// 外部不能访问 
console.log(div.shadowRoot?.querySelector("h1"));

安全设置

为了隐藏全局变量const shadowRoot,可以使用闭包函数

(function(){
  const div=document.createElement("div");
  const shadowRoot=div.attachShadow({ mode: "closed" });
  shadowRoot.innerHTML=`
<style>
    :host{
        color:red;
    }
</style>
<h1>Hello World</h1>
`
document.body.appendChild(div);
})()

你以为现在就安全啦?

attachShadow 函数修改. 现在mode配置失效.

HTMLDivElement.prototype._attachShadow = HTMLDivElement.prototype.attachShadow;
HTMLDivElement.prototype.attachShadow = function () {
    // 强制 改mod:open
    return this._attachShadow( { mode: "open" } );
};

通过删除shadowRoot属性,防止外面直接访问

// 删除shadowRoot属性
Object.defineProperty(div, "shadowRoot", {
    enumerable:false,
    writable:false,
    value:null,
    configurable:false
})

破坏者,但破坏者可禁用 Object.defineProperty 使用

// 禁用配置属性
Object._defineProperty=Object.defineProperty;
Object.defineProperty=function(obj,prop,conf){
    if(obj instanceof HTMLDivElement){
        return ;
    }
    return Object._defineProperty(obj,prop,conf);
};
// 不允许别人修改
Object.freeze(Object)

所以,之前说closed 只能防君子,不能防小人.还影响调试. 无解.

Shadow DOM 常用API

  • Element.attachShadow(): 创建
  • ShadowRoot.innerHTML: 获取/设置内容
  • ShadowRoot.querySelector() : 获取
  • ShadowRoot.host: 获取宿主.
  • ShadowRoot.mode: 获取mode模式,open 或 closed
  • ShadowRoot.adoptedStyleSheets: 将外部样式表(CSSStyleSheet)引入到 Shadow DOM 内,实现样式的隔离和共享。后面细节样式.
const shadowRoot = element.shadowRoot;
const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync('p { color: blue; }');
shadowRoot.adoptedStyleSheets = [styleSheet];
  • ShadowRoot.styleSheets: 获取样式
  • ShadowRoot.activeElement: 获取焦点所在的元素. document.activeElement是一样的
  • DocumentOrShadowRoot.getSelection: 光标位置
  • ShadowRoot.slotAssignment : slot 分配方式. slot 节解.

专题大纲

  1. Web Component #1 - 自定义组件简介
  2. Web Component #2 - 自定义元素
  3. Web Component #3 - Shadow DOM(影子 DOM)
  4. Web Component #4 - template模板和Slot插槽
  5. Web Component #5 - 样式
  6. Web Component #6 - 生命周期
  7. Web Component #7 - 属性getter/setter
  8. Web Component #8 - 自定义事件
  9. Web Component #9 - 组件module封装
  10. Web Component #10 - Form表单组件
  11. Web Component #11 - Seo优化
  12. Web Component #12 - 优秀组件库推荐

原作者:阿金
本文地址:https://hi-arkin.com/archives/web-components-3.html

标签: web components shadow DOM

(本篇完)

评论