Web Components ——创建自定义元素

三大框架都能自定义组件,也有 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 组件了。