Vue3 组件开发与通信
2026/3/20大约 8 分钟
Vue3 组件开发与通信:构建现代化组件体系
Vue3 的组件系统在保持简洁的同时变得更加强大。无论你是用 Options API 还是 Composition API,组件化开发的核心理念是一致的。本文将全面介绍 Vue3 中的组件开发与通信方式。
组件定义方式
Options API(传统方式)
<template>
<div class="user-card">
<h3>{{ fullName }}</h3>
<p>{{ user.email }}</p>
<button @click="handleClick">联系</button>
</div>
</template>
<script>
export default {
name: "UserCard",
props: {
user: {
type: Object,
required: true,
},
},
emits: ["contact"],
data() {
return {
isLoading: false,
};
},
computed: {
fullName() {
return `${this.user.firstName} ${this.user.lastName}`;
},
},
methods: {
handleClick() {
this.$emit("contact", this.user);
},
},
};
</script>
Composition API + setup()
<template>
<div class="user-card">
<h3>{{ fullName }}</h3>
<p>{{ user.email }}</p>
<button @click="handleClick">联系</button>
</div>
</template>
<script>
import { computed } from "vue";
export default {
name: "UserCard",
props: {
user: {
type: Object,
required: true,
},
},
emits: ["contact"],
setup(props, { emit }) {
const fullName = computed(() => {
return `${props.user.firstName} ${props.user.lastName}`;
});
function handleClick() {
emit("contact", props.user);
}
return {
fullName,
handleClick,
};
},
};
</script>
<script setup>(推荐)
<template>
<div class="user-card">
<h3>{{ fullName }}</h3>
<p>{{ user.email }}</p>
<button @click="handleClick">联系</button>
</div>
</template>
<script setup>
import { computed } from "vue";
const props = defineProps({
user: {
type: Object,
required: true,
},
});
const emit = defineEmits(["contact"]);
const fullName = computed(() => {
return `${props.user.firstName} ${props.user.lastName}`;
});
function handleClick() {
emit("contact", props.user);
}
</script>
Props 深入
Props 声明方式
<script setup>
// 方式一:数组形式(不推荐)
const props = defineProps(["title", "content"]);
// 方式二:对象形式
const props = defineProps({
title: String,
count: Number,
isActive: Boolean,
tags: Array,
author: Object,
callback: Function,
});
// 方式三:带验证的完整形式(推荐)
const props = defineProps({
title: {
type: String,
required: true,
},
count: {
type: Number,
default: 0,
},
author: {
type: Object,
// 对象/数组默认值必须用工厂函数
default: () => ({ name: "匿名" }),
},
status: {
type: String,
validator: (value) => {
return ["pending", "success", "error"].includes(value);
},
},
});
</script>
TypeScript 类型声明
<script setup lang="ts">
// 方式一:基于类型的声明
interface User {
id: number;
name: string;
email: string;
}
const props = defineProps<{
user: User;
title: string;
count?: number;
tags?: string[];
}>();
// 带默认值(使用 withDefaults)
const props = withDefaults(
defineProps<{
title: string;
count?: number;
tags?: string[];
}>(),
{
count: 0,
tags: () => [],
}
);
// 方式二:运行时声明 + 类型
import type { PropType } from "vue";
const props = defineProps({
user: {
type: Object as PropType<User>,
required: true,
},
status: {
type: String as PropType<"pending" | "success" | "error">,
default: "pending",
},
});
</script>
Props 解构(Vue 3.3+)
<script setup>
// Vue 3.3+ 支持响应式解构
const { title, count = 0 } = defineProps(["title", "count"]);
// 之前版本需要使用 toRefs
import { toRefs } from "vue";
const props = defineProps(["title", "count"]);
const { title, count } = toRefs(props);
</script>
自定义事件
基础用法
<!-- 子组件 -->
<script setup>
// 声明事件
const emit = defineEmits(["change", "update", "delete"]);
// 或者带验证
const emit = defineEmits({
// 无验证
change: null,
// 带验证
submit: (payload) => {
if (payload.email && payload.password) {
return true;
}
console.warn("Invalid submit payload!");
return false;
},
});
// 触发事件
function handleSubmit() {
emit("submit", { email: "test@example.com", password: "123456" });
}
</script>
<!-- 父组件 -->
<template>
<ChildComponent @change="handleChange" @submit="handleSubmit" />
</template>
TypeScript 类型声明
<script setup lang="ts">
// 基于类型的声明
const emit = defineEmits<{
(e: "change", value: string): void;
(e: "update", id: number, data: object): void;
}>();
// Vue 3.3+ 更简洁的语法
const emit = defineEmits<{
change: [value: string];
update: [id: number, data: object];
}>();
emit("change", "new value");
emit("update", 1, { name: "test" });
</script>
v-model 与组件
<!-- 父组件 -->
<template>
<!-- 单个 v-model -->
<CustomInput v-model="searchText" />
<!-- 等价于 -->
<CustomInput
:modelValue="searchText"
@update:modelValue="searchText = $event"
/>
<!-- 多个 v-model -->
<UserForm
v-model:firstName="user.firstName"
v-model:lastName="user.lastName"
/>
</template>
<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(["modelValue"]);
defineEmits(["update:modelValue"]);
</script>
<!-- UserForm.vue -->
<template>
<input
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>
<script setup>
defineProps(["firstName", "lastName"]);
defineEmits(["update:firstName", "update:lastName"]);
</script>
v-model 修饰符
<!-- 父组件 -->
<template>
<MyInput v-model.capitalize="text" />
</template>
<!-- MyInput.vue -->
<template>
<input :value="modelValue" @input="handleInput" />
</template>
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: {
default: () => ({}),
},
});
const emit = defineEmits(["update:modelValue"]);
function handleInput(e) {
let value = e.target.value;
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
emit("update:modelValue", value);
}
</script>
插槽系统
默认插槽
<!-- Card.vue -->
<template>
<div class="card">
<div class="card-body">
<slot>
<!-- 默认内容 -->
<p>暂无内容</p>
</slot>
</div>
</div>
</template>
<!-- 使用 -->
<Card>
<p>自定义内容</p>
</Card>
具名插槽
<!-- Layout.vue -->
<template>
<div class="layout">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<!-- 使用 -->
<Layout>
<template #header>
<h1>标题</h1>
</template>
<template #default>
<p>主要内容</p>
</template>
<template #footer>
<p>版权信息</p>
</template>
</Layout>
作用域插槽
<!-- ItemList.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot name="item" :item="item" :index="index">
{{ item.name }}
</slot>
</li>
</ul>
</template>
<!-- 使用 -->
<ItemList :items="items">
<template #item="{ item, index }">
<span>{{ index + 1 }}. {{ item.name }}</span>
<button @click="edit(item)">编辑</button>
</template>
</ItemList>
<!-- 解构写法 -->
<ItemList :items="items" v-slot="slotProps">
{{ slotProps.item.name }}
</ItemList>
动态插槽名
<template>
<BaseLayout>
<template v-for="slot in slots" #[slot.name]>
<component :is="slot.component" />
</template>
</BaseLayout>
</template>
组件通信方式汇总
1. Props / Emits(父子通信)
<!-- 父组件 -->
<template>
<Child :data="parentData" @update="handleUpdate" />
</template>
<!-- 子组件 -->
<script setup>
const props = defineProps(["data"]);
const emit = defineEmits(["update"]);
function updateData() {
emit("update", newData);
}
</script>
2. v-model(双向绑定)
<!-- 父组件 -->
<template>
<CustomInput v-model="text" />
</template>
3. ref / expose(父访问子)
<!-- 父组件 -->
<template>
<Child ref="childRef" />
</template>
<script setup>
import { ref, onMounted } from "vue";
const childRef = ref(null);
onMounted(() => {
// 访问子组件暴露的方法和属性
childRef.value.someMethod();
console.log(childRef.value.someData);
});
</script>
<!-- 子组件 -->
<script setup>
import { ref } from "vue";
const someData = ref("hello");
function someMethod() {
console.log("called from parent");
}
// 明确暴露给父组件的内容
defineExpose({
someData,
someMethod,
});
</script>
4. provide / inject(跨层级通信)
<!-- 祖先组件 -->
<script setup>
import { provide, ref, readonly } from "vue";
const theme = ref("dark");
const updateTheme = (newTheme) => {
theme.value = newTheme;
};
// 提供响应式数据
provide("theme", readonly(theme));
provide("updateTheme", updateTheme);
// 或者提供一个对象
provide("appContext", {
theme: readonly(theme),
updateTheme,
});
</script>
<!-- 后代组件(任意层级) -->
<script setup>
import { inject } from "vue";
// 注入
const theme = inject("theme");
const updateTheme = inject("updateTheme");
// 带默认值
const theme = inject("theme", "light");
// 注入对象
const { theme, updateTheme } = inject("appContext");
</script>
使用 Symbol 作为 key(推荐):
// keys.js
export const themeKey = Symbol("theme");
export const userKey = Symbol("user");
// 祖先组件
import { themeKey } from "./keys";
provide(themeKey, theme);
// 后代组件
import { themeKey } from "./keys";
const theme = inject(themeKey);
5. 全局状态管理(Pinia)
// stores/user.js
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
state: () => ({
name: "",
isLoggedIn: false,
}),
actions: {
login(name) {
this.name = name;
this.isLoggedIn = true;
},
},
});
// 组件中使用
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
console.log(userStore.name);
userStore.login("张三");
6. 事件总线(不推荐,但可用)
// Vue3 移除了 $on/$off,可以使用 mitt
// npm install mitt
// eventBus.js
import mitt from "mitt";
export const emitter = mitt();
// 组件 A:发送
import { emitter } from "./eventBus";
emitter.emit("user-login", { userId: 123 });
// 组件 B:接收
import { emitter } from "./eventBus";
import { onMounted, onUnmounted } from "vue";
const handler = (data) => {
console.log(data);
};
onMounted(() => {
emitter.on("user-login", handler);
});
onUnmounted(() => {
emitter.off("user-login", handler);
});
通信方式选择指南
| 场景 | 推荐方式 |
|---|---|
| 父 → 子 | props |
| 子 → 父 | emits |
| 父 ↔ 子 | v-model |
| 父访问子方法 | ref + expose |
| 跨层级(主题、配置) | provide/inject |
| 全局状态 | Pinia |
| 兄弟组件 | 共同父组件 / Pinia |
动态组件与异步组件
动态组件
<template>
<div>
<button v-for="tab in tabs" :key="tab" @click="currentTab = tab">
{{ tab }}
</button>
<!-- 动态组件 -->
<component :is="currentTabComponent" />
<!-- 带缓存 -->
<KeepAlive>
<component :is="currentTabComponent" />
</KeepAlive>
<!-- KeepAlive 配置 -->
<KeepAlive :include="['TabA', 'TabB']" :max="10">
<component :is="currentTabComponent" />
</KeepAlive>
</div>
</template>
<script setup>
import { ref, computed, shallowRef } from "vue";
import TabA from "./TabA.vue";
import TabB from "./TabB.vue";
import TabC from "./TabC.vue";
const tabs = ["TabA", "TabB", "TabC"];
const currentTab = ref("TabA");
const tabComponents = {
TabA,
TabB,
TabC,
};
const currentTabComponent = computed(() => {
return tabComponents[currentTab.value];
});
</script>
异步组件
import { defineAsyncComponent } from 'vue'
// 基础用法
const AsyncComponent = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
// 带配置
const AsyncComponentWithOptions = defineAsyncComponent({
loader: () => import('./components/HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // 显示 loading 前的延迟
timeout: 3000 // 超时时间
})
// 在模板中使用
<template>
<AsyncComponent />
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
组件 v-model 高级用法
自定义 v-model 修饰符
<!-- MyInput.vue -->
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) },
});
const emit = defineEmits(["update:modelValue"]);
function emitValue(e) {
let value = e.target.value;
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
if (props.modelModifiers.uppercase) {
value = value.toUpperCase();
}
emit("update:modelValue", value);
}
</script>
<template>
<input :value="modelValue" @input="emitValue" />
</template>
<!-- 使用 -->
<MyInput v-model.capitalize.uppercase="text" />
带参数的 v-model 修饰符
<!-- 对于 v-model:title.capitalize -->
<script setup>
const props = defineProps({
title: String,
titleModifiers: { default: () => ({}) },
});
// titleModifiers 会是 { capitalize: true }
</script>
透传 Attributes
基础透传
<!-- 父组件 -->
<MyButton class="large" @click="onClick" data-test="btn" />
<!-- MyButton.vue -->
<template>
<!-- class、事件、data-* 会自动透传到根元素 -->
<button>Click me</button>
</template>
<!-- 渲染结果 -->
<button class="large" data-test="btn">Click me</button>
禁用透传
<script setup>
defineOptions({
inheritAttrs: false,
});
</script>
<template>
<div class="wrapper">
<!-- 手动绑定到特定元素 -->
<button v-bind="$attrs">Click me</button>
</div>
</template>
访问透传属性
<script setup>
import { useAttrs } from "vue";
const attrs = useAttrs();
console.log(attrs.class); // 父组件传入的 class
console.log(attrs.onClick); // 父组件传入的事件
</script>
多根节点的透传
<template>
<!-- 多根节点必须显式绑定 $attrs -->
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
最佳实践
1. 组件命名规范
// 基础组件:Base/App/V 前缀
BaseButton.vue;
AppTable.vue;
VIcon.vue;
// 单例组件:The 前缀
TheHeader.vue;
TheSidebar.vue;
// 紧密耦合的子组件
TodoList.vue;
TodoListItem.vue;
TodoListItemButton.vue;
// 组件名使用 PascalCase
UserProfile.vue; // ✅
user - profile.vue; // ❌
2. Props 设计
// 使用对象而非多个布尔值
// ❌
<Button primary />
<Button secondary />
<Button danger />
// ✅
<Button variant="primary" />
<Button variant="secondary" />
<Button variant="danger" />
// 提供合理的默认值
defineProps({
size: {
type: String as PropType<'sm' | 'md' | 'lg'>,
default: 'md'
}
})
3. 事件命名
// 使用动词或动词短语
"click";
"submit";
"update:modelValue";
"row-select";
"page-change";
// 不要使用缩写
"sel"; // ❌
"select"; // ✅
4. 组件单一职责
<!-- ❌ 功能过多 -->
<UserManager />
<!-- ✅ 拆分职责 -->
<UserList />
<UserDetail />
<UserForm />
<UserActions />
总结
Vue3 的组件系统更加强大和灵活:
- 组件定义:推荐使用
<script setup>语法 - Props:支持 TypeScript 类型声明,更安全
- 事件:通过
defineEmits显式声明 - 插槽:作用域插槽更简洁
- 通信方式:多种方式适应不同场景
- v-model:支持多个和自定义修饰符
- 透传属性:更灵活的控制
掌握这些知识,你就能构建出高质量、可维护的 Vue3 组件。下一篇文章,我们将深入 Composition API,学习如何更好地组织和复用业务逻辑。