最近准备面试了,想重新回顾一下组件封装的一些要点
一、输入(props)和输出(emits)
- 举一个通用按钮(BaseButton.vue)的例子
我们要封装一个按钮,支持:
- 类型:primary、danger、default
- 禁用状态
- 点击事件
javascript
<template>
<button class="base-btn"
:class="[`base-btn--${type}`]"
:disabled="disabled"
@click="handleClick"
>
<!-- 默认插槽:让父组件可以传入文字或图标 -->
<slot></slot>
</button>
<script setup>
import { computed } from 'vue'
// 1. 定义输入
const props = defineProps({
type: {
type: String,
default: 'default', // 默认类型
validator: (value) => ['primary', 'danger', 'default'].includes(value) //简单的值校验
},
disabled: {
type: Boolean,
default: false
}
})
// 2. 定义输出
const emit = defineEmits(['click', 'otherAAction'])
const handleClick = (event) => {
if (!props.disabled) {
emit('click', event)
}
}
</script>
<style scoped>
/* 类型修饰符 */
.base-btn--primary { background-color: #409eff; color: white; }
.base-btn--danger { background-color: #f56c6c; color: white; }
.base-btn--default { background-color: #fff; border: 1px solid #dcdfe6; color: #606266; }
...
</style>
父组件使用:
javascript
<template>
<div>
// 使用primary类型
<BaseButton type="primary" @click="saveData">保存</BaseButton>
// 使用danger类型,禁用
<BaseButton type="danger" :disabled="true">保存</BaseButton>
</div>
</template>
<script setup>
import BaseButton ...
const saveData = () => {
console.log("保存数据...")
}
</script>
这里就是简单的组件封装,主要是体现props和emits的作用
二、双向绑定
- 举一个通用输入框(BaseInput.vue)的例子,这里仅展示核心代码,其余不过多展示
javascript
<template>
<input
:value="modelValue"
@input="handleInput"
/>
</template>
<script setup>
// 1. v-model本质是接受一个modelValue属性
const props = defineProps({
modelValue:{
type: [String, Number],
defalut: ''
},
})
// 2. v-model修改值时,会触发 update:modelValue 事件
const emit = defineEmits(['update:modelValue'])
</script>
三、属性透传⭐⭐⭐
- 属性透传主要在组件二次封装中使用,尤其是在UI方面对
Element Plus、Ant Design等组件库的适应再封装 - Vue 3 有一个默认行为:如果子组件只有一个根元素,那么父组件传过来的、子组件没声明的属性,会自动加到这个根元素上。例如:
javascript
<template>
// 这里el-button就是根元素
<el-button>
<slot></slot>
</el-button>
</template>
<script setup>
// 这里故意不声明 size、loading、type 等属性
</script>
在父组件中
javascript
<template>
<!-- 父组件传了 size 和 loading,但 MyButton 自己没声明 -->
<MyButton size="large" loading type="primary">
点击我
</MyButton>
</template>
因为只有根元素,Vue会把多余的属性自动透传到根元素,等效于
javascript
<el-button size="large" loading type="primary">
点击我
</el-button>
- 手动透传($attrs) 修改一下子组件MyButton,在最外层包装
div。
v-bind=...这里相当于精准投放,把父组件传递的属性精确的传递到我们选定的节点上。
javascript
<template>
<div>
// 这里我们希望传递过来的属性作用在el-button上,而不是div上
<el-button v-bind="$attrs">
<slot></slot>
</el-button>
</div>
</template>
<script setup>
// 关键一步:关闭默认的自动透传
defineOptions({ inheritAttrs: false })
</script>
$attrs是一个对象,包含了父组件传递过来,但当前组件没有声明为props的属性和事件 ,注意这里包含事件。
还是举个例子吧,父组件:
javascript
<template>
<MyButton size="large" @focus="handleFocus" @input="handleInput">
点击我
</MyButton>
</template>
<script setup>
const handleFocus = () => {
clg(...)
}
</script>
四、方法暴露(defineExpose)
上面提到的都是父组件传递给子组件属性,子组件触发emit激活父组件。那么当父组件想要直接调用子组件的内部方法时:
javascript
// 子组件 my-input.vue
<script setup>
const inputRef = ref(null)
const focus = () => {
inputRef.value.focus()
}
defineExpose({
focus,
...
})
<template>
<input ref="inputRef" />
</template>
父组件调用
javascript
<template>
<MyInput ref="myInputRef" />
<button @click="myInputRef.focus()">点击聚焦</button>
</template>
结语
这些就是简单的组件封装玩法,除此之外还有插槽的一些用法、以及逻辑抽离composable的玩法、深层嵌套组件provide/inject的用法。有兴趣的可以自己学习一下。文章有错欢迎指正!鄙人保持学习的初心。