使用外部框架将Vue3自定义元素添加到Vue2应用程序中
我有一个用Vue2编写的应用程序,它还没有真正准备好升级到Vue3。但是,我想开始在Vue3中编写一个组件库,并将组件导入回Vue2中,最终在它准备好后进行升级。
Vue 3.2+引入了defineCustomElement
,它工作得很好,但一旦我在Vue3环境(例如Quasar)中使用附加到Vue实例的框架,它就开始在Vue2应用程序中引发错误,可能是因为defineCustomElement(SomeComponent)
的结果试图使用应该附加到应用程序的框架中的内容。
我想过扩展HTMLElement并在connectedCallback
上挂载应用程序,但后来我失去了反应性,不得不手动处理所有道具/发射/..如下所示:
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);
所以最后的问题是--有没有可能以某种方式将defineCustomElement
与附加到应用程序的框架结合起来?
解决方案
所以,经过一番挖掘,我得出了以下结论。
首先,让我们创建一个使用外部库(在我的例子中是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>
然后我们准备要构建的组件(这就是魔术发生的地方)
// 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);
现在,我们需要将其构建为库(我使用的是vite,但也应该适用于vue-cli)
// vite.config.ts
export default defineConfig({
...your config here...,
build: {
lib: {
entry: 'path/to/build.ts',
name: 'ComponentsLib',
fileName: format => `components-lib.${format}.js`
}
}
})
现在我们需要将构建的库导入到包含Vue3的上下文中,在我的例子中index.html
运行良好。
// index.html (Vue2 project)
<!DOCTYPE html>
<html lang="">
<head>
// Vue3
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
// Quasar styles
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/quasar@2.4.3/dist/quasar.prod.css" rel="stylesheet" type="text/css">
// Our built component
<script src="path/to/components-lib.umd.js"></script>
</head>
...rest of your html...
</html>
现在,我们已经准备好在我们的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>
现在,您可以在Vue2中使用Vue3组件:)
相关文章