组件基础
组件注册:一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。
组件注册有两种方式:全局注册和局部注册。
全局注册
<script>
import { createApp } from 'vue'
import ComponentA from './App.vue'
import ComponentB from './App.vue'
const app = createApp({})
app.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB) //可以链式调用
</script>
局部注册
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
Props声明
defineProps({
greetingMessage: String //命名建议camelCase
})
<MyComponent greeting-message="hello" /> //命名建议 kebab-case形式
任何类型的值都可以作为 props 的值被传递
单向数据流
所有的 props 都遵循着单向绑定原则
这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
Prop 校验
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
注意:
Type类型可以是:String、Number、Boolean、Array、Object、Date、Function、Symbol
所有 prop 默认都是可选的,除非声明了 required: true。
除 Boolean 外的未传递的可选 prop 将会有一个默认值 undefined
Boolean 类型的未传递 prop 将被转换为 false。你应该为它设置一个 default 值来确保行为符合预期。
defineProps() 宏中的参数不可以访问 <script setup> 中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data、computed 等) 在 default 或 validator 函数中是不可用的
Boolean 类型转换
defineProps({
disabled: Boolean
})
<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />
<!-- 等同于传入 :disabled="false" -->
<MyComponent />
组件中的事件传递
<!-- 子组件 BlogPost.vue -->
<script setup>
const props = defineProps(['title']) //定义props参数
const emit = defineEmits(['enlarge-text']) //定义emit事件
console.log(props.title) //打印参数
</script>
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>
<!--父组件调用-->
const postFontSize = ref(1)
<BlogPost
title="My journey with Vue"
@enlarge-text="postFontSize += 0.1" //组件中自定义事件
/>
//选项形式写法
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}
emit传递参数
<button @click="$emit('increaseBy', 1)">
Increase by 1
</button>
<MyButton @increase-by="(n) => count += n" />
emit 参数校验
<script setup>
const emit = defineEmits({
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})
function submitForm(email, password) {
emit('submit', { email, password })
}
</script>
注意事项:
和原生 DOM 事件不一样,组件触发的事件没有冒泡机制
透传 Attributes
一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 props 或 emits 定义的 attribute。常见的示例包括 class、style 和 id attribute。可以通过 $attrs property 访问那些 attribute。
当组件返回单个根节点时,非 prop 的 attribute 将自动添加到根节点的 attribute 中。
<!-- 具有非 prop 的 attribute 的 date-picker 组件-->
<date-picker data-status="activated"></date-picker>
<!-- 渲染后的 date-picker 组件 -->
<div class="date-picker" data-status="activated">
<input type="datetime-local" />
</div>
同样的规则也适用于事件监听器
<div id="date-picker" class="demo">
<date-picker @change="showChange"></date-picker>
</div>
app.component('date-picker', {
template: `
<select>
<option value="1">Yesterday</option>
<option value="2">Today</option>
<option value="3">Tomorrow</option>
</select>
`,
methods: {
showChange(event) {
console.log(event.target.value) // 将打印所选选项的值
}
}
})
禁用 Attribute 继承
如果你不希望组件的根元素继承 attribute,可以在组件的选项中设置 inheritAttrs: false。
app.component('date-picker', {
inheritAttrs: false,
template: `<div class="date-picker"><input type="datetime-local" v-bind="$attrs" /></div>`
})
<!-- date-picker 组件使用非 prop 的 attribute -->
<date-picker data-status="activated"></date-picker>
<!-- 渲染后的 date-picker 组件 -->
<div class="date-picker">
<input type="datetime-local" data-status="activated" />
</div>
在组件上使用v-model
默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。比如:
<custom-input v-model="searchText"></custom-input>
相当于:
<custom-input
:model-value="searchText"
@update:model-value="searchText = $event"
></custom-input>
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>`
})
我们可以通过向 v-model 传递参数来修改这些名称,如:
<my-component v-model:title="bookTitle"></my-component>
app.component('my-component', {
props: {
title: String
},
emits: ['update:title'],
template: `
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"> `
})
多个model绑定(写自定义组件或者插件时可能会用到)
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>
app.component('user-name', {
props: {
firstName: String,
lastName: String
},
emits: ['update:firstName', 'update:lastName'],
template: `
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)">
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)">
`
})
插槽
渲染作用域:该插槽可以访问与模板其余部分相同的实例 property (即相同的“作用域”)。
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
备用内容(默认内容):它只会在没有提供内容的时候被渲染,如果我们提供内容,则这个提供的内容将会被渲染从而取代备用内容
简单用法:
<!-- 组件内容 -->
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
<!-- 组件调用 -->
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
<!-- 此时会渲染成 -->
<button class="fancy-btn">Click me!</button>
具名插槽
组件在slot上添加name属性,父组件模板调用v-slot:,其简写为 #
如定义<base-layout> 组件:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
父组件调用
<base-layout>
<template v-slot:header> //可简写为<template #header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default> //可简写为<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
注意,v-slot 只能添加在 <template>
作用域插槽(插槽props参数传递)
目的:是让父组件中插槽的内容能够访问子组件的数据(子组件solt向父组件中传参)
子组件定义
app.component('todo-list', {
data() {
return {
items: ['Feed a cat', 'Buy milk']
}
},
template: `
<ul>
<li v-for="(item, index) in items">
<slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot> //参数传递
</li>
</ul>
`
})
绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在,在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:
父组件中使用v-slot接收
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
</todo-list>
<!-- 如果只要一个默认插槽,可简写为-->
<todo-list v-slot="slotProps"> // 另一种写法<todo-list #default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</todo-list>
<!-- 多个的话,需要分开写-->
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</todo-list>
<!-- 结合结构解析-->
<todo-list v-slot="{ item }"> //v-slot="{ item: todo }" 结构解析重命名 v-slot="{ item = 'Placeholder' }" 结构解析的默认值
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
依赖注入 Provide / Inject
父组件 provide 选项来提供数据,子组件inject 选项使用这些数据。
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
注意:当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。
可以使用 readonly()
来包装提供的值。表示只读如:
const count = ref(0)
provide('read-only-count', readonly(count))
动态组件
动态组件可以使用is attribute 来切换不同组件
如
<div id="dynamic-component-demo" class="demo">
<button
v-for="tab in tabs"
:key="tab"
:class="['tab-button', { active: currentTab === tab }]"
@click="currentTab = tab"
>
{{ tab }}
</button>
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
</div>
const app = Vue.createApp({
data() {
return {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
},
computed: {
currentTabComponent() {
return 'tab-' + this.currentTab.toLowerCase()
}
}
})
app.component('tab-home', {
template: `<div class="demo-tab">Home component</div>`
})
app.component('tab-posts', {
template: `<div class="demo-tab">posts component</div>`,
})
app.component('tab-archive', {
template: `<div class="demo-tab">Archive component</div>`
})
app.mount('#dynamic-component-demo')
异步组件
defineAsyncComponent 方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve 回调方法应该在从服务器获得组件定义时调用。
<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
</script>
<template>
<AdminPage />
</template>
搭配 Suspense 使用
可参考:https://cn.vuejs.org/guide/built-ins/suspense.html