组件名使用大驼峰命名 PascalCase ,如 MyName.vue
- 以 MyComponent 为名注册的组件,在模板中可以通过
<MyComponent>
或<my-component>
引用,以便配合不同来源的模板。
组合式 API
定义组件
在一个单独的 .vue 文件中定义
child.vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">点击了 {{ count }} 次</button>
</template>
使用组件
在父组件中
<script setup>
import Child from './child.vue'
</script>
<template>
<Child/>
</template>
父组件给子组件传值
通过给子组件标签自定义属性来传递
<Child title="博客的标题" />
较长的属性名,建议用 kebab-case 形式(为了和 HTML 属性名保持一致),如
<MyComponent greeting-message="hello" />
传数值和布尔类型,必须动态绑定
<BlogPost :likes="42" />
<BlogPost :is-published="false" />
传多个 prop,可以直接绑定一个对象
const post = {
id: 1,
title: 'My Journey with Vue'
}
<BlogPost v-bind="post" />
通常只写 prop 但不传值,会隐式转换为 true
defineProps({
disabled: Boolean
})
<BlogPost is-published />
但若 prop 被声明为允许多种类型时,会有特殊的转换规则
// disabled 将被转换为 true
defineProps({
disabled: [Boolean, Number]
})
// disabled 将被转换为 true
defineProps({
disabled: [Boolean, String]
})
// disabled 将被转换为 true
defineProps({
disabled: [Number, Boolean]
})
// disabled 将被解析为空字符串 (disabled="")
defineProps({
disabled: [String, Boolean]
})
子组件接收父组件的传值 defineProps
子组件必须显式声明接收的父组件传值 props,以便 Vue 区分是否是透传属性
- props 名较长时,使用小驼峰命名法,如 greetingMessage
- defineProps 无需导入,可直接使用
<script setup>
defineProps(['title'])
</script>
- defineProps() 的参数可以是数组,也可以是对象(属性名为 prop 的名称 ,属性值为预期数据类型的构造函数,用于描述数据类型)
defineProps({
title: String,
likes: Number
})
- defineProps() 返回包含所有 props 的对象
const props = defineProps(['title'])
console.log(props.title)
props 校验
defineProps({
// 限定数据类型 -- 字符串
propA: String,
// 多种可能的数据类型 -- 字符串/数值
propB: [String, Number],
// 必传
propC: {
required: true
},
// 可为 null 的字符串
propD: {
type: [String, null],
},
// 可为任意类型
propD2: {
type: null,
},
// 默认值 100
propE: {
default: 100
},
// 对象类型的默认值
propF: {
type: Object,
// 对象或数组的默认值,必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
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'
}
}
})
- 未传递的布尔类型的 props 的值为 false
- 未传递的非布尔类型的 props 的值为 undefined
不要改 props !
不应该在子组件中去更改 prop
若 prop 仅用于传入初始值,需新定义一个响应式变量接收它!
const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)
若需对 prop 值做进一步的转换,如格式化,则用计算属性
const props = defineProps(['size'])
// 该 prop 变更时计算属性也会自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
TS 中的 props
通过泛型参数来定义 props 的类型
<script setup lang="ts">
const props = defineProps<{
foo: string
bar?: number
}>()
</script>
或(用 interface )
<script setup lang="ts">
interface Props {
foo: string
bar?: number
}
const props = defineProps<Props>()
</script>
添加默认值需用 withDefaults
export interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
// 当默认值为引用类型时,需用函数
labels: () => ['one', 'two']
})
withDefaults 帮助程序为默认值提供类型检查,并确保返回的 props 类型删除了已声明默认值的属性的可选标志。
子组件添加自定义事件
组件的自定义事件和原生 DOM 事件不一样,没有冒泡机制
声明自定义事件 defineEmits
- 可以不声明,但推荐声明
- defineEmits 无需导入,可直接使用,但必须直接放置在
<script setup>
的顶级作用域下。 - 自定义事件名称推荐采用 kebab-case 形式(短横线连接)
- 若自定义事件名称和原生事件同名,如 click ,则自定义事件会覆盖原生事件。
<script setup>
defineEmits(['fav'])
</script>
对象的写法
<script setup lang="ts">
const emit = defineEmits({
submit(payload: { email: string, password: string }) {
// 通过返回值为 `true` 还是为 `false` 来判断
// 验证是否通过
}
})
</script>
TS 的写法
<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
为自定义事件添加校验
给自定义事件赋值一个函数,参数为自定义事件的参数,返回一个布尔值来表明事件是否合法。
<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>
触发自定义事件
在模板中触发自定义事件 $emit
<button @click="$emit('fav')">喜欢</button>
传参
<button @click="$emit('fav', 1)">
喜欢
</button>
传更多参数
$emit('fav', 1, 2, 3)
在 JS 中触发自定义事件用 emit 函数
defineEmits() 返回一个等同于 $emit 方法的 emit 函数
<script setup>
const emit = defineEmits(['fav'])
// 触发自定义事件
emit('fav')
</script>
父组件监听子组件的自定义事件
<Child title="博客的标题" @fav="favNum++" />
接收子组件自定义事件的传参
<Child @fav="(n) => favNum+= n" />
或
<Child @fav="favIncrease" />
function favIncrease(n) {
favNum.value += n
}
子组件对外暴露数据 defineExpose
子组件 Child.vue 中
<script setup>
const a = 1
defineExpose({
a
})
</script>
父组件中
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const child_ref = ref(null)
onMounted(() => {
// 打印子组件中的变量 a
console.log(child_ref.value.a)
})
</script>
<template>
<Child ref="child_ref" />
</template>
插槽 slot
用于父组件给子组件传递模板内容
- 插槽可以访问到父组件的数据作用域,无法访问子组件的数据。
父组件
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
子组件
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
最终渲染
<button class="fancy-btn">Click me!</button>
插槽的默认内容
子组件的 <slot>
标签内的内容,即插槽的默认内容(当父组件未传插槽内容时会显示)
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
具名插槽
子组件( <slot>
标签的 name 属性给插槽命名,无name 属性的为默认插槽)
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
父组件( 用 <template>
标签包裹不同插槽的内容,#插槽名称
来与子组件的插槽对应,没被 <template>
标签包裹的内容传递给子组件的默认插槽)
<BaseLayout>
<template #header>
<h1>header的内容</h1>
</template>
<p>默认内容</p>
<template #footer>
<p>footer的内容</p>
</template>
</BaseLayout>
-
#
是指令v-slot
的简写,原写法是<template v-slot:header>
条件插槽
根据插槽是否存在来渲染某些内容,使用 $slots 属性与 v-if 来实现。
<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>
作用域插槽
用于子组件向插槽内传数据。
默认插槽的实现
子组件(向 slot 传 props)
<slot :text="greetingMessage" :count="1"></slot>
父组件(通过 v-slot 接收)
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
改用解构写法更简洁
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
具名插槽的实现
子组件
<slot name="header" message="hello"></slot>
父组件
<template #header="headerProps">
{{ headerProps.message }}
</template>
同时使用具名插槽与默认插槽,需要为默认插槽使用显式的 <template>
标签。
<!-- 子组件 -->
<slot :message="hello"></slot>
<slot name="footer" />
<!-- 父组件 -- 使用显式的默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
跨组件通信 – 依赖注入 provide inject
https://blog.csdn.net/weixin_41192489/article/details/140566761
动态组件 :is
<!-- currentTab 改变时组件也改变 -->
<component :is="currentTab"></component>
currentTab 的值为 被注册的组件名
或 导入的组件对象
应用场景:在多个组件间来回切换,比如 Tab 界面
异步组件 defineAsyncComponent
用于在需要时才加载相关组件,提升性能
本地加载
<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
</script>
<template>
<AdminPage />
</template>
从服务器加载
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
加载与错误状态
通过选项配置组件在加载和报错状态显示的组件和相关逻辑
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
注册全局组件
src/main.ts
import Welcome from '@/components/Welcome.vue'
app.component('Welcome ', Welcome )
可以链式调用
app.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC)
支持异步组件
app.component('MyComponent', defineAsyncComponent(() =>
import('./components/MyComponent.vue')
))
全局注册的缺点
- 没有被使用的组件也会被打包
- 使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现,会影响应用长期的可维护性。
透传属性(属性继承)$attrs
透传属性即没有被组件声明为 props 或 emits 的属性 或者 v-on 事件监听器,如 class、style 和 id。
演示范例
给子组件添加 class 样式
<MyButton class="large" />
子组件仅一个根元素
<button>Click Me</button>
最终效果
<button class="large">Click Me</button>
此时的 class="large"
没有声明为 prop ,但加载到子组件的 button 标签上了!
样式的合并
如果子组件的根元素已经有了 class 或 style ,则它们会和从父组件上继承的值合并。
<!-- 子组件 -->
<button class="btn">Click Me</button>
最终效果
<button class="btn large">Click Me</button>
事件的继承(v-on 监听器继承)
- 父组件中给子组件绑定的事件,会被添加子组件的根元素上
- 若子组件本身就有同类事件,则两个事件都会触发
深层组件继承
子组件的根元素是另一个组件时,透传的属性会直接传给该组件。
禁用属性继承
若不想属性被继承,可按以下方式配置:
// 选项 API
inheritAttrs: false
组合 API vue3.3+
<script setup>
defineOptions({
inheritAttrs: false
})
</script>
使用场景
需要将透传的属性应用在根节点以外的其他元素上。
$attrs 的使用
$attrs 对象中包含了除组件所声明的 props 和 emits 之外的所有其他属性,例如 class,style,v-on 监听器等等。
- 透传属性在 JavaScript 中保留了它们原始的大小写,
foo-bar
需要通过$attrs['foo-bar']
来访问。 - 透传的事件,如
@click
需通过$attrs.onClick
访问
将透传属性应用于指定元素
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">Click Me</button>
</div>
多根节点的属性继承
多根节点的组件不会自动继承透传属性,必须显式绑定,否则会抛出一个运行时警告。
所以,必须显示绑定
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
JS 中获取透传属性
组合 API
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
选项 API
export default {
setup(props, ctx) {
// 透传 attribute 被暴露为 ctx.attrs
console.log(ctx.attrs)
}
}
注意事项
此处的 attrs 对象总是反映为最新的透传 attribute,但它并不是响应式的 (考虑到性能因素)。你不能通过侦听器去监听它的变化。如果你需要响应性,可以使用 prop。或者你也可以使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用。
选项式 API
定义组件
在一个单独的 .vue 文件中定义
child.vue
<script>
export default {
data() {
return {
count: 0
}
}
}
</script>
<template>
<button @click="count++">点击了 {{ count }} 次 </button>
</template>
使用组件
在父组件中
- 导入
组件名首字母大写
import Child from './child.vue'
- 注册
components: {
Child
}
- 模板中渲染
<Child/>
父组件给子组件传值
通过给子组件标签自定义属性来传递
<Child title="博客的标题" />
子组件接收父组件的传值 props
通过 props
选项声明子组件可以接收数据的属性名
props: ['title']
此时 title 便成为子组件实例的一个新增的属性,可像使用 data 中定义的数据一样,使用 title
子组件添加自定义事件 emits
通过 emits
选项声明子组件自定义的事件名
emits: ['fav']
触发自定义事件
<button @click="$emit('fav')">喜欢</button>
父组件监听子组件的自定义事件
<Child title="博客的标题" @fav="favNum++" />
插槽 slot
父组件
<Child>
你好
</Child>
子组件
<div>
<h1>明天</h1>
<slot />
</div>
动态组件
<!-- currentTab 改变时组件也改变 -->
<component :is="currentTab"></component>
currentTab 的值为 被注册的组件名
或 导入的组件对象
应用场景:在多个组件间来回切换,比如 Tab 界面