最佳实践:Vue 3 中将组件附加到 DOM 的方法
<p>我想在我的Vue 3应用程序中动态创建一个组件,该组件在一个单文件组件(SFC)中,并将其附加到DOM中。我正在使用<code><script setup></code>风格的组件,这是另一个问题。</p>
<p>这似乎是不必要的困难。</p>
<p>以下大致是我想要做的事情:</p>
<ol>
<li>获取一些数据。已经完成。</li>
<li>创建一个Vue组件的实例:Foo.vue。</li>
<li>将数据作为属性传递给它。</li>
<li>将它附加到我想要的位置。</li>
</ol>
<p>问题是,我不能在模板中使用<component :is="Foo:>,因为在模板渲染之后很久之后,我不知道它将在哪里。</p>
<p>有没有最佳实践?有没有善心人士可以提供一个简单的例子,我会非常感激。</p>
<p>我有时无法理解Vue文档的一半时间。抱歉,不想这么说,但对于Vue的新手来说,它们相当晦涩,让我感到很愚蠢。</p>
<p>以下是一些假代码,说明我想要做的事情:</p>
<pre class="brush:php;toolbar:false;">import Foo from "../components/Foo.vue"
function makeAFoo(p, data){
// 实例化我的Foo.vue(不确定如何在内联中实现),并将其传递所需的数据
let foo = new Foo(data); // 如果只有这么简单,对吧?
// 将其附加到p(这是一个HTML元素)
p.appendChild(foo)
}</pre>
<p><br /></p>
更简单的方法是使用v-if或v-for。
与其直接处理组件,不如处理组件的状态,让Vue的响应性魔法发挥作用
这是一个示例,动态添加一个组件(Toast),只需操作组件的状态
Toast.vue文件:这里的v-for是响应式的,每当向errors对象添加新的错误时,它将被渲染
<script setup lang="ts"> import { watch } from 'vue'; import { ref, onUpdated } from 'vue'; import { Toast } from 'bootstrap'; const props = defineProps({ errors: { type: Array, default: () => [] }, }); onUpdated(() => { const hiddenToasts = props.errors.filter((obj) => { return obj.show != true; }); hiddenToasts.forEach(function (error) { var errorToast = document.getElementById(error.id); var toast = new Toast(errorToast); toast.show(); error.show = true; errorToast.addEventListener('hidden.bs.toast', function () { const indexOfObject = props.errors.findIndex((item) => { return item.id === error.id; }); if (indexOfObject !== -1) { props.errors.splice(indexOfObject, 1); } }); }); }); </script> <script lang="ts"> const TOASTS_MAX = 5; export function push(array: Array, data): Array { if (array.length == TOASTS_MAX) { array.shift(); array.push(data); } else { array.push(data); } } </script> <template> <div ref="container" class="position-fixed bottom-0 end-0 p-3" style="z-index: 11" > <div v-for="item in errors" v-bind:id="item.id" class="toast fade opacity-75 bg-danger" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="15000" > <div class="toast-header bg-danger"> <strong class="me-auto text-white">Error</strong> <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close" ></button> </div> <div class="toast-body text-white error-body">{{ item.msg }}</div> </div> </div> </template>ErrorTrigger.vue:每当发生点击事件时,我们向errors对象推送一个错误
<script setup lang="ts"> import { ref, reactive } from 'vue'; import toast from './Toast.vue'; import { push } from './Toast.vue'; const count = ref(0); const state = reactive({ errors: [] }); function pushError(id: int) { push(state.errors, { id: id, msg: 'Error message ' + id }); } </script> <template> <toast :errors="state.errors"></toast> <button type="button" @click="pushError('toast' + count++)"> Error trigger: {{ count }} </button> </template>完整示例: https://stackblitz.com/edit/vitejs-vite-mcjgkl
选项1:使用
createVNode(component, props)和render(vnode, container)创建:使用
createVNode()创建一个带有props的组件定义的VNode(例如,从*.vue导入的SFC),可以将其传递给render()在给定的容器元素上渲染。销毁:调用
render(null, container)会销毁附加到容器的VNode。当父组件卸载时(通过unmounted生命周期钩子)应该调用此方法进行清理。// renderComponent.js import { createVNode, render } from 'vue' export default function renderComponent({ el, component, props, appContext }) { let vnode = createVNode(component, props) vnode.appContext = { ...appContext } render(vnode, el) return () => { // destroy vnode render(null, el) vnode = undefined } }注意:此方法依赖于内部方法(
createVNode和render),这些方法在将来的版本中可能会进行重构或删除。演示1
选项2:使用
createApp(component, props)和app.mount(container)创建:使用
createApp()创建一个应用实例。该实例具有mount()方法,可以用于在给定的容器元素上渲染组件。销毁:应用实例具有
unmount()方法来销毁应用和组件实例。当父组件卸载时(通过unmounted生命周期钩子)应该调用此方法进行清理。// renderComponent.js import { createApp } from 'vue' export default function renderComponent({ el, component, props, appContext }) { let app = createApp(component, props) Object.assign(app._context, appContext) // 必须使用Object.assign在_context上 app.mount(el) return () => { // 销毁app/component app?.unmount() app = undefined } }注意:此方法为每个组件创建一个应用实例,如果需要在文档中同时实例化多个组件,可能会产生不小的开销。
演示2
示例用法
<script setup> import { ref, onUnmounted, getCurrentInstance } from 'vue' import renderComponent from './renderComponent' const { appContext } = getCurrentInstance() const container = ref() let counter = 1 let destroyComp = null onUnmounted(() => destroyComp?.()) const insert = async () => { destroyComp?.() destroyComp = renderComponent({ el: container.value, component: (await import('@/components/HelloWorld.vue')).default props: { key: counter, msg: 'Message ' + counter++, }, appContext, }) } </script> <template> <button @click="insert">插入组件</button> <div ref="container"></div> </template>