文章目录
- 组件注册
- 全局注册
- 局部注册
- 组件中的props
- 格式
- 单向数据
- 校验
- 组件中的事件
- 使用
- 传参
- 声明事件
- 校验
- 组件上的v-model
- 使用
- 携带参数
- 多个v-model
- 处理修饰符
- 透传 Attributes
- 简单使用
- 禁用透传
- 多个继承
- 动态组件
- 介绍
- 使用
- KeepAlive
- 包含
- 缓存生命周期
- 插槽
- 使用
- 默认内容
- 具名插槽
- 条件插槽
- 作用域插槽
- 具名作用域插槽
- 异步组件
- 基本用法
- 添加配置
组件注册
组件在使用前需要先被注册
全局注册
全局注册的组件在其他组件中可以不用导入,直接使用
//main.js
import { createApp } from 'vue'
const app = createApp({})
import MyComponent from './MyComponent.vue'
app.component('MyComponent', MyComponent)
可以注册多个组件
app
.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC)
在其他组件中直接使用
<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>
全局注册的缺点
1.没有被使用的组件无法在生产打包时被自动移除 。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
2.依赖关系不明显,影响长期可维护性
局部注册
局部注册需要使用 components 选项:
<script>
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
}
}
</script>
<template>
<ComponentA />
</template>
mponentA 注册后仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可用。
命名格式
使用驼峰命名法(PascalCase)
原因:
1.PascalCase是合法js命名符
2. 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。
但是PascalCase 的标签名在 DOM 内模板中是不可用的。
vue将其解析为kebab-case,所以我们在模板中可以通过 或 引用
组件中的props
组件需要使用props选项进行接受
export default {
props: ['myprop'],
created() {
// props 会暴露到 `this` 上
console.log(this.myprop)
}
}
我们也可以使用对象形式声明
export default {
props: {
title: String, // key-value prop名字-类型
likes: Number
}
}
如果传递错误类型,控制台将抛出警告
格式
使用 camelCase 形式
export default {
props: {
getMessage: String
}
}
<span>{{ getMessage }}</span>
在向子组件传递props时也可以使用camelCase 形式
但是为了和HTML attribute 对齐,通常写为kebab-case 形式
<MyComponent get-message="hello" />
传递的props可以是静态或者动态
<BlogPost title="My journey with Vue" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" />
<!-- 根据一个更复杂表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />
单向数据
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。
避免子组件改变父组件
export default {
props: ['foo'],
created() {
// 发出警告,不能修改
this.foo = 'bar'
}
}
更改props的方法
1.在data中定义值接收
export default {
props: ['initialCounter'],
data() {
return {
// 计数器只是将 this.initialCounter 作为初始值
// 像下面这样做就使 prop 和后续更新无关了
counter: this.initialCounter
}
}
}
2.使用computed响应式更新
export default {
props: ['size'],
computed: {
// 该 prop 变更时计算属性也会自动更新
normalizedSize() {
return this.size.trim().toLowerCase()
}
}
}
校验
要声明对 props 的校验,你可以向 props 选项提供一个带有 props 校验选项的对象,例如:
export default {
props: {
// 基础类型检查
//(给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// 必传但可为 null 的字符串
propD: {
type: [String, null],
required: true
},
// Number 类型的默认值
propE: {
type: Number,
default: 100
},
// 对象类型的默认值
propF: {
type: Object,
// 对象或者数组应当用工厂函数返回。
// 工厂函数会收到组件所接收的原始 props
// 作为参数
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
// 在 3.4+ 中完整的 props 作为第二个参数传入
propG: {
validator(value, props) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propH: {
type: Function,
// 不像对象或数组的默认,这不是一个
// 工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
}
}
//所有 prop 默认都是可选的,除非声明了 required: true。
//除 Boolean 外的未传递的可选 prop 将会有一个默认值 undefined。
//Boolean 类型的未传递 prop 将被转换为 false。这可以通过为它设置 default 来更改——例如:设置为 default: undefined 将与非布尔类型的 prop 的行为保持一致。
//如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。
组件中的事件
使用
事件常用于组件通信
在子组件中
<!-- MyComponent -->
<button @click="$emit('someEvent')">Click Me</button>
//其他形式
export default {
methods: {
submit() {
this.$emit('someEvent')
}
}
}
父组件监听
<MyComponent @some-event="callback" />
传参
有时候我们会需要在触发事件时附带一个特定的值。可以使用这种形式
<button @click="$emit('sum', 1)">
sum + 1
</button>
在父组件中监听事件
<MyButton @sum="(n) => count += n" />
//其他形式
<MyButton @sum="increaseCount" />
methods: {
increaseCount(n) {
this.count += n
}
}
声明事件
通过 emits 选项来声明它要触发的事件:
export default {
emits: ['inFocus', 'submit']
}
//emits 选项和 defineEmits() 宏还支持对象语法
校验
都是在子组件中进行校验
要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 this.$emit 的内容,返回一个布尔值来表明事件是否合法。
示例
export default {
emits: {
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm(email, password) {
this.$emit('submit', { email, password })
}
}
}
组件上的v-model
v-model 可以在组件上使用以实现双向绑定。
使用
v-model 会被展开为如下的形式:
<CustomInput
:model-value="searchText"
@update:model-value="newValue => searchText = newValue"
/>
这个组件做了两个事
将内部原生 元素的 value attribute 绑定到 modelValue prop 当原生的 input
事件触发时,触发一个携带了新值的 update:modelValue 自定义事件
在CustomInput中
<!-- CustomInput.vue -->
<script>
export default {
props: ['modelValue'], //接受参数
emits: ['update:modelValue'] //触发事件
}
</script>
<template>
<input
:value="modelValue" //将value绑定在input上
@input="$emit('update:modelValue', $event.target.value)"
/> //如果改变,触发事件,将最新的值传递到父组件
</template>
另一种是使用computed的get和set实现
<!-- CustomInput.vue -->
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
computed: {
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
}
}
</script>
<template>
<input v-model="value" />
</template>
携带参数
//组件上的 v-model 也可以接受一个参数:
<MyComponent v-model:title="bookTitle" />
子组件应该使用 title prop 和 update:title 事件来更新父组件的值
<!-- MyComponent.vue -->
<script>
export default {
props: ['title'],
emits: ['update:title']
}
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
多个v-model
组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:
<script>
export default {
props: {
firstName: String,
lastName: String
},
emits: ['update:firstName', 'update:lastName']
}
</script>
<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)"
/>
</template>
处理修饰符
如何处理 .trim,.number 和 .lazy修饰符呢?
//创建一个去除空格的修饰符
<MyComponent v-model.trim="myText" />
添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件。
<script>
export default {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
created() {
console.log(this.modelModifiers) // { trim: true }
}
}
</script>
<template>
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
我们已经为组件配置了 prop,我们可以检查 modelModifiers 对象的键并编写一个处理程序来更改抛出的值。在下面的代码中,每当 元素触发 input 事件时,我们都会去除空格。
<script>
export default {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
methods: {
emitValue(e) {
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
}
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
带参数的修饰符如何绑定呢?
<UserName
v-model:first-name.capitalize="first"
v-model:last-name.uppercase="last"
/>
<script>
export default {
props: {
firstName: String,
lastName: String,
firstNameModifiers: {
default: () => ({})
},
lastNameModifiers: {
default: () => ({})
}
},
emits: ['update:firstName', 'update:lastName'],
created() {
console.log(this.firstNameModifiers) // { capitalize: true }
console.log(this.lastNameModifiers) // { uppercase: true }
}
}
</script>
透传 Attributes
简单使用
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和 id。
最常见的就是传递class样式
一个父组件使用了这个组件,并且传入了 class:
<MyButton class="large" />
最后渲染出的 DOM 结果是:
<button class="large">Click Me</button>
监听器也能透传
<MyButton @click="onClick" />
click 监听器会被添加到 的根元素,即那个原生的 元素之上。当原生的 被点击,会触发父组件的 onClick 方法。同样的,如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。
禁用透传
如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false。
这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到。
<span>Fallthrough attribute: {{ $attrs }}</span>
这个 $attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 class,style,v-on 监听器等等。
有几点需要注意:
和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过 $attrs[‘foo-bar’] 来访问。
像 @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick。
我们想要所有像 class 和 v-on 监听器这样的透传 attribute 都应用在内部的 上而不是外层的
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">Click Me</button>
</div>
多个继承
和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs 没有被显式绑定,将会抛出一个运行时警告。
<CustomLayout id="custom-layout" @click="changeValue" />
但是如果子组件有多个根节点,将会抛出警告
//在需要的地方被显示绑定,就不会有警告了
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
动态组件
介绍
动态组件指的是动态切换组件的显示与隐藏。
使用
<component :is="activeComponent" />
可以通过控制activeComponent的值来显示不同的组件,常用于tab栏切换
但是一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态——当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。
遇到这种情况该怎么解决呢?这时候就需要内置组件KeepAlive
KeepAlive
是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。
<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
<component :is="activeComponent" />
</KeepAlive>
这样数据就不会丢失了
包含
KeepAlive默认缓存所有的组件实例
我们可以通过 include 和 exclude prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:
<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
<component :is="view" />
</KeepAlive>
<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
<component :is="view" />
</KeepAlive>
<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
<component :is="view" />
</KeepAlive>
缓存生命周期
当一个组件实例从 DOM 上移除但因为被 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。
一个持续存在的组件可以通过 activated 和 deactivated 选项来注册相应的两个状态的生命周期钩子:
export default {
activated() {
// 在首次挂载、
// 以及每次从缓存中被重新插入的时候调用
},
deactivated() {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
}
}
activated 在组件挂载时也会调用,并且 deactivated 在组件卸载时也会调用。
这两个钩子不仅适用于 缓存的根组件,也适用于缓存树中的后代组件。
插槽
使用
在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
在AboutView组件中
渲染出来的效果
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
原理:
和js函数类比
// 父元素传入插槽内容
FancyButton('Click me!') //传递参数
// FancyButton 在自己的模板中渲染插槽内容
function FancyButton(slotContent) { //接收参数渲染到模板字符串内
return `<button class="fancy-btn">
${slotContent}
</button>`
}
插槽不限制形式,组件也可以传入
<FancyButton>
<span style="color:red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>
优点:通过使用插槽,组件更加灵活和具有可复用性。现在组件可以用在不同的地方渲染各异的内容,但同时还保证都具有相同的样式。
默认内容
在父组件中没有传递内容
渲染效果
如果父组件提供内容了,默认内容将会被替代
具名插槽
假如子组件中有多个根节点
<div class="container">
<header>
<!-- 标题内容放这里 -->
</header>
<main>
<!-- 主要内容放这里 -->
</main>
<footer>
<!-- 底部内容放这里 -->
</footer>
</div>
不同的根节点想要接收不同的内容,对于这种场景,要使用具名插槽,
用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name 的 出口会隐式地命名为“default”。
要给具名插槽传递内容,在父组件中需要使用v-slot指令
<BaseLayout>
<template v-slot:header>
// 简写形式 <template #header>
// 动态插槽名 <template #[dynamicSlotName]>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
template中的所有内容会被传递到相应的插槽
条件插槽
有时你需要根据插槽是否存在来渲染某些内容。
你可以结合使用 $slots 属性与 v-if 来实现。
// 当 header、footer 或 default 存在时,我们希望包装它们以提供额外的样式
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div v-if="$slots.default" class="card-content">
<slot />
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
作用域插槽
由于作用域的限制,插槽中的内容无法访问到子组件中的状态。
但是有时候我们需要子组件中的一部分数据,这时候就需要使用作用域插槽。
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象:
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
具名作用域插槽
具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot 指令的值被访问到:v-slot:name=“slotProps”。当使用缩写时是这样:
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
<slot name="header" message="hello"></slot>
异步组件
基本用法
异步组件的使用让我们在仅在需要时再从服务器加载相关组件。
Vue 提供了 defineAsyncComponent 方法来实现此功能:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
异步组件也可以全局注册
app.component('MyComponent', defineAsyncComponent(() =>
import('./components/MyComponent.vue')
))
局部注册
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {
AdminPage: defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
}
}
</script>
<template>
<AdminPage />
</template>
添加配置
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent() 也支持在高级选项中处理这些状态:
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})