组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。
1、定义组件
单文件组件(SFC)
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue
文件中,这被叫做单文件组件 (简称 SFC),推荐为子组件使用 PascalCase
的标签名。
<template>
<div @click="count ++">{{ count }}</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
javascript组件
当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
// 这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
// 也可以针对一个 DOM 内联模板:
// template: '#my-template-element'
}
2、组件注册
全局注册
全局注册的组件可以在应用的任意组件的模板中使用。注册时使用vue应用实例的component方法即可:
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './MyComponent.vue'
const app = createApp(App)
app.component('MyComponent', MyComponent)
局部注册
局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它使组件之间的依赖关系更加明确。
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
3、Props声明
一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,声明方式如下:
<script setup lang="ts">
// 字符串数组
// const props = defineProps(['foo'])
// 对象形式
// const props = defineProps({
// title: String
// })
// 类型声明
const props = defineProps<{
title: String,
age?: number,
dispalyName?: String
}>()
// 将类型的声明移动到单独的接口中
// interface Props {
// title: String,
// age?: number
// }
// const props = defineProps<Props>()
</script>
父组件传递prop方式如下:
<template>
<!-- 静态传值 -->
<DemoComponent title="标题" dispaly-name="显示名称"/>
<!--
根据一个变量的值动态传入
-->
<DemoComponent :title="title" />
<!--
1、支持表达式
2、任何类型的值都可以作为 props 的值被传递
3、当传递数字、boolean、数组、对象常量的时候,也需要使用:进行绑定
-->
<DemoComponent :title="'title: ' + title" :age="15" :ids="[1, 2, 3]" :obj="{t: 1}" />
<!--
可以使用没有参数的 v-bind,传递一个对象来绑定多个prop、
等价于:<DemoComponent title="title" :age="15" />
-->
<DemoComponent v-bind="{title: 'title', age: 15}" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import DemoComponent from './DemoComponent.vue'
const title = ref('')
</script>
注意:
(1)prop的名称应使用 camelCase 形式;
(2)虽然prop名称是 camelCase 形式,但是组件传递props时应使用kebab-case 形式,主要是为了保证和 HTML attribute 对齐;
(3)所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递;
(4)子组件的prop是只读的,且每次父组件更新后,所有的子组件中的 props 都会被更新到最新值;因此,不能在子组件中去更改一个 prop;如果需要进行调整,最好是新定义一个局部数据属性,或者使用computed做进一步的转换。
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这些原生构造函数,也可以是自定义的类或构造函数,Vue 将会通过 instanceof
来检查类型是否匹配:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
}
defineProps({
author: Person
})
4、事件
$emit
在组件的模板表达式中,可以直接使用 $emit
方法触发自定义事件 。
<button @click="$emit('beforeSubmit')">click me</button>
defineEmits
在<script setup>中触发事件,需要使用defineEmits来显示的声明需要触发的事件,不能使用$emit;
<script setup lang="ts">
// 数组字符串声明触发的事件名称
// const emit = defineEmits(["beforeSubmit"])
// 对触发事件的参数进行验证
// const emit = defineEmits({
// // 没有校验
// click: null,
// // 通过返回值为 `true` 还是为 `false` 来判断验证是否通过
// beforeSubmit: ({ email, password }) => {
// if (email && password) {
// return true
// } else {
// console.warn('Invalid submit event payload!')
// return false
// }
// }
// })
/**
* 基于类型进行申明
* e:表示触发事件的名称
* 后续参数表示事件的参数
*/
const emit = defineEmits<{
(e: 'beforeSubmit'): void,
(e: 'update', name: string): void
}>()
</script>
父组件监听
父组件可以通过 v-on
(缩写为 @
) 来监听事件:
<DemoComponent @before-submit="callback" />
注意:在子组件中,以 camelCase 形式命名事件;在父组件中,以kebab-case 形式来监听。
5、v-model
双向绑定
v-model
可以在组件上使用以实现双向绑定。默认情况下,v-model
在组件上都是使用 modelValue
作为 prop,并以 update:modelValue
作为对应的事件,但是我们可以通过给 v-model
指定一个参数来更改这些名字。
子组件写法:
<template>
<div @click="updateModelValue">v-model双向绑定数据:{{ props.modelValue }}</div>
<div @click="updateTitle">指定参数名称:{{ props.title }}</div>
</template>
<script setup lang="ts">
const props = defineProps(['modelValue', 'title'])
const emits = defineEmits(['update:modelValue', 'update:title'])
/**
* 修改父组件指定的v-model的值
*/
const updateModelValue = () => {
emits('update:modelValue', 'new Name');
}
/**
* 改变指定参数名称的值
*/
const updateTitle = () => {
emits('update:title', 'new title');
}
</script>
父组件:
<template>
<!-- 双向绑定 -->
<DemoComponent v-model="name" />
<!-- 等价于上面的v-model="name" -->
<DemoComponent
:modelValue="name"
@update:modelValue="newValue => name = newValue"
/>
<!-- 指定参数名称 -->
<DemoComponent v-model:title="title" />
<!-- 等价于上面的v-model:title="title" -->
<DemoComponent
:title="title"
@update:title="newValue => title = newValue"
/>
<!-- 可以同时有多个,会同步不同的 prop -->
<DemoComponent v-model="name" v-model:title="title"/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import DemoComponent from './DemoComponent.vue'
const title = ref('')
const name = ref('')
</script>
定义修饰符
在处理v-model时,我们可以定义一些修饰符,为了能够处理这些修饰符,我们需要在子组件的prop中定义对应的参数来接收指定的修饰符。
父组件:
<template>
<!-- 处理修饰符 -->
<DemoComponent v-model.capitalize="name" />
<!-- 带参数名称的处理修饰符 -->
<DemoComponent v-model:title.capitalize="title" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import DemoComponent from './DemoComponent.vue'
const title = ref('')
const name = ref('')
</script>
子组件:
<script setup lang="ts">
const props = defineProps({
modelValue: String,
// 接收v-model定义的修饰符
modelModifiers: { default: () => ({}) },
title: String,
// 接收指定参数定义的修饰符,格式为:参数名 + "Modifiers"
titleModifiers: { default: () => ({}) }
})
console.log(props.modelModifiers) // { capitalize: true }
console.log(props.titleModifiers) // { capitalize: true }
</script>
6、插槽
子组件:
<template>
<div class="slot_wrapper">
<slot :info="defaultInfo">
<!-- 可以没有默认内容 -->
默认内容,当父组件没有提供任何插槽内容时,将显示该信息
</slot>
<!--
使用name属性,指定具名插槽,父组件使用时需指定插槽名称
-->
<br />
<slot name="header"></slot>
<br />
<slot name="content" :info="info" :obj="defaultInfo"></slot>
<br />
<!--
作用域插槽
可以将子组件的数据共享给父组件,像对组件传递 props 那样,向一个插槽的出口上传递 attributes
-->
<slot name="msg" :info="info" :obj="defaultInfo"></slot>
</div>
<p />
</template>
<script setup lang="ts">
const info = "子组件消息";
const defaultInfo = {
text: '显示消息',
value: 0
}
</script>
父组件:
<template>
<!-- 不提供插槽数据,此时会显示默认内容 -->
测试一:<DemoComponent></DemoComponent>
测试二:
<DemoComponent>
父组件提供了插槽数据,将不会显示默认内容
</DemoComponent>
测试三:
<DemoComponent>
<!--
指定插槽名称,使用具名插槽
格式为:v-slot:[插槽的名称]
-->
<template v-slot:header>
我是header插槽内容
</template>
<!-- v-slot可以简写成# -->
<template #content>
我是content插槽内容
</template>
<!--
#default代表默认插槽
并且当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容
<template #default>
我是默认插槽
</template>
-->
<div>
我是位于顶级的非 template 节点,视为默认插槽的内容,和#default写法意义相同
</div>
</DemoComponent>
测试四:
<DemoComponent>
<!--
动态插槽名称
注意这里的表达式和动态指令参数受相同的语法限制
-->
<template v-slot:[dynamicHeaderName]>
我是header插槽内容
</template>
<template #[dynamicSlotName]>
我是content插槽内容
</template>
</DemoComponent>
测试五:
<DemoComponent>
<!-- 接受所有具名插槽上的消息数据 -->
<template #msg="info">
子组件全部内容:{{ info }} -- {{ info.info }}
</template>
<!-- 显示的指定需要接受消息的名称 -->
<template #content="{ info }">
子组件指定内容:{{ info }}
</template>
<!-- 默认插槽内容 -->
<template #default="{ info }">
{{ info }}
</template>
</DemoComponent>
测试六:
<DemoComponent v-slot="{ info }">
{{ info }}
默认插槽传递的消息可以直接放在组件上进行接收。
但是如有有其他的具名插槽需要展示的时候,则需要通过上面template方式进行接收。
</DemoComponent>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import DemoComponent from './DemoComponent.vue'
const dynamicSlotName = "content";
const dynamicHeaderName = "header";
</script>
最终页面输出效果:
测试一:
默认内容,当父组件没有提供任何插槽内容时,将显示该信息
测试二:
父组件提供了插槽数据,将不会显示默认内容
测试三:
我是位于顶级的非 template 节点,视为默认插槽的内容,和#default写法意义相同
我是header插槽内容
我是content插槽内容
测试四:
默认内容,当父组件没有提供任何插槽内容时,将显示该信息
我是header插槽内容
我是content插槽内容
测试五:
{ "text": "显示消息", "value": 0 }
子组件指定内容:子组件消息
子组件全部内容:{ "info": "子组件消息", "obj": { "text": "显示消息", "value": 0 } } -- 子组件消息
测试六:
{ "text": "显示消息", "value": 0 } 默认插槽传递的消息可以直接放在组件上进行接收。 但是如有有其他的具名插槽需要展示的时候,则需要通过上面template方式进行接收。
7、依赖注入
通常情况下,我们需要将数组从父组件传递到子组件时,会使用props。但是比如有多层级组件依赖,如A-->B-->C--D...;这种情况下,如果使用props,就会存在下面问题:
(1)必须沿着组件链逐级传递下去,这会非常麻烦;
(2)链路中间组件(如B)根本就不关心这些props,但为了使C组件能访问到这些props,仍需要定义并向下进行传递;
(3)如果组件链路非常长,可能会影响到更多这条链路上的组件。
这种现象称作为prop逐级传透。依赖注入功能则是为了帮助我们解决这个问题。
<script setup lang="ts">
import { provide, inject, ref, readonly } from 'vue'
import type { InjectionKey } from 'vue'
/**
* 使用provide函数为组件后代提供数据
* 第一个参数: 注入名,可以是一个字符串或者一个Symbol,后代组件会用注入名查找期望注入的值
* 第二个参数: 注入值,可以是任何类型,包括响应式的状态
*
* 除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
* const app = createApp({})
* app.provide('message', 'hello!')
* 在应用级别提供的数据在该应用内的所有组件中都可以注入。
*
*
* 注意:
* 1. 父组件的provide函数调用必须在render函数之前,否则无效
* 2. 后代组件使用inject函数注入数据,注入名必须和父组件的provide函数的第一个参数一致
* 3. 后代组件的inject函数调用必须在setup函数之前,否则无效
* 4. 注入的数据是响应式的,如果注入的是一个对象,那么后代组件修改对象属性,父组件也会同步更新
* 6. 不要在provide和inject函数中使用任何计算属性,它们只会增加复杂度,并且会创建一个依赖链,使得更新变量变得困难
*/
provide('message', 'hello');
/**
* 尽可能将任何对响应式状态的变更都保持在供给方组件中,确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护
*/
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
/**
* 类型标注
*
* 1、最好使用 Symbol 来作为注入名以避免潜在的冲突
* 2、建议将注入 key 的类型放在一个单独的文件中,这样它就可以被多个组件导入。
*/
const key = Symbol() as InjectionKey<string>
provide(key, 'foo')
const foo = inject<string>(key);
/**
* 使用readonly函数将一个响应式状态转换为只读状态,确保提供的数据不能被注入方的组件更改
*/
const count = ref(0)
provide('read-only-count', readonly(count))
/**
* 使用inject函数获取注入的数据
*
* 注意:
* 1. 如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。
*/
const message = inject('message')
</script>
Comments NOTHING