写在前头
相信大家平时工作中没少被各种坑爹产品的奇葩需求气吐血,原本谈的好好的需求说改就改,也不知道是用户抽风还是产品抽风。被这么搞了几次以后,想了想还是写个组件封装思路出来保护自己所剩不多的头发。
后续会出一个plus版本、敬请期待`
准备
采用vue3的组合式api来编写,详情可以参考我的 common-template 专栏,有现成的代码可以使用。
一、目标
先明确我们要做什么:
- 封装一个
useXXX
的函数,可以通过该函数传递参数、调用组件内部方法(修改传入参数、获取传入参数等)。 - 主模块可以使用
@register
的方式进行复用。
对于一个组件来说,上面两点是最基本的需求。
二、文件结构
bash
📦XXX
┣ 📂src
┃ ┣ 📂comps # 子模块
┃ ┃ ┣ 📜XXXitem.vue
┃ ┣ 📂hooks
┃ ┃ ┣ 📜useXXX.ts
┃ ┣ 📜BasicXXX.vue # 主模块
┃ ┣ 📜props.ts
┃ ┣ 📜types.ts
┗ 📜index.ts
三、具体实现
在新建好的 useXXX
文件里编写 register
部分,这部分主要是为了让组件可以复用。
把组件要暴露出去的 method
也一起放在这里。
ts
// src/hooks/useXXX.ts
import { nextTick, onUnmounted, ref, unref, watch } from 'vue';
import { getDynamicProps } from '/@/utils/props';
import { DemoActionType, DemoPropsType, UseDemoReturnType } from '../types';
export function useDemo(props: DemoPropsType): UseDemoReturnType {
// 组件实例
const listRef = ref<Nullable<DemoActionType>>(null);
// 确保获取到组件实例
async function getDemo() {
const list = unref(listRef);
if (!list) {
console.log('demo示例尚未获取,请确保在执行操作时已呈现demo!');
}
await nextTick();
return list as DemoActionType;
}
// 注册组件
function register(instance: DemoActionType) {
// 确保性能
onUnmounted(() => {
listRef.value = null;
});
// 组件实例赋值
listRef.value = instance;
watch(
() => props,
() => {
// getDynamicProps函数可以把ref数据转为unref数据
props && instance.setProps(getDynamicProps(props));
},
{
immediate: true,
deep: true,
},
);
}
// 组件暴露出去的方法
const method: DemoActionType = {
setProps: async (props: Partial<DemoPropsType>) => {
const demo = await getDemo();
demo.setProps(props);
},
};
return [register, method];
}
ts
// utils
import { unref } from 'vue';
// 动态使用hook参数
export function getDynamicProps<T, U>(props: T): Partial<U> {
const ret: Recordable = {};
Object.keys(props as Recordable).forEach((key) => {
ret[key] = unref((props as Recordable)[key]);
});
return ret as Partial<U>;
}
ts
// src/types.ts
export interface DemoPropsType {
// 入参1
field1: string;
}
export interface DemoActionType {
setProps: (prop: Partial<DemoPropsType>) => void;
}
export type RegisterFn = (instance: DemoActionType) => void;
export type UseDemoReturnType = [RegisterFn, DemoActionType];
接下来我们到主模块里编写 useXXX
函数里用到的 setProps
方法。
把主模块需要的 getProps
方法也放在这里。
vue
// src/BasicDemo.vue
<script setup lang="ts">
import { computed, onMounted, ref, unref } from 'vue';
import { DemoActionType, DemoPropsType } from './types';
import { demoProps } from './props';
import { deepMerge } from '/@/utils';
const props = defineProps(demoProps);
const emit = defineEmits(['register']);
const propsRef = ref<Partial<DemoPropsType>>();
const getProps = computed(() => {
return { ...props, ...unref(propsRef) };
});
const setProps = (props: Partial<DemoPropsType>) => {
propsRef.value = deepMerge(unref(propsRef) || {}, props);
};
const demoAction: DemoActionType = {
setProps,
};
onMounted(() => {
emit('register', demoAction);
});
</script>
<template>
<div>{{ getProps.field1 }}</div>
</template>
ts
// src/props.ts
export const demoProps = {
filed1: {
type: String,
default: '',
},
};
四、思路解析
当我们在页面上用 <BasicXXX @register="register"></BasicXXX>
的方式使用时,组件通过在 onMounted
生命周期里 emit
出方法来实现暴露组件内部函数的需求。
用 const [register, {setProps}] = useXXX()
的方式来调用useXXX
方法里的 register
满足组件复用的需求。
当我们传参的时候会触发 watch
调用 setProps
方法,满足传参和修改参数的需求。
这实际上就是组合式api的一种运用。
vue
<script setup lang="ts">
import { BasicDemo, useDemo } from '/@/components/hp-demo';
const [register, { setProps }] = useDemo({
field1: '示例数据',
});
</script>
<template>
<div>
<span @click="setProps({ field1: '修改后数据1' })">点击修改参数1</span>
<span @click="setProps({ field1: '修改后数据2' })">点击修改参数2</span>
<BasicDemo @register="register" />
</div>
</template>
如果我们有多个组件,可以使用多个 useXXX
互相不会冲突。
如果我们有多个子模块,可以在 comps
文件夹里添加,在 BasicXXX
主模块里使用。
如果我们有多个 hook
,可以在 hooks
文件夹里编写,在 BasicXXX
主模块里通过 demoAction
向外暴露。