Create: March 2, 2022
Last Modify: Mar 2, 2022

Web Component 踩坑记

以下是一个简单的 web component

<demo-component></demo-component>

<template id='demo-component'>
  <style></style>
  <div>
    Web Component
  </div>
</template>

<script>
  window.customElements.define('demo-component', class extends HTMLElement {
  constructor() {
    super()
    
    const shadow = this.attachShadow({mode: 'closed'})
    const target = document.getElementById('demo-component')
    const content = target.content.cloneNode(true)

    shadow.appendChild(content)
  }
})
</script>

但是通常在实际场景中,作为 component 肯定是要可抽象复用,由此带来的一个场景是我们在复用 components 的同时,也要通过一个接口(例如 React props)传入数据来显示不同数据。官方也提供了两种不同的方式 attribute & slot

以下是通过 attribute 实现,引用 component 时通过设置 attribute 方式传递数据,在实例化类中通过 this.getAttributes 获取设置的 attribute,带来的问题是会将数据都放在标签属性上。如果是数据是一个列表,则需要 decodeURI/encodeURI,过长的 attribute 对页面也不太友好

<demo-component demo-attr='Web Component'></demo-component>

<template id='demo-component'>
<style></style>
<div id='demo-attr'></div>
</template>

<script>
  window.customElements.define('demo-component', class extends HTMLElement {
  constructor() {
    super()
    const shadow = this.attachShadow({mode: 'closed'})
    const target = document.getElementById('demo-component')
    const content = target.content.cloneNode(true)

    this.setAttr(content)

    shadow.appendChild(content)
  }

  setAttr(content) {
    const attr = this.getAttributes('demo-attr')
    content.querySelector(`#demo-attr`).innerText = attr
  }
})
</script>

另一种方式是通过 slot 插槽来控制 component 内部;但是这种方式引出的问题更多 最重要的一点是让 shadow DOM “失效”了,在外部能直接对传入的 slot 进行操作(当然也可能是官方故意为之),这样就失去了使用web component 的初衷; 其次 web component 专属的 ::slotted 选择器的权重比通配符(*)还要低,在外部设置的样式能直接更改 slot 自身的样式; 此外如果slot还存在子元素,子元素的样式没办法通过 ::slotted 选择器来控制(https://github.com/WICG/webcomponents/issues/594)

<style>
  * {
    color: #38f;
  }
</style>

<demo-component>
<p slot='slot1'>
  Web Component
  <span>SLOT</span>
</p>
</demo-component>

<template id='demo-component'>
<style>
  ::slotted(p[slot=slot1]) {
    color: #f40;
  }
</style>
<div>
  <slot name='slot1'></slot>
</div>
</template>

<script>
  window.customElements.define('demo-component', class extends HTMLElement {
    constructor() {
      super()
      const shadow = this.attachShadow({mode: 'closed'})
      const target = document.getElementById('demo-component')
      const content = target.content.cloneNode(true)

      shadow.appendChild(content)
    }
  })
</script>

基于以上原因,实现 slotpro

<demo-component>
  <div slotpro='slotpro-demo'>
    <p>
      Web Component
      <span>SLOTPRO</span>
    </p>
  </div>
</demo-component>

<template id='demo-component'>
<style></style>  
<div>
  <slotpro name='slotpro-demo'></slotpro>
</div>
</template>

<script>
  window.customElements.define('demo-component', class extends HTMLElement {
    constructor() {
      super()
      const shadow = this.attachShadow({mode: 'closed'})
      const target = document.getElementById('demo-component')
      const content = target.content.cloneNode(true)

      this.init(content)

      shadow.appendChild(content)
    }

    init(content) {
      [...content.querySelectorAll('slotpro')].forEach(item => {
        const name = item.getAttribute('name')
        const slotValue = [...this.querySelectorAll(`*[slotpro=${name}]`)]

        item.outerHTMl = slotValue.reduce((dom, next) => {
            dom.insertAdjacentElement('beforeend', next)
            return dom
          }, document.createElement('div')).innerHTML
      })
    }
  })
</script>

© 2019