前言
之前写了篇文章 还在烦恼满足不了坑爹产品的奇葩需求?来感受下Vue3组合式api的魅力! ,介绍了一种组件封装的新思路,接下来我们就着实际业务场景来思考一下怎么改良这个思路。
一定要先去看上面的文章,否则会对本篇代码感到陌生
业务场景
老板闲的没事儿想把公司项目里的弹框捣鼓捣鼓,便来问你能不能在不同的场景展示不同的弹框效果。尽管是很是不解:这老登没事儿加什么工作量?如果按照以前的弹框封装方式,代码肯定会十分冗余,有没有什么好的偷懒方式呢?
具体实现
我们之前实现了一个组件最基本的功能,接下来我们针对该组件再明确一下要实现的需求:
- 组件要可以抽离出来,并且参数可以灵活传递
- 抽离组件不能起冲突,里面的方法要可以在页面上使用
来看我们的 useDemo 函数,里面有一个 listRef 变量,这是每个组件的实例对象。
那么我们能不能在这个实例对象上做做文章,在 useDemo 文件里声明一个公共对象 xxxData ,以 组件 uid 为 key , visible 为 value,然后创建一个 useDemoInner 函数,使用一个公共 Ref 实例作为该函数回调的传参。
先用 vue 的 getCurrentInstance() 获取 BasicDemo.vue 的实例,然后改造 getProps 和 setProps ,注意新增的 visibleRef 变量和 visible-change 事件 ,具体代码如下:
// BasicDemoPlus.vue
<script setup lang="ts">
import { computed, getCurrentInstance, nextTick, ref, unref, watch } from 'vue';
import { DemoActionType, DemoPropsType } from './types';
import { demoProps } from './props';
import { deepMerge } from '/@/utils';
const props = defineProps(demoProps);
const emit = defineEmits(['register', 'visible-change']);
const propsRef = ref<Partial<DemoPropsType>>();
const visibleRef = ref<boolean>(false);
const instance = getCurrentInstance();
const getMergeProps = computed(() => {
return { ...props, ...unref(propsRef) };
});
const getProps = computed(() => {
const opt = {
...unref(getMergeProps),
visible: unref(visibleRef),
};
return opt as DemoPropsType;
});
const setProps = (props: Partial<DemoPropsType>) => {
// deepMerge函数可以把两份props合并
propsRef.value = deepMerge(unref(propsRef) || {}, props);
if (Reflect.has(props, 'visible')) {
visibleRef.value = !!props.visible;
}
};
const demoAction: DemoActionType = {
setProps,
emitVisible: undefined,
};
instance && emit('register', demoAction, instance.uid);
watch(
() => visibleRef.value,
(v) => {
nextTick(() => {
emit('visible-change', v);
instance && demoAction.emitVisible?.(v, instance.uid);
});
},
);
</script>
<template>
<div>{{ getProps }}</div>
</template>
接着转到 useDemoPlus.ts 文件,声明 visibleData 公共数组变量和 dataTransFerRef 组件对应实例对象数据。
// useDemoPlus.ts
import { computed, onUnmounted, reactive, ref, toRaw, unref, watch } from 'vue';
import { getDynamicProps } from '/@/utils/props';
import { DemoActionType, DemoPropsType, ReturnMethods, UseDemoReturnType } from '../types';
import { isEqual } from 'lodash-es';
const dataTransFerRef = reactive({});
const visibleData = reactive<{ [key: number]: boolean }>({});
export function useDemoPlus(props: DemoPropsType): UseDemoReturnType {
// 组件实例
const listRef = ref<Nullable<DemoActionType>>(null);
// 组件uid
const uid = ref<string>('');
// 确保获取到组件实例
function getDemo() {
const list = unref(listRef);
if (!list) {
console.log('demo示例尚未获取,请确保在执行操作时已呈现demo!');
}
return list as DemoActionType;
}
// 注册组件
function register(instance: DemoActionType, uuid: string) {
// 确保性能
onUnmounted(() => {
listRef.value = null;
});
// 组件实例赋值
listRef.value = instance;
uid.value = uuid;
// 声明赋值函数
instance.emitVisible = (visible: boolean, uid: number) => {
visibleData[uid] = visible;
};
watch(
() => props,
() => {
// getDynamicProps函数可以把ref数据转为unref数据
props && instance.setProps(getDynamicProps(props));
},
{
immediate: true,
deep: true,
},
);
}
// 组件暴露出去的方法
const method: ReturnMethods = {
setProps: async (props: Partial<DemoPropsType>) => {
const demo = await getDemo();
demo.setProps(props);
},
getVisible: computed((): boolean => {
return visibleData[~~unref(uid)];
}),
openDialog: <T = any>(visible = true, data?: T, openOnSet = true): void => {
getDemo()?.setProps({
visible: visible,
...data,
});
if (!data) return;
if (openOnSet) {
// 重复赋值会为了避免原有数据的影响
dataTransFerRef[unref(uid)] = null;
dataTransFerRef[unref(uid)] = toRaw(data);
}
const equal = isEqual(toRaw(dataTransFerRef[unref(uid)]), toRaw(data));
if (!equal) {
dataTransFerRef[unref(uid)] = toRaw(data);
}
},
closeDialog: () => {
getDemo()?.setProps({ visible: false });
},
};
return [register, method];
}
声明 useDemoPlusInner 函数作为子页面调用的函数,注意该函数必须在 setup 函数中使用。
这个函数的关键点在于 watchEffect ,代码如下:
// useDemoPlus.ts
import {
computed,
getCurrentInstance,
nextTick,
onUnmounted,
reactive,
ref,
toRaw,
unref,
watch,
watchEffect,
} from 'vue';
import { getDynamicProps } from '/@/utils/props';
import {
DemoActionType,
DemoPropsType,
ReturnMethods,
UseDemoReturnType,
UseDemoInnerReturnType,
ReturnInnerMethods,
} from '../types';
import { isEqual, isFunction } from 'lodash-es';
const dataTransFerRef = reactive({});
const visibleData = reactive<{ [key: number]: boolean }>({});
...
...
export function useDemoPlusInner(callbackFn?: Fn): UseDemoInnerReturnType {
const dialogInstanceRef = ref<Nullable<DemoActionType>>(null);
const currentInstance = getCurrentInstance();
const uidRef = ref<string>('');
if (!getCurrentInstance()) {
throw new Error('useDemoPlusInner()只能在setup或component内部使用!');
}
const getInstance = () => {
const instance = unref(dialogInstanceRef);
if (!instance) {
console.log('useDemoPlusInner实例未创建!');
return;
}
return instance;
};
const register = (instance: DemoActionType, uuid: string) => {
uidRef.value = uuid;
dialogInstanceRef.value = instance;
// 与useDialog建立emit联系
currentInstance?.emit('register', instance, uuid);
};
watchEffect(() => {
const data = dataTransFerRef[unref(uidRef)];
if (!data) return;
if (!callbackFn || !isFunction(callbackFn)) return;
nextTick(() => {
callbackFn(data);
});
});
const methods: ReturnInnerMethods = {
getVisible: computed((): boolean => {
return visibleData[~~unref(uidRef)];
}),
closeDialog: () => {
getInstance()?.setProps({ visible: false });
},
setProps: (props: Partial<DemoPropsType>) => {
getInstance()?.setProps(props);
},
};
return [register, methods];
}
在页面上使用
// Demo.vue 主模块
<script setup lang="ts">
import { useDemoPlus } from '/@/components/hp-demo-plus';
import DemoPlus from './DemoPlus.vue';
// import { BasicDemo, useDemo } from '/@/components/hp-demo';
// const [register, { setProps }] = useDemo({
// field1: '示例数据',
// });
const [DemoPlusRegister, { openDialog }] = useDemoPlus();
</script>
<template>
<div>
<!-- <span @click="setProps({ field1: '修改后数据1' })">点击修改参数1</span>
<span @click="setProps({ field1: '修改后数据2' })">点击修改参数2</span> -->
<span @click="openDialog(true, { field1: '父页面传递过去的数据' })">点击修改plus3</span>
<!-- <BasicDemo @register="register" /> -->
<DemoPlus @register="DemoPlusRegister" />
</div>
</template>
// DemoPlus.vue 子模块
<script setup lang="ts">
import { BasicDemoPlus, useDemoPlusInner } from '../components/hp-demo-plus';
const [register] = useDemoPlusInner((data) => {
console.log(data);
});
</script>
<template>
<div>
<BasicDemoPlus @register="register" />
</div>
</template>
来看下效果
这种思路适合在交互类组件中使用,比如弹框等等。
结语
本篇文章没有实现实际的需求,只是提供一种组件封装的思路,这些是我自己经过思考和学习开源代码总结的,俗话说一个个摘瓜,不如抓住瓜藤,掌握一种思路就可以应对各种业务场景。