注册
全局注册
在项目的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>
建议
建议优先使用局部注册,这样做的好处有两个:
- 未被引用的组件在生产打包时将自动移除(tree-shaking),极大地减少包体积;
- 显示地引入组件方便定位子组件的实现,组件间的依赖关系清晰,利于长期的维护。
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设置type、require、default、validator等校验参数,显示地声明校验参数可以规范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>
总结
-
prop命名规范:与
v-model:后跟的参数名一致,若为单个v-model,则使用modelValue; -
emit命名规范:
update:+v-model:后跟的参数名,若为单个v-model,则使用update:modelValue; -
修饰符命名规范:
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-class和child-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>