likes
comments
collection
share

vue3中如何优雅地扩展第三方组件

作者站长头像
站长
· 阅读数 14

第三方组件并不总能满足业务需求,有时候我们仅仅只是想要扩展上面的一点点功能。那如何优雅的把业务逻辑加上去又尽可能不去破坏组件原本的功能和使用方式呢?

一个组件的使用方式包含props,emit,slots,expose,其中在vue3中,emit可以在props中声明以达到类型提示的目的

函数式组件,最简单的扩展方式

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会出问题

例如,定义如下组件

const ShowFirstName = (props: { firstName?: string }) => <div>{props.firstName}</div>;

在template中使用

<ShowFirstName first-name="666" /> // 无效的,因为不是props,不会进行转义
<ShowFirstName firstName="666" />  // 有效的,完全匹配props的读取方式

这无疑给使用者带来了一些心智负担

使用defineComponent来扩展第三方组件

先来一个简单的例子

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数组,当然,如果需要更完善的校验器也是可以的

  // ...
  {
    name: 'MyInput',
    props: {
      ...NInput.props,
      preset: String,
    },
  },
  // ...

至此,扩展第三方组件的基本框架就搭建完了,我们可以在组件内部对传入的props做一些处理,添加上一些业务功能

如果需要对最终传入NInput的props做一些处理,一般需要用到mergeProps

    const bindValue = computed(() =>
      mergeProps(
        props as any,
        props.preset === 'something'
          ? {
              // do something
            }
          : {},
      ),
    );

因为emit事件可以改成onEvent的形式写在props中,因此一些组件库的emit声明就是在props里编写的,因此不需要在额外编写emit声明

使用extens扩展基础组件

<script lang="tsx">
import { NInput } from 'naive-ui';

export default {
  name: 'MyInput',
  extends: NInput,
  props: {
    preset: {
      type: String,
    },
  },
  setup(props, ctx) {
    // you can do something
    const bindProps = computed<any>(() => props);

    return () => <NInput {...bindProps.value} />;
  },
};
</script>

十分简单方便优雅地就可以拿到原有组件地props并进行额外的功能扩展

感谢大佬补充

emits说明

在vue3中,emit可以在props中声明以达到类型提示的目的,包括各个开源组件库,也基本都采用了这样的方式,为了简单的使用,我们可以使用vue-types仅仅提供类型提示而不进行类型校验,像是这样

<script lang="tsx">
import { object } from 'vue-types';

type MaybeFunctionArray<T extends (...args: any[]) => void> = T | T[];

function event<T extends (...args: any[]) => void>() {
  return object<MaybeFunctionArray<T>>()
}

function callEvent<T extends (...args: any[]) => void>(func?: MaybeFunctionArray<T>, ...args: Parameters<T>) {
  if (Array.isArray(func)) {
    func.forEach((f) => f(...args));
  } else {
    func?.(...args);
  }
}

export default {
  props: {
    //  外部的使用还是一样的,@myEvent,或者 传递onMyEvent也是可以的
    //  传递多个事件监听器也是可以接受的
    onMyEvent: event<(e: Event) => void>(),
  },
  setup(props, { emit }: any) {
    function handleClick() {
      // 图简单,你可以继续使用emit分发事件
      // eslint-disable-next-line vue/require-explicit-emits
      emit('myEvent', new Event('myEvent'));

      // 你也可以选择手动调用它
      callEvent(props.onMyEvent, new Event('myEvent'));
    }

    return () => <button onClick={handleClick}>click me</button>;
  },
};
</script>

额外的说明

以上的扩展方式仅针对于足够简单的组件,也就是仅仅在原有的组件上额外扩展一些些的功能,添加的自定义props数量有限,这样封装可以更方便使用者去使用

而如果你要封装的组件是protable这种融合了好几个组件组合在一起的上帝组件,那么对应的组件props和自定义的props还是分开来比较好,否则难以区分对应的props是传递给谁的,理解上就很困难了

以上