Vue3中如何使用defineCustomElement定义组件

    使用 Vue 构建自定义元素

    Web Components 是一组 web 原生 API 的统称,允许开发者创建可复用的自定义元素 (custom elements)。

    使用自定义元素的主要优势在于,它们可以在任何框架甚至非框架环境中使用。当你面向的最终用户可能使用了不同的前端技术栈,或是当你希望将最终的应用与它使用的组件实现细节解耦时,它们会是理想的选择。

    Vue 和 Web Components 是互补的技术,Vue 为使用和创建自定义元素提供了出色的支持。在 Vue 应用中,你可以集成自定义元素,也可以使用 Vue 来构建和发布自定义元素。

    Vue 在 Custom Elements Everywhere 测试中取得了 100% 的分数。在 Vue 应用中使用自定义元素基本上与使用原生 HTML 元素的效果相同,但需要进行一些额外的配置才能工作:

    跳过组件解析

    默认情况下,Vue 会将任何非原生的 HTML 标签优先当作 Vue 组件处理,而将“渲染一个自定义元素”作为后备选项。这会在开发时导致 Vue 抛出一个“解析组件失败”的警告。

    • 要让 Vue 知晓特定元素应该被视为自定义元素并跳过组件解析,我们可以指定 compilerOptions.isCustomElement 这个选项,设置在此选项对象上的值将会在浏览器内进行模板编译时使用,并会影响到所配置应用的所有组件。

    • 另外也可以通过 compilerOptions 选项在每个组件的基础上覆盖这些选项(针对当前组件有更高的优先级)。

    因为它是一个编译时选项,构建工具需要将配置传递给 @vue/compiler-dom:

    • vue-loader:通过 compilerOptions loader 的选项传递。

    // vue.config.js
    module.exports = {
      chainWebpack: config => {
        config.module
          .rule('vue')
          .use('vue-loader')
          .tap(options => ({
            ...options,
            compilerOptions: {
              // 将所有带 ion- 的标签名都视为自定义元素
              isCustomElement: tag => tag.startsWith('ion-')
            }
          }))
      }
    }
    • vite:通过 @vitejs/plugin-vue 的选项传递。

    // vite.config.js
    import vue from '@vitejs/plugin-vue'
    export default {
      plugins: [
        vue({
          template: {
            compilerOptions: {
              // 将所有带短横线的标签名都视为自定义元素
              isCustomElement: (tag) => tag.includes('-')
            }
          }
        })
      ]
    }
    • 浏览器内编译时的配置。

    // src/main.js
    // 仅在浏览器内编译时才会工作
    const app = createApp(App)
    app.config.compilerOptions.isCustomElement = (tag) => tag.includes('-')

    传递 DOM 属性

    由于 DOM attribute 只能为字符串值,因此我们只能使用 DOM 对象的属性来传递复杂数据。当为自定义元素设置 props 时,Vue 3 将通过 in 操作符自动检查该属性是否已经存在于 DOM 对象上,并且在这个 key 存在时,更倾向于将值设置为一个 DOM 对象的属性。这意味着,在大多数情况下,如果自定义元素遵循推荐的最佳实践,你就不需要考虑这个问题。

    然而,也会有一些特别的情况:必须将数据以一个 DOM 对象属性的方式传递,但该自定义元素无法正确地定义/反射这个属性 (因为 in 检查失败)。在这种情况下,你可以强制使用一个 v-bind 绑定、通过 .prop 修饰符来设置该 DOM 对象的属性:

    <my-element :user.prop="{ name: &#39;jack&#39; }"></my-element>
    <!-- 等价简写 -->
    <my-element .user="{ name: &#39;jack&#39; }"></my-element>

    defineCustomElement()

    Vue 提供了一个和定义一般 Vue 组件几乎完全一致的 defineCustomElement 方法来支持创建自定义元素。defineComponent 和这个方法所接收的参数完全一致。但它会返回一个继承自 HTMLElement 的原生自定义元素类的构造器(可以通过 customElements.define() 注册)。

    function defineCustomElement(
      component:
        | (ComponentOptions & { styles?: string[] })
        | ComponentOptions[&#39;setup&#39;]
    ): {
      new (props?: object): HTMLElement
    }

    除了常规的组件选项,defineCustomElement() 还支持一个特别的选项 styles,它是一个内联 CSS 字符串的数组,所提供的 CSS 会被注入到该元素的 ShadowRoot 上。

    <my-vue-element></my-vue-element>
    import { defineCustomElement } from &#39;vue&#39;
    const MyVueElement = defineCustomElement({
      // 这里是同平常一样的 Vue 组件选项
      props: {},
      emits: {},
      template: `...`,
      // defineCustomElement 特有的:注入进 ShadowRoot 的 CSS
      styles: [`/* css */`]
    })
    // 注册自定义元素之后,所有此页面中的 `<my-vue-element>` 标签都会被升级
    customElements.define(&#39;my-vue-element&#39;, MyVueElement)
    // 也可以在注册之后实例化元素:
    document.body.appendChild(
      new MyVueElement({
        // 初始化 props(可选)
      })
    )

    如果这时控制台报错:\color{red}{如果这时控制台报错:}如果这时控制台报错:Component provided template option but runtime compilation is not supported,在 vite.config.js 中添加以下配置:

    resolve: { alias: { &#39;vue&#39;: &#39;vue/dist/vue.esm-bundler.js&#39; } },

    生命周期

    • 当该元素的 connectedCallback 初次调用时,一个 Vue 自定义元素会在内部挂载一个 Vue 组件实例到它的 ShadowRoot 上。

    • 当此元素的 disconnectedCallback 被调用时,Vue 会在一个微任务后检查元素是否还留在文档中。

      • 如果元素仍然在文档中,那么说明它是一次移动操作,组件实例将被保留;

      • 如果该元素不再存在于文档中,那么说明这是一次移除操作,组件实例将被销毁。

    Props

    • 所有在 props 选项中声明的 props 都会被定义为自定义元素的属性。Vue 能够自动地适当处理它是否反射为属性或属性。

    • attribute 总是根据需要反射为相应的属性类型。原话已经非常清晰明了,可以直接重述。重新表述如下: 基本数据类型的属性值(例如字符串、布尔值或数字)可以通过反射反映为属性。

    • 当它们被设为 attribute 时 (永远是字符串),Vue 也会自动将以 Boolean 或 Number 类型声明的 prop 转换为所期望的类型。比如下面这样的 props 声明:

    props: {
      selected: Boolean,
      index: Number
    }

    并以下面这样的方式使用自定义元素:

    <my-element selected index="1"></my-element>

    在组件中,selected 会被转换为 true (boolean 类型值) 而 index 会被转换为 1 (number 类型值)。

    事件

    • emit 触发的事件都会通过以 CustomEvents 的形式从自定义元素上派发。

    • 额外的事件参数 (payload) 将会被暴露为 CustomEvent 对象上的一个 detail 数组。

    插槽

    • 在一个组件中,插槽将会照常使用 渲染。但是,使用最终元素时,只能使用原生插槽的语法,无法使用作用域插槽。

    • 当传递具名插槽时,应使用 slot attribute 而不是 v-slot 指令:

    <my-element>
      <div slot="named">hello</div>
    </my-element>

    依赖注入

    • Provide / Inject API 和相应的组合式 API 在 Vue 定义的自定义元素中都可以正常工作。

    • 但是,依赖关系只在自定义元素之间起作用。一个由常规 Vue 组件提供的属性无法被注入到由 Vue 定义的自定义元素中。

    将 SFC 编译为自定义元素

    defineCustomElement 也可以搭配 Vue 单文件组件 (SFC) 使用。但是,根据默认的工具链配置,SFC 中的