Web Components ——创建自定义元素

Table of Contents

三大框架都能自定义组件,也有 css 隔离方案。但组件不通用,css 隔离性也不是很好。如果要加载一段未知的 html,这段 html 里还包含了 style 或者 link 标签,框架自带的组件方案就不好解决了。常用的解决方案是 iframe。iframe 的隔离性非常好,但交互不方便,总是通过 postMessage 也比较麻烦。如果放弃 ie 的话,还可以使用浏览器原生提供的 Web Components 功能。

Web Components 可以让用户自定义元素,css 隔离性也较好,交互也方便。

自定义元素

比如我定义了一个 dict-content 的元素。

export default class DictContent extends HTMLElement {
  static get observedAttributes() { return ['text'] }

  constructor() {
    super()
    const shadowRoot = this.attachShadow({ mode: 'open' })
    shadowRoot.innerHTML = ``
  }

  get text() {
    return this.getAttribute('text' ||'')
  }

  set text(val) {
    this.setAttribute('text', val)
  }

  connectedCallback() {
    this.text = this.text || ''
  }

  attributeChangedCallback (name, oldValue, newValue) {
    if (name === 'text') {
      this.shadowRoot.innerHTML = newValue
      setTimeout(() => {
        const els = this.shadowRoot.querySelectorAll('a')
        ;[].map.call(els, el => {
          if (el.attributes.href?.value?.startsWith('entry://')) {
            let entry = el.attributes.href.value.replace('entry://', '')
            el.setAttribute('href', 'javascript: void(0)')
            el.onclick = () => {
              this.dispatchEvent(new CustomEvent('click-entry', {
                detail: {
                  entry
                }
              }))
              console.log(entry)
            }
          }
        })
      }, 0)
    }
  }
}

if(!customElements.get('dict-content')){
  customElements.define('dict-content', DictContent);
}

自定义元素命名必须包含连字符,内部有 dom 就需要使用 shadowRoot。事件可以通过 dispatchEvent 和 CustomEvent 实现。

参照 mdn 文档(Web Components | MDN (mozilla.org))和已有的库(XboxYan/xy-ui: 🎨面向未来的原生 web components UI组件库 (github.com))很容易实现一个自定义元素。

vue3 里使用

如果要在 vue3 里使用自定义元素,需要告诉 vue 哪些标签是自定义元素(不设置也没关系,vue3 会自动回退)。如果是 script 引入的 vue3,添加如下代码就可以:

app.config.compilerOptions.isCustomElement = tag => ['dict-content'].includes(tag)

如果是 webpack,并且使用的是 runtimeOnly 版本(默认就是),需要在 vue.config.js 里配置:

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => ({
        ...options,
        compilerOptions: {
          // 排除自定义元素处理
          isCustomElement: tag => ['dict-content'].includes(tag)
        }
      }))
  }
}

设置以后 vue 就不会把自定义的原生元素当成 vue 组件了。