『精』Vue 组件如何模块化抽离Props

『精』Vue 组件如何模块化抽离Props.jpg

『精』Vue 组件如何模块化抽离Props


一、为什么要抽离Props

在前端框架中封装一个组件是非常常见的,对于组件来讲,往往离不开组件之间属性的传递,通过属性能够控制组件的样式内容、处理逻辑类型、数据传递。假如有这么一个用于签名的组件,能够控制画笔的大小、颜色、画布背景颜色宽高、按钮的文案提示等,那么它的属性必定是有十几个选项,假设 800 行代码里可能就得花费 100 行去声明这些属性,这明显是不合适的。
这也是我为什么提议要抽离组件 Props 的原因,实际上这个签名组件也确实存在,源自于我在 Uniapp 插件市场上的一个提交
图 1-1
抽离Props的好处有这么几点:

  • 降低、隔离组件的复杂度,降低维护成本与提高程序可靠率。
  • 引入一个中间的、易于理解的抽象概览。
  • 隐藏非核心处理部分的实现细节,提高组件源码的可阅读性。
  • 促进代码的重用,包装一定的结构风范。
  • 属性的抽离,有助于自动化程序生成对应文档。

那么要怎么做才能抽离出 Props 呢,在 Vue 身上根据选项式 API 和组合式 API 写法的不同,做法也不一样。
一些简单的组件可能只有一两个属性,甚至是没有属性,对于这种情况下,可能不抽离 Props 会显得更方便一些,但一定要注意的是应当把组件以后要做的和其扩展维护性放在一起考虑,往往十几个属性堆叠的组件都是从一两个属性的组件开始的,既然以后会越加越多,那为何不提早抽离处理方便以后再加呢,当然具体怎么做取决于自己。


二、选项式API方式抽离

选项式 API 适用于 Vue2 和 Vue3,需要使用到 Mixins 混合方法。Mixins 不只是能抽离 Props,它能做到更多,我相信大部分对这个功能会比较熟悉,有兴趣的可以参考此文档 Mixins

import type { PropType } from "vue";

export default {
  props: {
    prop1: {
      type: String,
      required: true
    },
    prop2: {
      type: Array as PropType<string[]>,
      default: ["a", "b", "c"]
    },
    prop3: {
      type: Array as PropType<Record<string, unknown>[]>,
      defalue: []
    }
  }
};

<script lang="ts">
  import props from "./props";

  export default {
    mixins: [props]
  };
  
</script>

三、组合式API方式抽离

组合式 API 属性的声明依赖于 defineProps 编译宏,这个编译宏只能出现在 .vue 的文件中才能正确识别,那么对于 Hook 来说,抽离 Props 是行不通的。

3.1 TypeScript类型方式

defineProps 是支持 typescript 类型标注声明的,直接引入一个类型行不行得通的呢?关于这个问题要分 Vue3.3 版本之前与之后,Vue3.3 版本之前是不允许将类型接口以导入的形式传给 defineProps 泛型,在官方文档中也有对应的标注,相关讨论在这 issue
图 3-1
图 3-2

但这还有一个问题,typescript只能声明类型,如果要配置默认值的话需要搭配另外一个编译宏 withDefaults 去实现,且类型接口能导入还好,若是不能导入的话,本地类型接口又违背降低复杂度而抽离的初衷了,在我看来还不如不用。依据 Vue3.3 版本之后,可以参考下列示例:

export interface Props {
  prop1: string;
  prop2?: string[];
  prop3?: Record<string, unknown>[];
}

export const propsDefault = {
  prop1: "默认值",
  prop2: () => ["a", "b", "c"]
};

<template>
  <div>{{ $props.prop1 }}</div>
  <div>{{ $props.prop2 }}</div>
</template>

<script lang="ts" setup>
	import { type Props, propsDefault } from "./props";
  
  const $props = withDefaults(defineProps<Props>(), propsDefault);
  
</script>
3.2 文件分离方式

