第三方组件并不总能满足业务需求,有时候我们仅仅只是想要扩展上面的一点点功能。那如何优雅的把业务逻辑加上去又尽可能不去破坏组件原本的功能和使用方式呢?
一个组件的使用方式包含props,emit,slots,expose,其中在vue3中,emit可以在props中声明以达到类型提示的目的
函数式组件,最简单的扩展方式
tsx
import type { InputProps } from 'naive-ui';
import { NInput } from 'naive-ui';
const MyInput = (props: InputProps & { preset?: string }) => {
// you can do something
// 这里的逻辑每次重新渲染都会重新执行
return <NInput {...props} />;
};
这是最简单的扩展方式,保留了原有组件的props和emit类型提示,又可以轻松的添加额外的逻辑
但这种方式有两个明显的缺点
- 一个是没有setup阶段,这意味着你只能处理一些简单的逻辑,带有副作用的逻辑(watch之类的)不能编写
- 二是props仅仅提供了类型提示,不是真正的props,这在遇到驼峰命名的props会出问题
例如,定义如下组件
ts
const ShowFirstName = (props: { firstName?: string }) => <div>{props.firstName}</div>;
在template中使用
html
<ShowFirstName first-name="666" /> // 无效的,因为不是props,不会进行转义
<ShowFirstName firstName="666" /> // 有效的,完全匹配props的读取方式
这无疑给使用者带来了一些心智负担
使用defineComponent来扩展第三方组件
先来一个简单的例子
tsx
import type { InputProps } from 'naive-ui';
import { NInput } from 'naive-ui';
export interface MyInputProps extends InputProps {
preset?: 'search';
}
const MyInput = defineComponent(
(props: MyInputProps, { slots }) => {
// setup
const finalSlots = computed(() => ({
...slots,
...(props.preset === 'search' && {
suffix: () => <div class="i-ant-design:search-outlined" />,
}),
}));
return () => (
<NInput {...props}>
{{
...finalSlots.value,
}}
</NInput>
);
},
{
name: 'MyInput',
props: Object.keys(NInput.props).concat(['preset']) as any,
},
);
首先是defineComponent接收一个setup函数,然后返回一个render函数,这defineComponent其中的一个主要用法
这里面有一个重要的点是,setup里的props声明不会生成实际的组件props,只是用来提示组件的props声明,也就意味着,如果第二个参数的props没有定义,组件接收到的永远是一个空对象,因此我们需要在第二个参数把props传递进去
因为只是想要组件去接收props,所以直接传递props的string数组,当然,如果需要更完善的校验器也是可以的
tsx
// ...
{
name: 'MyInput',
props: {
...NInput.props,
preset: String,
},
},
// ...
至此,扩展第三方组件的基本框架就搭建完了,我们可以在组件内部对传入的props做一些处理,添加上一些业务功能
如果需要对最终传入NInput的props做一些处理,一般需要用到mergeProps
tsx
const bindValue = computed(() =>
mergeProps(
props as any,
props.preset === 'something'
? {
// do something
}
: {},
),
);
因为emit事件可以改成onEvent的形式写在props中,因此一些组件库的emit声明就是在props里编写的,因此不需要在额外编写emit声明
以上