Vue3-组件

175 阅读3分钟

注册

全局注册

在项目的main.js中使用app.component('[组件注册名]',[组件])进行注册,注册后在项目的任一模板中均可直接使用,无需再行注册。

import App from './App.vue'
import BaseList from "/@/components/BaseList.vue";

const app = createApp(App)
app.component('BaseList', BaseList)
app.mount('#app')

当有多个组件需要全局注册时,建议链式调用进行注册:

app.component('BaseList', BaseList)
    .component('BaseForm', BaseForm)
    .component('BaseTable', BaseTable)

局部注册

<template>
    <BaseList />
</template>
  
<script setup>
import BaseList from '/@/components/BaseList.vue';
</script>

建议

建议优先使用局部注册,这样做的好处有两个:

  1. 未被引用的组件在生产打包时将自动移除(tree-shaking),极大地减少包体积;
  2. 显示地引入组件方便定位子组件的实现,组件间的依赖关系清晰,利于长期的维护。

Props

声明

在组件中使用defineProps()声明props,支持传入数组或对象:

<script setup>
// 声明props数组
defineProps(['title', 'content'])

// 声明props对象
defineProps({
    title,
    content
})
</script>

Tips:defineProps()只能在setup()中调用。

使用

在父组件中通过v-bind:[prop](简写:[prop])向子组件传递数据:

<template>
    <ChildComponent title="props使用示例" :content="childContent" />
</template>

若需要传递多个prop,可以使用不带参数的v-bind传递一个对象,对象的每个属性都将作为一个prop进行传递:

<template>
    <ChildComponent v-bind="childObj" />
</template>

<script setup>
const childObj = ref({
    title: 'props使用示例',
    content: '这是Props使用示例'
})
</script>

在子组件中访问props中的数据:

<template>
    <!-- 在模板中访问props中的数据 -->
    <div>{{ title }}</div>
</template>

<script>
// 访问props数组中的数据
const props = defineProps(['title', 'content'])
console.log(props['title'])

// 访问props对象中的数据
const propsN = defineProps({
    class,
    grade
})
console.log(propsN.class)
</script>

切记Javascript中不可直接使用props中的数据,这是Vue3组合式API和Vue2的区别。

props中的数据不允许直接修改,应将prop作为初始值声明到一个响应式变量或使用computed()进行计算后使用:

<script setup>
import { ref, computed } from 'vue';

const props = defineProps(['title'])

// 将prop作为响应式变量的初始值
let newTitle = ref(props['title'])

// 将prop通过计算属性计算后返回给变量
let upperTitle = computed(() => {
    return props['title'].toUpperCase();
})
</script>

校验

当声明props对象时,可以为每个prop添加校验:

  • type:类型,支持String、Number、Array、Boolean、Object、Function、Date、Symbol及自定义类。支持多类型时使用数组形式声明,如:[String, Number]
  • require:是否必传;
  • default:默认值,require为false时有效;
  • validator:自定义校验函数。
<script setup>
defineProps({
    title: {
        type: String,
        require: true
    },
    content: {
        type: String,
        default: '这是一个示例.'
    },
    number: {
        type: Number,
        require: true,
        validator(val) {
            return val > 10;
        }
    }
})
</script>

建议

建议优先声明props对象,并为每个prop设置typerequiredefaultvalidator等校验参数,显示地声明校验参数可以规范prop的使用,并为他人提供一个良好的文档。

事件

声明

使用defineEmits()声明事件:

<script setup>
defineEmits(['change', 'confirm'])
</script>

Tips:defineEmits()只能在setup()中调用。

使用

在父组件中使用v-on:[emit](简写@[emit])监听子组件传递的事件:

<template>
    <ChildComponent @change="childItemChange" />
</template>

在子组件中抛出事件以供父组件监听:

<script setup>
const emits = defineEmits(['change', 'confirm'])
emits('change', 1, 2)
</script>

除第一个参数以外,emits()接收到的所有参数都会传递给父组件的监听:

<template>
    <ChildComponent @change="childItemChange" />
</template>

<script setup>
function childItemChange(data1, data2) {
    console.log(data1); // 1
    console.log(data2); // 2
}
</script>

校验

可以将事件赋值为一个函数,接收传入的参数进行校验后返回校验是否通过:

<script setup>
const emits = defineEmits({
    change(data1, data2) {
        if (data1 > data2) {
            return true
        } else {
            console.warn('校验失败')
            return false
        }
    }
})

emits('change', 1, 2) // 控制台输出:校验失败
</script>

注意:校验通过与否该事件及参数都会向上传递给父组件。

补充

在模板中可以使用$emit('[事件名]'[,参数])向父组件传递一个事件:

<template>
    <input :input="$event => $emit('input', $event.target.value)" />
</template>

双向数据绑定

单个v-model

在父组件中使用v-model为子组件进行双向数据绑定:

<ChildComponent v-model="number" />

在子组件中要使用props、emit实现number的绑定及更新:

<template>
    <input :value="modelValue" @input="$event => $emit('update:modelValue', $event.target.value)" />
</template>

<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue'])
</script>

多个v-model

在父组件中使用v-model:[名称]进行多个双向数据绑定:

<template>
    <ChildComponent v-model:number="number" v-model:type="type" />
