为什么 Vue3 封装 Table 组件丢失 expose 方法呢?
在实际开发中,我们通常会将某些常见组件进行二次封装,以便更好地实现特定的业务需求。然而,在封装 Table 组件时,遇到一个问题:Table 内部暴露的方法,在封装之后的组件获取不到。
代码展示为:
const MyTable = defineComponent({
name: 'MyTable',
props: ElTable.props,
emits: Object.keys(ElTable.emits || {}),
setup(props, { slots, attrs, expose }) {
const tableRef = ref<InstanceType<typeof ElTable>>();
return () => (
<div class='table-container'>
<ElTable
ref={tableRef}
{...props}
{...attrs}
v-slots={slots}
>
{slots.default && slots.default()}
</ElTable>
</div>
);
},
});
在 Element Plus 的 ElTable 组件中,有很多暴露的方法,如下:
官方文档:Table 表格 | Element Plus
但使用上面封装的 MyTable 打印组件实例时,只有 table 本身的方法:
下面简单分析一下原因。
1. expose 未正确定义或调用
原因分析:
在 Vue 3 中,expose 用于暴露子组件的方法和属性,使父组件能够访问和操作它们。如果封装了 ElTable 组件,但是没有正确定义 expose 或者没有通过 ref 正确引用子组件实例,那么 expose 的方法无法生效。
如果在 setup 中使用 expose API 或者 expose 的位置不对,那么暴露的方法就无法通过 ref 访问到。🌰:
expose({
setCurrentRow: tableRef.value?.setCurrentRow, // 错误:此时 tableRef.value 还可能为 null
clearSelection: tableRef.value?.clearSelection // 错误
});
解决方法:确保在 setup 中正确使用 expose 并且在暴露方法时,确保 tableRef 已经指向 ElTable 的实例。
2. ElTable 组件实例未传递给 tableRef
原因分析:
在封装代码中,ElTable 内部 expose 的方法(例如:setCurrentRow、clearSelection 等)可能会因为 ref 没有正确透传而丢失。Vue3 中,ref 的默认行为不能直接传递组件的 expose 方法到父组件中(本质)。
解决方法:手动暴露,确保 ElTable 组件通过 ref 绑定到 tableRef。
3. 组件生命周期中的调用顺序问题
原因分析:
expose 需要在 setup 函数中定义,而 tableRef 需要在组件挂载后才能被正确引用。如果在组件生命周期的某个不合适的时间调用 expose,比如,在 setup 函数之外或者组件渲染之前,可能导致tableRef 无法正确指向组件实例,从而无法暴露方法。
解决方法:使用 nextTick 来确保组件渲染完成后再执行某些操作。
综上,代码如下:
import { defineComponent, ref } from 'vue';
import { ElTable } from 'element-plus';
export default defineComponent({
name: 'MyTable',
props: ElTable.props,
emits: Object.keys(ElTable.emits || {}),
setup(props, { attrs, slots, expose }) {
const tableRef = ref<InstanceType<typeof ElTable>>();
// 确保暴露方法时,tableRef 已经引用了正确的实例
expose({
setCurrentRow: (...args: Parameters<InstanceType<typeof ElTable>['setCurrentRow']>) =>
tableRef.value?.setCurrentRow(...args),
clearSelection: (...args: Parameters<InstanceType<typeof ElTable>['clearSelection']>) =>
tableRef.value?.clearSelection(...args),
});
return () => (
<div class="table-container">
<ElTable ref={tableRef} {...props} {...attrs} v-slots={slots}>
{slots.default && slots.default()}
</ElTable>
</div>
);
}
});
现在再看一下 组件实例,已经存在了需要的方法。
注意,在 table 内部存在很多暴露的方法,要想让我们封装的 MyTable 和 ElTable 可以通用,那么需要将方法全部暴露出来:
export default defineComponent({
name: 'MyTable',
props: ElTable.props,
emits: Object.keys(ElTable.emits || {}),
setup(props, { attrs, slots, expose }) {
const tableRef = ref<InstanceType<typeof ElTable>>();
const ExposedMethods = [
"clearSelection",
"getSelectionRows",
"toggleRowSelection",
"toggleAllSelection",
"toggleRowExpansion",
// ...
];
// 将方法透传
const exposedMethods: Record<string, Function> = {};
ExposedMethods.forEach(method => {
exposedMethods[method] = (...args: any[]) => {
return tableRef.value?.[method](...args);
};
});
// 使用 expose 透传所有方法
expose(exposedMethods);
return () => (
<div class="table-container">
<ElTable ref={tableRef} {...props} {...attrs} v-slots={slots}>
{slots.default && slots.default()}
</ElTable>
</div>
);
}
});
现在,所有的方法均存在,后续可以直接使用我们封装的 MyTable 组件。
4. 注意
有一个坑,不能将 expose 放在 onMounted 内部执行,为什么呢?
onMounted(() => {
const exposedMethods: Record<string, Function> = {};
ExposedMethods.forEach(method => {
exposedMethods[method] = (...args: any[]) => {
return tableRef.value?.[method](...args);
};
});
// 使用 expose 透传所有方法
expose(exposedMethods);
});
1. expose 在 setup 函数内部调用时,会立即暴露方法或属性给外部访问。 如果将 expose 放入 onMounted 中,setup 函数的执行已经结束,因此暴露的方法不会被 Vue 视为暴露的实例方法。
2. onMounted 是一个生命周期钩子函数,它会在组件挂载后执行,但此时 Vue 的响应式系统和父组件的访问已经设定好了,因此暴露的方法不再能正确传递。