内容有些啰嗦,内容记载了当时遇到了bug以及解决问题的思路。
业务场景简述:
前端做配置化组件,通过url内的唯一标识,请求后端sql 哪取页面配置信息,前端通过配置信息动态渲染查询表单,导出、表格,toolbar以及动态弹窗;该动态渲染组件的功能,就是渲染的toolbar内的按钮,类型为自定义弹窗,弹窗路径为后端配置数据,前端通过点击该按钮,打开指定路径下的弹窗组件。
之前用的vue2的动态挂载组件,也没见vue3那么麻烦,官网上的例子以及网上的所以例子都是前端知道要导哪些组件,然后程序进入就直接导进来了,只是写了逻辑动态切换。我这个不一样,需要挂载的组件是未知的。所以实现起来有点吃力。
一、基础的动态引入组件:
简单的动态引入的意思是,前端知道要引入哪些组件,将多个组件引入到父组件中,但不渲染它,满足一定条件后,才去在某个位置渲染指定的组件。
<template>
<custom-modal ref="custom"></custom-modal>
</template>
<script>
import {
reactive,
ref,
shallowReactive,
onActivated,
defineAsyncComponent,
} from 'vue';
const customModal = defineAsyncComponent(() => import('./modal/CustomModal.vue'));
const custom = ref();
</script>
以上的例子就是通过vue的defineAsyncComponent
实现挂载组件,并赋值给customModal
,模板中可以直接使用<custom-modal>
作为标签使用,也可以将它赋值给component中的is属性,is属性执向一个变量,可通过业务逻辑动态,更改该变量的值,就可以实现多个组件进行来回的渲染了
<template>
<component :is="componentKey" ref="custom"></component>
</template>
import {
reactive,
ref,
shallowReactive,
onActivated,
defineAsyncComponent,
} from 'vue';
const componentKey = ref(null);
const components: any = shallowReactive({});
const customModal = defineAsyncComponent(() => import('./modal/CustomModal.vue'));
componentKey = customModal
二、复杂的引入:不确定到底引入什么组件,组件的路径由后端返回
将以上代码 添加到项目代码中,并不能实现,虽然引入不报错,但是ref一直为undefined,无法调用动态组件内的open函数。
不断尝试了很多次,得出以下结论
1.起初是在按钮的click函数内去挂载自定义组件并调用ref函数的,ref为undefined。
尝试多次不能实现功能(这里是挂载与调用最合适的位置),
2.接着又在初始化配置数据时(查询后端sql),axios的then函数内挂载组件,然后点击按钮的地方调用ref内的函数,ref依旧为null。
3. 接着在最外层,调用初始化时挂载,也就是生命周期函数体内,测试还是一样的结果。
4. 接着发现带有async函数体内挂载组件,也无法完成。
5.单独写个函数,不加async,函数内挂载组件,然后再生命周期外调用该函数,按钮内调用ref内的方法,成功弹窗。这并不是我想要的,因为路径不是固定的,它要等到后端sql放回结果,才能执行。
总结:上面的多次测试,得出以下结论,都不能让动态组件ref对象有值
1、不能在组件的事件函数内挂载,
2、不能在axios的then函数体内挂载
3、不能在带有async声明的函数体内挂载
4、不能在vue的生命周期内挂载
5、只能在最外层挂载实现,这时ref才是个对象。
好在天无绝人之路;脑海里有个思路:
页面初始化时将项目里所有的全局挂载view组件扔到一个object内,使用component组件,is:对应object内指定的组件对象,然后通过后端的数据,这时后端就不用给组件路径了,给个组件名,我从object中找到挂载的组件然后将对象给is。
const modules = import.meta.glob('@/views/*/**.vue');
// 获取所有项目路径
mudules为views内所有的vue的相对路径,然后循环它,在循环体内实现挂载,将它存入一个对象内,key为相对路径的项目名称(可以截取以下)。
有了上面的思路,通过反复测试和实现,最终功能实现了。
<template>
<component :is="componentKey" ref="custom"></component>
</template>
<script>
import {
reactive,
ref,
shallowReactive,
onActivated,
defineAsyncComponent,
} from 'vue';
//声明componentkey,用于告诉component当前挂载什么组件,components为一个对象,存放多个不确定的自定义组件。
const componentKey = ref(null);
const components: any = shallowReactive({});
// 组件挂载
const initTableConfig = (gridId, type) => {
queryTableConfig({ gridId }).then(({ data }) => {
if (type === 'main') {
Object.assign(mainConfig, data);
tabsKey.value = -1;
} else {
tabsDetail.value.push(data);
tabsKey.value = tabsDetail.value.length - 1;
}
// 涉及到自定义组件的部分,这里需要提前挂载,在用到时不至于ref为null
XEUtils.objectEach(data.action, (action, key) => {
if (
action.modalCfg &&
action.modalCfg.type === 'CustomModal' &&
action.modalCfg.src
) {
components[action.actionId] = defineAsyncComponent(
() => import(`../../../${action.modalCfg.src}`)
);
//注意:这里的路径后端只能返回相对路径,不能使用@/xxx/xxx.vue ,不能使用src/xxx/xxx.vue,只能./xxx.vue或者../../xxx/xxx.vue。由于并不确定组件在什么位置,避免容易出错的原则,我在前端通过../../../的形式将路径回退到src下,后端只需要从src下配置路径即可,不用考虑那么多了。如后端src的值为src/xxx/xxx/xxx.vue 则在前端合成的路径就为../../../src/xx/xxx/xxx.vue
componentKey.value = components[action.actionId];
// 为什么componentKey.vue在这里赋值,在后面点击窗口后又赋值,这里能不能省略。
// 答:这里省略的话,到点击按钮触发时会报错,第一次点击会报错,第二次点击不会报错,窗口正常弹出。可能是因为,组件挂载时并没有引入组件,只在使用时才引入,如果上面不提前将挂载好的组件引入进来,后面触发事件触发时引入在调用ref,执行太快,costom就会报错,所以才会点两次才弹窗。
}
});
});
};
</script>
按钮点击触发事件,确定弹窗要弹出什么组件
} else if (action.modalCfg.type === 'CustomModal') {
// 这里的actionid和组件是对应的,所以在按钮触发后,通过按钮携带的actionid能取到对应的组件。
componentKey.value = components[action.actionId];
custom.value.init(row);
}
经过以上的方式:在任何地方挂载都不会报错,完美解决。
注意:挂载与使用ref不能在同一个方法体内,如果可以的话,页面加载时,执行挂载,需要调用ref时就不会报错。