jk's notes
  • Vue3 中, 基于 TS 封装组件的注记

Vue3 中, 基于 TS 封装组件的注记

封装组件重点在于:

  • 如何导出属性, 事件
  • 如何处理插槽
  • 如何控制生命周期

参考官方文档:

  • 组件: https://cn.vuejs.org/guide/components/registration.html
  • TS 篇: https://cn.vuejs.org/guide/typescript/composition-api.html

基本上就是官方文档的梳理

1. 注册组件的方法

方法包括:

  1. 全局注册 (在 main.ts 中导入, 然后使用 component() 方法给 app 注册)
  2. 局部注册 (在具体需要的页面中 import)
// 全局注册
import <实现> from '...'
const app = createApp({})
app.component('组件名', <实现>)

2. 属性的定义

仅列举个人比较习惯的方案

  • 使用 interface 定义组件需要的数据类型
  • 使用 defineProps<T> 来定义参数
  • 使用 withDefault 来定义默认值

注意属性值不可修改

定义属性数据类型:

<script lang="ts" setup>
interface Person {
  name: string;
  age: number;
  gender: string;
}
</script>

定义属性:

<script lang="ts" setup>
interface Person {
  name: string;
  age: number;
  gender: string;
}
const props = defineProps<Person>()
</script>

image-20231124181349078

对于需要基于属性传入的数据, 而内部又需要进行修改的数据, 可以使用:

const props = defineProps<...>()
const propName = ref<...>(props.propName)

而复杂数据也支持使用计算属性

const props = defineProps<...>()
const propName = computed<...>(() => {
  return props.xxx 表达式
})

3. 属性默认值

为属性定义默认值, 使用 withDefaults 宏:

const props = withDefaults(defineProps<类型>, {
                             属性: 默认值
                           })

简单说就是将 props 作为参数传入, 设置其默认值

image-20231127090940187

4. 定义事件

  • 定义事件声明有三种方式: 运行时声明, 类型声明, 基于字面量声明.
  • 基于运行时声明最简单, 只用定义事件名即可
  • 基于类型与字面量的声明逻辑一样, 只是形式不同
const emit = defineEmits(['事件名'])  // 基于 运行时

// 基于声明
const emit = defineEmits<{
  (e: '事件名', 参数列表): void
}>()

const emit = defineEmits<{
  '事件名': [...] // 元祖, 或数组, 用于描述参数列表
}>()

5. v-model 本质

v-model 是两个部分构成:

  1. 一个是属性.
  2. 一个是 input 事件. 一般约定事件名带有 update: 前缀.

image-20231127093011062

6. 透传 Attributes

所谓透传, 是指定义组件时未设置属性, 以及事件. 但在使用组件时传入了数据.

常见的透传属性有: style, class, 和 id.

规则:

  1. 默认会将该属性绑定到组件的根元素上. 如果根元素是另一个组件, 同样待之.
  2. 如果根元素非单根元素 (多根节点), 而属性又未被使用, 则会警告.
  3. 如果属性是 style, 或 class 则会合并. 其他属性则会被传入元素覆盖.
  4. 如果禁用自动透传, 使用 inheritAttrs: false 来配置.
  5. 如果需要在组件内模板中引用传入属性, 使用 $attrs 来引用. 其中 props, emits 定义的成员不包含在其内.
  6. 若要全部绑定到某一子元素, 可以使用: v-bind="$attrs"
  7. 如果在组件内 JS 中使用透传属性, 则使用 useAttrs() 方法来获得该属性对象.
// 禁用透传
defineOptions({
  inheritAttrs: false
})

使用组件时传入的属性会覆盖定义的属性

image-20231127101153424

7. 插槽

插槽, 用来定义容器组件, 定义时只定义容器, 使用时提供内部组件, 最终组合在一起使用.

说明:

  • 插槽使用 <slot></slot> 来定义. 组件中可以有多个 <slot></slot>. 每一个必须有唯一的名字 (name 属性), 不写名字默认为 default.
  • 使用插槽时, 使用 <template v-slot:名字></template> 来指定使用哪一个插槽接纳元素. v-slot:名字 可简写为 #名字.
  • 使用插槽时, 未指定插槽名的, 顶级的元素, 均会放置于 default 插槽中.
  • 作用域插槽, 即在 <slot> 中传入数据, 这些数据就会交给将来放置的子组件中.

组件定义中, 无法访问插槽内的数据 (因为定义时, 会传入什么是不确定的). 反过来, <slot> 中将来放置的内容可以使用组件内的数据, 即作用域插槽.

7.1 文档配图很直观

默认插槽

插槽图示

定义

<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>

使用

<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>

插槽内容可以是任意的内容.

具名插槽

具名插槽图示

7.2 动态插槽名

可以将插槽名定义为响应式变量, 来动态控制:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

image-20231127103154497

7.3 作用域插槽

scoped slots diagram

  • 组件无法访问到 slot 中的内容.
  • 但是 slot 中可以访问组件的内容, 即作用域插槽.
  • 作用域插槽在使用中可以利用解构语法, 例如: v-slot="{ text, count }"
Last Updated:
Contributors: jk