</template>

在子组件中需要声明多个prop和emit实现数据的绑定及更新:

<template>
    <input :value="number" @input="$event => $emit('update:number', $event.target.value)" />
    <input :value="type" @input="$event => $emit('update:type', $event.target.value)" />
</template>

<script setup>
defineProps(['number', 'type']);
defineEmits(['update:number', 'update:type'])
</script>

修饰符

v-model后同样可以跟trim等修饰符,若要跟自定义修饰符,需要在子组件中实现相应逻辑:

<!-- 父组件 -->
<template>
    <ChildComponent v-model.double="number" />
</template>
<!-- 子组件 -->
<template>
    <input :value="modelValue" @input="numberChange" />
</template>

<script setup>
const props = defineProps(['modelValue', 'modelModifiers']);
const emits = defineEmits(['update:modelValue']);

function numberChange(e) {
    let value = e.target.value
    if (props.modelModifiers.double) {
        value = value * 2
    }
    emits('update:modelValue', value)
}
</script>

总结

  1. prop命名规范:与v-model:后跟的参数名一致,若为单个v-model,则使用modelValue

  2. emit命名规范:update:+v-model:后跟的参数名,若为单个v-model,则使用update:modelValue

  3. 修饰符命名规范:v-model:后跟的参数名+Modifiers,若为单个v-model,则使用modelModifiers

属性透传

在父组件中所有写在子组件上而未在子组件中使用defineProps()defineEmits()声明的属性都将透传并合并到子组件的根节点上(假如子组件只有一个根节点):

<!-- 父组件 -->
<template>
    <ChildComponent class="parant-class" @input="change" />
</template>
<script setup>
import ChildComponent from '../components/ChildComponent.vue';

function change(e) {
    console.log(e.target.value)
}
</script>

<style scoped>
.parent-class {
    color: red
}
</style>
<!-- 子组件 -->
<template>
    <input class="child-class" />
</template>

<style scoped>
.child-class {
    font-size: 20px;
}
</style>

在上述示例中,父组件中写在ChildComponent上的class属性和input事件未在ChildComponent中定义props或emits,但自动透传并合并到了子组件的文本框上,故文本框渲染后同时满足parent-classchild-class样式,且父组件中可以使用文本框的input事件,等效于:

<template>
    <input class="parant-class child-class" @input="change" />
</template>

<script setup>
function change(e) {
    console.log(e.target.value)
}
</script>

<style scoped>
.parent-class {
    color: red
}

.child-class {
    font-size: 20px;
}
</style>

若子组件中仅包含另一个子组件,则透传的属性将继续透传到另一个子组件。

禁止透传属性

若不想子组件使用父组件透传的属性,可以在子组件中使用inheritAttrs: false禁止透传:

<script>
export default {
    inheritAttrs: false
}
</script>

<script setup>

</script>

控制透传属性的使用

当子组件有多个根节点或想将透传属性使用到其他子节点上时,需要用v-bind将透传属性绑定到某个节点:

<template>
    <input class="child-class" />
    <button v-bind="$attrs" />
</template>

在JavaScript中可以使用useAttrs()访问透传属性

插槽

插槽使用<slot>标签定义,通过name属性为插槽赋予名字,方便区分。在父组件中使用#[插槽名]向子组件插入模板:

<!-- 子组件 -->
<template>
     <div>
        <slot name="header"></slot>
    </div>
    <div>
        <slot name="default"></slot>
    </div>
    <div>
        <slot name="footer"></slot>
    </div>
</template>
<!-- 父组件 -->
<template>
    <ChildComponent>
        <template #header>
            这是header插槽
        </template>
        <template #default>
            这是default插槽
        </template>
        <template #footer>
            这是footer插槽
        </template>
    </ChildComponent>
</template>

插槽还可以提供默认内容,当父组件未使用该插槽时,将渲染默认内容:

<!-- 子组件 -->
<template>
     <div>
        <slot name="header">Header</slot>
    </div>
</template>
<!-- 父组件 -->
<template>
    <ChildComponent/>
</template>

当组件只有一个插槽时,可不为插槽具名。在父组件中使用时,被子组件包裹起来的内容将自动插入到插槽内:

<!-- 子组件 -->
<template>
     <div>
        <slot></slot>
    </div>
</template>
<!-- 父组件 -->
<template>
    <ChildComponent>这是default插槽的简写</ChildComponent>
</template>

访问作用域

插槽内容仅可访问父组件中的数据,若想访问子组件中的数据,需要在插槽中通过prop输出:

<!-- 子组件 -->
<template>
    <div>
        <slot title="默认插槽作用域访问示例"></slot>
    </div>
</template>

<!-- 父组件 -->
<template>
    <ChildComponent v-slot="slotProps">
        {{ slotProps.title }}
    </ChildComponent>
</template>
<!-- 子组件 -->
<template>
    <div>
        <slot name="header" title="具名插槽作用域访问示例"></slot>
    </div>
</template>

<!-- 父组件 -->
<template>
    <ChildComponent #header="slotProps">
        {{ slotProps.title }}
    </ChildComponent>
</template>

<!-- 解构的写法 -->
<template>
    <ChildComponent #header="slotProps">
        {{ slotProps.title }}
    </ChildComponent>
</template>