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 组件了。