使用外部框架将 Vue3 自定义元素集成到 Vue2 应用程序中

Jak*_*ián 3 vue.js custom-element vuejs2 quasar-framework vuejs3

我有一个用 Vue2 编写的应用程序,它还没有真正准备好升级到 Vue3。但是,我想开始在 Vue3 中编写一个组件库,并将组件导入回 Vue2,以便在准备就绪后最终进行升级。

Vue 3.2+ 引入了defineCustomElement它,它工作得很好,但是一旦我在附加到 Vue 实例的 Vue3 环境(例如 Quasar)中使用框架,它就会开始在 Vue2 应用程序中抛出错误,可能是因为尝试defineCustomElement(SomeComponent)使用框架中的某些内容的结果应该附加到应用程序中。

我考虑过扩展 HTMLElement 并安装应用程序connectedCallback,但随后我失去了反应性,必须手动处理所有 props/emits/.. ,如下所示:

class TestQuasarComponentCE extends HTMLElement {
  // get init props
  const prop1 = this.getAttribute('prop1')

  // handle changes
  // Mutation observer here probably...

  const app = createApp(TestQuasarComponent, { prop1 }).use(Quasar)
  
  app.mount(this)
}

customElements.define('test-quasar-component-ce', TestQuasarComponentCE);
Run Code Online (Sandbox Code Playgroud)

所以最后的问题是 - 是否有可能以某种方式将其defineCustomElement与附加到应用程序的框架结合起来?

Jak*_*ián 6

因此,经过一番挖掘后,我得出了以下结论。

首先,让我们创建一个使用外部库的组件(在我的例子中是 Quasar)

// SomeComponent.vue (Vue3 project)
<template>
  <div class="container">

    // This is the quasar component, it should get included in the build automatically if you use Vite/Vue-cli
    <q-input
      :model-value="message"
      filled
      rounded
      @update:model-value="$emit('update:message', $event)"
    />
  </div>
</template>

<script setup lang="ts>
defineProps({
  message: { type: String }
})

defineEmits<{
  (e: 'update:message', payload: string | number | null): void
}>()
</script>
Run Code Online (Sandbox Code Playgroud)

然后我们准备要构建的组件(这就是神奇发生的地方)

// SomeComponent.vue (Vue3 project)
<template>
  <div class="container">

    // This is the quasar component, it should get included in the build automatically if you use Vite/Vue-cli
    <q-input
      :model-value="message"
      filled
      rounded
      @update:model-value="$emit('update:message', $event)"
    />
  </div>
</template>

<script setup lang="ts>
defineProps({
  message: { type: String }
})

defineEmits<{
  (e: 'update:message', payload: string | number | null): void
}>()
</script>
Run Code Online (Sandbox Code Playgroud)

现在,我们需要将其构建为库(我使用 Vite,但也应该适用于 vue-cli)

// build.ts
import SomeComponent from 'path/to/SomeComponent.vue'
import { reactive } from 'vue'
import { Quasar } from 'quasar' // or any other external lib

const createCustomEvent = (name: string, args: any = []) => {
  return new CustomEvent(name, {
    bubbles: false,
    composed: true,
    cancelable: false,
    detail: !args.length
      ? self
      : args.length === 1
      ? args[0]
      : args
  });
};

class VueCustomComponent extends HTMLElement {
  private _def: any;
  private _props = reactive<Record<string, any>>({});
  private _numberProps: string[];

  constructor() {
    super()
    
    this._numberProps = [];
    this._def = SomeComponent;
  }
  
  // Helper function to set the props based on the element's attributes (for primitive values) or properties (for arrays & objects)
  private setAttr(attrName: string) {
    // @ts-ignore
    let val: string | number | null = this[attrName] || this.getAttribute(attrName);

    if (val !== undefined && this._numberProps.includes(attrName)) {
      val = Number(val);
    }

    this._props[attrName] = val;
  }
  
  // Mutation observer to handle attribute changes, basically two-way binding
  private connectObserver() {
    return new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        if (mutation.type === "attributes") {
          const attrName = mutation.attributeName as string;

          this.setAttr(attrName);
        }
      });
    });
  }
  
  // Make emits available at the parent element
  private createEventProxies() {
    const eventNames = this._def.emits as string[];

    if (eventNames) {
      eventNames.forEach(evName => {
        const handlerName = `on${evName[0].toUpperCase()}${evName.substring(1)}`;

        this._props[handlerName] = (...args: any[]) => {
          this.dispatchEvent(createCustomEvent(evName, args));
        };
      });
    }
  }
  
  // Create the application instance and render the component
  private createApp() {
    const self = this;

    const app = createApp({
      render() {
        return h(self._def, self._props);
      }
    })
      .use(Quasar);
      // USE ANYTHING YOU NEED HERE

    app.mount(this);
  }
  
  // Handle element being inserted into DOM
  connectedCallback() {
    const componentProps = Object.entries(SomeComponent.props);
    componentProps.forEach(([propName, propDetail]) => {
      // @ts-ignore
      if (propDetail.type === Number) {
        this._numberProps.push(propName);
      }

      this.setAttr(propName);
    });

    this.createEventProxies();
    this.createApp();
    this.connectObserver().observe(this, { attributes: true });
  }
}

// Register as custom element
customElements.define('some-component-ce', VueCustomElement);
Run Code Online (Sandbox Code Playgroud)

现在我们需要在具有 Vue3 的上下文中导入构建的库,在我的例子中index.html工作得很好。

// vite.config.ts

export default defineConfig({
  ...your config here...,
  build: {
    lib: {
      entry: 'path/to/build.ts',
      name: 'ComponentsLib',
      fileName: format => `components-lib.${format}.js`
    }
  }
})
Run Code Online (Sandbox Code Playgroud)

现在我们已经准备好在 Vue2(或任何其他)代码库中使用我们的组件,就像我们习惯的那样,进行一些小的更改,请检查下面的评论。

// App.vue (Vue2 project)
<template>
  <some-component-ce
    :message="message" // For primitive values
    :obj.prop="obj" // Notice the .prop there -> for arrays & objects
    @update:message="message = $event.detail" // Notice the .detail here
  />
</template>

<script>
  export default {
    data() {
      return {
        message: 'Some message here',
        obj: { x: 1, y: 2 },
      }
    }
  }
</script>
Run Code Online (Sandbox Code Playgroud)

现在,您可以在 Vue2 中使用 Vue3 组件了:)