人们往往容易把简单的东西想复杂,将 Props 类型声明存放到一个单独文件里,只要确保 defineProps 在 .vue 文件中使用即可,这么做有个好处就是简单易用且在不依赖typescript的时候基本兼容 Vue3 各版本,对于我来讲,我更习惯于使用 文件分离式 进行 Props 的抽离。
这种方式与 Hook 不同的是,Hook需要一层函数包裹执行,而这样相当于直接引入配置文件中的一个变量。

import type { PropType } from "vue";

export const props = {
  prop1: {
    type: String,
    required: true
  },
  prop2: {
    type: Array as PropType<string[]>,
    default: ["a", "b", "c"]
  },
  prop3: {
    type: Array as PropType<Record<string, unknown>[]>,
    defalue: []
  }
};

<template>
  <div>{{ $props.prop1 }}</div>
  <div>{{ $props.prop2 }}</div>
</template>
  
<script lang="ts" setup>
    const $props = defineProps(props);

</script>

对于 TypeScript 如果需要获取 Props 的类型,那就需要用到 Vue 的一个辅助类型 ExtractPropTypes,对于外部引用(父组件)的话使用 ExtractPublicPropTypes,只需在上面示例的基础上改造一下👇。

import type { PropType, ExtractPropTypes } from "vue";

export const props = {
  prop1: {
    type: String,
    required: true
  },
  prop2: {
    type: Array as PropType<string[]>,
    default: ["a", "b", "c"]
  },
  prop3: {
    type: Array as PropType<Record<string, unknown>[]>,
    defalue: []
  }
};

// 导出 Props 类型
export type Props = ExtractPropTypes<typeof props>;

其实 ElementUI 的源码也是这么去做的,以 el-empty 组件为例,源码中对于属性的分离是这么写的。
图 3-3

3.3 对文件分离方式优化

从优化的角度上看,Vue 的Props声明方式都是一样的,同样的代码写多次显得有些冗余,那不妨将这些 Props 声明再次优化一下,编写一个公共的 Props 辅助工具进行声明。

"use strict";
/**
 * 组件属性辅助
 * 🙌 能够使用更少的代码编写,有助于减少包体积
 */
import type { PropType } from "vue";

export const unknownProp = null as unknown as PropType<unknown>;

export const numericProp = [Number, String];

export const truthProp = {
  type: Boolean,
  default: true as const
};

export const lieProp = {
  type: Boolean,
  default: false as const
};

export const makeRequiredProp = <T>(type: T) => {
  return { type, required: true as const };
};

export const makeNumericProp = <T>(defVal: T) => {
  return { type: numericProp, default: defVal };
};

export const makeStringProp = <T>(defVal: T) => {
  return { type: String as unknown as PropType<T>, default: defVal };
};

export const makeNumberProp = <T>(defVal: T) => {
  return { type: Number as unknown as PropType<T>, default: defVal };
};

export const makeArrayProp = <T>(defVal: T[]) => {
  return { type: Array as PropType<T[]>, default: () => defVal };
};

export const makeObjectProp = <T>(defVal: T) => {
  return { type: Object as PropType<T>, default: () => defVal };
};

export const makeFuncProp = <T>(defVal: T) => {
  return { type: Function as PropType<T>, default: defVal };
};

// 联和类型属性
export const makeUniteProp = <T, V>(type: T[], defVal: V) => {
  return { type, default: () => defVal };
};

import type { PropType, ExtractPropTypes } from "vue";

import {makeArrayProp, makeRequiredProp} from "@/utils/props"

export const props = {
  prop1: makeRequiredProp(String),
  prop2: makeArrayProp<string>(["a", "b", "c"]),
  prop3: makeArrayProp<Record<string, unknown>[]>([])
};

export type Props = ExtractPropTypes<typeof props>;


参考资料💘

🍅因发布平台差异导致阅读体验不同,源文贴出:《『精』Vue 组件如何模块化抽离Props》


推荐博文🍗