#shadow-root: webComponent自定义元素

2,056 阅读2分钟

背景

如果你开发过小程序的话或许你看到过这样的内容

image.png

我第一次见到这个的时候是这样的

image.png

这是啥?iframe?我封装的组件咋变成这样了?其实这个是微信小程序的组件组织框架Exparser生成的,但是今天的主角并不是它,是它的亲生父亲 webComponent,两者有着高度相似的内容,或者说是Exparser是根据webComponent标准在更好的满足小程序开发需求上自行实现的一套自定义元素框架。

我们都用过video标签,你有想过为什么传入一个链接就能生成视频,还附带有暂停,播放等按钮呢?那些元素都跑哪儿去了呢?还有github上的提pr或者issue的内容框,如果你留意过该元素的话,会发现是一个textarea标签,但是我输入的所有内容都看不到的。

其实我们可以通过勾选调试工具 => 设置 => 偏好设置 => 显式用户代理的 shadow dom 来显式这些隐藏的内容。这就是神奇的webComponent组件。

image.png

image.png

WebComponent

创建

WebComponent为了区别于html标签,必须以烧烤串的形式使用

<body>
  <hello-world></hello-world>
</body>

这个hello-world组件可以通过两种形式去定义内容

js脚本形式

  <body>
    <hello-world></hello-world>
  </body>
  <script>
    // 所有的自定义组件都是继承于HTMLElement,也可以理解成必须这么写
    class HelloWorld extends HTMLElement {
      constructor() {
        super(); // 这个也是必须的
        
        // 元素的具体功能写在下面
        
        // mode 有两种模式 open 和 closed,功能是让外部能否访问到自定义元素内部
        var shadow = this.attachShadow( { mode: 'open' } )
        var container = document.createElement('div')
        container.innerText = 'hello world'
        shadow.appendChild(container)
      }
    }
    
    // 告诉浏览器`<hello-world>`元素与这个类关联
    window.customElements.define('hello-world', HelloWorld); 
  </script>

template形式

<body>
  <hello-world></hello-world>
  <!-- 使用template去定义组件的html、style、js -->
  <template id="helloworldTemplate"> 
    <!-- 样式应该封装在组件内部 -->
    <style>
     :host { /** :host 定义当前自定义元素样式 **/
       border: 1px solid red;
       height: 100px;
       display: flex;
       flex-direction: column;
       align-items: center;
       justify-content: center;
     }
     .conatiner {
       color: red;
     }
    </style>
  
    <div class="container">hello world</div>
  </template>
  <script>
    class HelloWorld extends HTMLElement {
      constructor() {
        super()
        // 脚本内容跟上面大体相似,只是不再需要去创建dom了
        var shadow = this.attachShadow( { mode: 'closed' } )
        var templateElem = document.getElementById('helloworldTemplate')
        
        // 只有通过调用templateElem.content才能获取到template中的内容
        var content = templateElem.content.cloneNode(true)
        shadow.appendChild(content)
      }
    }
    window.customElements.define('hello-world', HelloWorld);   
  </script>
</body>

只是通过template会在dom树种生成一个不会再页面显示的template。如下所示

image.png

添加事件

一个组件除了样式意外还需要交互,我们可以通过在组件内部监听事件去实现

    class HelloWorld extends HTMLElement {
      constructor() {
        super();
        var shadow = this.attachShadow( { mode: 'open' } );
        var templateElem = document.getElementById('helloworldTemplate');
        var content = templateElem.content.cloneNode(true);
        
        // 获取节点
        const dom = content.querySelector('.container')
        // 添加事件
        dom.addEventListener('click', () => {
          alert('hello world')
        })
        shadow.appendChild(content);
      }
    }
    window.customElements.define('hello-world', HelloWorld);    
  </script>

将组件封装成纯js形式

// demo.js
// 首先定义一个template
let template = document.createElement("template")
template.setAttribute('id', 'helloWorldTemplate')

// 将template的内容装进去
template.innerHTML = `
<style>
:host {
  border: 1px solid red;
  display: flex;
  flex-direction: column;
  align-items: center;
  height: 100px;
  justify-content: center;
}
.conatiner {
  color: red;
}
</style>

<div class="container">hello world</div>
`
document.body.append(template) // 到这里其实也就是之前的手动写template的操作,只是改成脚本生成罢了
// 下面就是js脚本
class HelloWorld extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow( { mode: 'open' } );
    var templateElem = document.getElementById('helloWorldTemplate');
    var content = templateElem.content.cloneNode(true);
    const dom = content.querySelector('.container')
    dom.addEventListener('click', () => {
      alert('hello world')
    })
    shadow.appendChild(content);
  }
}
window.customElements.define('hello-world', HelloWorld);  

最后在页面中引用该脚本就能自动创建自定义组件啦

<body>
  <hello-world></hello-world>
  <script src="./demo.js"></script>
</body>

参考

# 阮一峰 Web Components 入门实例教程

# MDN 使用 shadow DOM

# MDN Web Components