vue-component-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVue Component Patterns
Vue 组件模式
Master Vue component patterns to build reusable, maintainable components
with proper prop validation, events, and composition.
掌握Vue组件模式,构建具备正确props验证、事件处理和组合能力的可复用、可维护组件。
Props Patterns
Props 模式
Basic Props with TypeScript
基于TypeScript的基础Props
typescript
<script setup lang="ts">
interface Props {
title: string;
count?: number;
items: string[];
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
});
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
</template>typescript
<script setup lang="ts">
interface Props {
title: string;
count?: number;
items: string[];
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
});
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
</template>Advanced Prop Types
高级Props类型
typescript
<script setup lang="ts">
import type { PropType } from 'vue';
type Status = 'pending' | 'success' | 'error';
interface User {
id: number;
name: string;
email: string;
}
interface Props {
// Literal types
status: Status;
// Complex objects
user: User;
// Functions
onUpdate: (value: string) => void;
// Generic arrays
tags: string[];
// Object arrays
users: User[];
// Nullable
description: string | null;
// Union types
value: string | number;
}
const props = defineProps<Props>();
</script>typescript
<script setup lang="ts">
import type { PropType } from 'vue';
type Status = 'pending' | 'success' | 'error';
interface User {
id: number;
name: string;
email: string;
}
interface Props {
// 字面量类型
status: Status;
// 复杂对象
user: User;
// 函数类型
onUpdate: (value: string) => void;
// 泛型数组
tags: string[];
// 对象数组
users: User[];
// 可空类型
description: string | null;
// 联合类型
value: string | number;
}
const props = defineProps<Props>();
</script>Runtime Props Validation
运行时Props验证
typescript
<script setup lang="ts">
import type { PropType } from 'vue';
type ButtonSize = 'sm' | 'md' | 'lg';
const props = defineProps({
// Type checking
title: {
type: String,
required: true
},
// Default values
count: {
type: Number,
default: 0
},
// Multiple types
value: {
type: [String, Number],
required: true
},
// Object with type
user: {
type: Object as PropType<{ name: string; age: number }>,
required: true
},
// Array with type
tags: {
type: Array as PropType<string[]>,
default: () => []
},
// Custom validator
size: {
type: String as PropType<ButtonSize>,
default: 'md',
validator: (value: string) => ['sm', 'md', 'lg'].includes(value)
},
// Complex validator
email: {
type: String,
validator: (value: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
},
// Function prop
onClick: {
type: Function as PropType<(id: number) => void>,
required: false
}
});
</script>typescript
<script setup lang="ts">
import type { PropType } from 'vue';
type ButtonSize = 'sm' | 'md' | 'lg';
const props = defineProps({
// 类型检查
title: {
type: String,
required: true
},
// 默认值
count: {
type: Number,
default: 0
},
// 多类型
value: {
type: [String, Number],
required: true
},
// 指定类型的对象
user: {
type: Object as PropType<{ name: string; age: number }>,
required: true
},
// 指定类型的数组
tags: {
type: Array as PropType<string[]>,
default: () => []
},
// 自定义验证器
size: {
type: String as PropType<ButtonSize>,
default: 'md',
validator: (value: string) => ['sm', 'md', 'lg'].includes(value)
},
// 复杂验证器
email: {
type: String,
validator: (value: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
},
// 函数类型Props
onClick: {
type: Function as PropType<(id: number) => void>,
required: false
}
});
</script>Props with Defaults
带默认值的Props
typescript
<script setup lang="ts">
interface Props {
title?: string;
count?: number;
items?: string[];
user?: {
name: string;
email: string;
};
options?: {
enabled: boolean;
timeout: number;
};
}
// Simple defaults
const props = withDefaults(defineProps<Props>(), {
title: 'Default Title',
count: 0
});
// Function defaults for objects/arrays
const propsWithComplex = withDefaults(defineProps<Props>(), {
title: 'Default',
count: 0,
items: () => [],
user: () => ({ name: 'Guest', email: 'guest@example.com' }),
options: () => ({ enabled: true, timeout: 5000 })
});
</script>typescript
<script setup lang="ts">
interface Props {
title?: string;
count?: number;
items?: string[];
user?: {
name: string;
email: string;
};
options?: {
enabled: boolean;
timeout: number;
};
}
// 简单默认值
const props = withDefaults(defineProps<Props>(), {
title: 'Default Title',
count: 0
});
// 对象/数组的函数式默认值
const propsWithComplex = withDefaults(defineProps<Props>(), {
title: 'Default',
count: 0,
items: () => [],
user: () => ({ name: 'Guest', email: 'guest@example.com' }),
options: () => ({ enabled: true, timeout: 5000 })
});
</script>Emits Patterns
Emits 模式
TypeScript Emits
TypeScript类型的Emits
typescript
<script setup lang="ts">
// Define emit types
const emit = defineEmits<{
// No payload
close: [];
// Single payload
update: [value: string];
// Multiple payloads
change: [id: number, value: string];
// Object payload
submit: [data: { name: string; email: string }];
}>();
function handleClose() {
emit('close');
}
function handleUpdate(value: string) {
emit('update', value);
}
function handleChange(id: number, value: string) {
emit('change', id, value);
}
function handleSubmit() {
emit('submit', { name: 'John', email: 'john@example.com' });
}
</script>typescript
<script setup lang="ts">
// 定义emit类型
const emit = defineEmits<{
// 无负载
close: [];
// 单个负载
update: [value: string];
// 多个负载
change: [id: number, value: string];
// 对象类型负载
submit: [data: { name: string; email: string }];
}>();
function handleClose() {
emit('close');
}
function handleUpdate(value: string) {
emit('update', value);
}
function handleChange(id: number, value: string) {
emit('change', id, value);
}
function handleSubmit() {
emit('submit', { name: 'John', email: 'john@example.com' });
}
</script>Runtime Emits Validation
运行时Emits验证
typescript
<script setup lang="ts">
const emit = defineEmits({
// Basic event
click: null,
// Validation
update: (value: number) => {
return value >= 0;
},
// Complex validation
submit: (payload: { email: string; password: string }) => {
if (!payload.email || !payload.password) {
console.warn('Invalid submit payload');
return false;
}
return true;
}
});
</script>typescript
<script setup lang="ts">
const emit = defineEmits({
// 基础事件
click: null,
// 验证逻辑
update: (value: number) => {
return value >= 0;
},
// 复杂验证逻辑
submit: (payload: { email: string; password: string }) => {
if (!payload.email || !payload.password) {
console.warn('Invalid submit payload');
return false;
}
return true;
}
});
</script>Custom v-model
自定义v-model
typescript
<!-- CustomInput.vue -->
<script setup lang="ts">
interface Props {
modelValue: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
'update:modelValue': [value: string];
}>();
function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
emit('update:modelValue', target.value);
}
</script>
<template>
<input
:value="modelValue"
@input="handleInput"
type="text"
/>
</template>
<!-- Usage -->
<script setup lang="ts">
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const text = ref('');
</script>
<template>
<CustomInput v-model="text" />
</template>typescript
<!-- CustomInput.vue -->
<script setup lang="ts">
interface Props {
modelValue: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
'update:modelValue': [value: string];
}>();
function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
emit('update:modelValue', target.value);
}
</script>
<template>
<input
:value="modelValue"
@input="handleInput"
type="text"
/>
</template>
<!-- 使用示例 -->
<script setup lang="ts">
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const text = ref('');
</script>
<template>
<CustomInput v-model="text" />
</template>Multiple v-models
多v-model绑定
typescript
<!-- RangeSlider.vue -->
<script setup lang="ts">
interface Props {
min: number;
max: number;
}
const props = defineProps<Props>();
const emit = defineEmits<{
'update:min': [value: number];
'update:max': [value: number];
}>();
</script>
<template>
<div>
<input
type="range"
:value="min"
@input="emit('update:min', Number($event.target.value))"
/>
<input
type="range"
:value="max"
@input="emit('update:max', Number($event.target.value))"
/>
</div>
</template>
<!-- Usage -->
<script setup lang="ts">
import { ref } from 'vue';
const minValue = ref(0);
const maxValue = ref(100);
</script>
<template>
<RangeSlider v-model:min="minValue" v-model:max="maxValue" />
</template>typescript
<!-- RangeSlider.vue -->
<script setup lang="ts">
interface Props {
min: number;
max: number;
}
const props = defineProps<Props>();
const emit = defineEmits<{
'update:min': [value: number];
'update:max': [value: number];
}>();
</script>
<template>
<div>
<input
type="range"
:value="min"
@input="emit('update:min', Number($event.target.value))"
/>
<input
type="range"
:value="max"
@input="emit('update:max', Number($event.target.value))"
/>
</div>
</template>
<!-- 使用示例 -->
<script setup lang="ts">
import { ref } from 'vue';
const minValue = ref(0);
const maxValue = ref(100);
</script>
<template>
<RangeSlider v-model:min="minValue" v-model:max="maxValue" />
</template>Slots Patterns
Slots 模式
Basic Slots
基础插槽
typescript
<!-- Card.vue -->
<template>
<div class="card">
<header v-if="$slots.header">
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer v-if="$slots.footer">
<slot name="footer" />
</footer>
</div>
</template>
<!-- Usage -->
<template>
<Card>
<template #header>
<h1>Card Title</h1>
</template>
<p>Card content goes here</p>
<template #footer>
<button>Action</button>
</template>
</Card>
</template>typescript
<!-- Card.vue -->
<template>
<div class="card">
<header v-if="$slots.header">
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer v-if="$slots.footer">
<slot name="footer" />
</footer>
</div>
</template>
<!-- 使用示例 -->
<template>
<Card>
<template #header>
<h1>卡片标题</h1>
</template>
<p>卡片内容区域</p>
<template #footer>
<button>操作按钮</button>
</template>
</Card>
</template>Scoped Slots
作用域插槽
typescript
<!-- List.vue -->
<script setup lang="ts" generic="T">
interface Props {
items: T[];
}
const props = defineProps<Props>();
</script>
<template>
<div>
<div v-for="(item, index) in items" :key="index">
<slot :item="item" :index="index" />
</div>
</div>
</template>
<!-- Usage -->
<script setup lang="ts">
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];
</script>
<template>
<List :items="users">
<template #default="{ item, index }">
<div>
{{ index + 1 }}. {{ item.name }} - {{ item.email }}
</div>
</template>
</List>
</template>typescript
<!-- List.vue -->
<script setup lang="ts" generic="T">
interface Props {
items: T[];
}
const props = defineProps<Props>();
</script>
<template>
<div>
<div v-for="(item, index) in items" :key="index">
<slot :item="item" :index="index" />
</div>
</div>
</template>
<!-- 使用示例 -->
<script setup lang="ts">
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];
</script>
<template>
<List :items="users">
<template #default="{ item, index }">
<div>
{{ index + 1 }}. {{ item.name }} - {{ item.email }}
</div>
</template>
</List>
</template>Fallback Slot Content
插槽默认内容
typescript
<!-- Button.vue -->
<template>
<button>
<slot>
Click Me
</slot>
</button>
</template>
<!-- Custom content -->
<Button>Custom Text</Button>
<!-- Uses fallback -->
<Button />typescript
<!-- Button.vue -->
<template>
<button>
<slot>
点击我
</slot>
</button>
</template>
<!-- 自定义内容 -->
<Button>自定义文本</Button>
<!-- 使用默认内容 -->
<Button />Dynamic Slots
动态插槽
typescript
<!-- DynamicSlots.vue -->
<script setup lang="ts">
import { useSlots } from 'vue';
const slots = useSlots();
// Check if slot exists
const hasHeader = !!slots.header;
// Access slot props
const headerProps = slots.header?.();
</script>
<template>
<div>
<div v-if="hasHeader" class="header">
<slot name="header" />
</div>
<slot />
</div>
</template>typescript
<!-- DynamicSlots.vue -->
<script setup lang="ts">
import { useSlots } from 'vue';
const slots = useSlots();
// 检查插槽是否存在
const hasHeader = !!slots.header;
// 访问插槽属性
const headerProps = slots.header?.();
</script>
<template>
<div>
<div v-if="hasHeader" class="header">
<slot name="header" />
</div>
<slot />
</div>
</template>Renderless Components with Slots
基于插槽的无渲染组件
typescript
<!-- Mouse.vue - Renderless component -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
const x = ref(0);
const y = ref(0);
function update(event: MouseEvent) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
window.removeEventListener('mousemove', update);
});
</script>
<template>
<slot :x="x" :y="y" />
</template>
<!-- Usage -->
<template>
<Mouse v-slot="{ x, y }">
<p>Mouse position: {{ x }}, {{ y }}</p>
</Mouse>
</template>typescript
<!-- Mouse.vue - 无渲染组件 -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
const x = ref(0);
const y = ref(0);
function update(event: MouseEvent) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
window.removeEventListener('mousemove', update);
});
</script>
<template>
<slot :x="x" :y="y" />
</template>
<!-- 使用示例 -->
<template>
<Mouse v-slot="{ x, y }">
<p>鼠标位置: {{ x }}, {{ y }}</p>
</Mouse>
</template>Provide and Inject for Deep Passing
Provide/Inject 深层传值
Basic Provide/Inject
基础Provide/Inject
typescript
<!-- Parent.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue';
const theme = ref('dark');
function toggleTheme() {
theme.value = theme.value === 'dark' ? 'light' : 'dark';
}
provide('theme', theme);
provide('toggleTheme', toggleTheme);
</script>
<!-- Child.vue (any depth) -->
<script setup lang="ts">
import { inject, type Ref } from 'vue';
const theme = inject<Ref<string>>('theme');
const toggleTheme = inject<() => void>('toggleTheme');
</script>
<template>
<div :class="theme">
<button @click="toggleTheme">Toggle Theme</button>
</div>
</template>typescript
<!-- Parent.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue';
const theme = ref('dark');
function toggleTheme() {
theme.value = theme.value === 'dark' ? 'light' : 'dark';
}
provide('theme', theme);
provide('toggleTheme', toggleTheme);
</script>
<!-- Child.vue(任意层级) -->
<script setup lang="ts">
import { inject, type Ref } from 'vue';
const theme = inject<Ref<string>>('theme');
const toggleTheme = inject<() => void>('toggleTheme');
</script>
<template>
<div :class="theme">
<button @click="toggleTheme">切换主题</button>
</div>
</template>Type-Safe Provide/Inject
类型安全的Provide/Inject
typescript
// types.ts
import type { InjectionKey, Ref } from 'vue';
export interface AppConfig {
apiUrl: string;
timeout: number;
}
export interface User {
id: number;
name: string;
email: string;
}
export const ConfigKey: InjectionKey<AppConfig> = Symbol('config');
export const UserKey: InjectionKey<Ref<User | null>> = Symbol('user');
// Provider
<script setup lang="ts">
import { provide, ref } from 'vue';
import { ConfigKey, UserKey } from './types';
const config: AppConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
const user = ref<User | null>(null);
provide(ConfigKey, config);
provide(UserKey, user);
</script>
// Consumer
<script setup lang="ts">
import { inject } from 'vue';
import { ConfigKey, UserKey } from './types';
const config = inject(ConfigKey);
const user = inject(UserKey);
// Fully typed!
console.log(config?.apiUrl);
console.log(user?.value?.name);
</script>typescript
// types.ts
import type { InjectionKey, Ref } from 'vue';
export interface AppConfig {
apiUrl: string;
timeout: number;
}
export interface User {
id: number;
name: string;
email: string;
}
export const ConfigKey: InjectionKey<AppConfig> = Symbol('config');
export const UserKey: InjectionKey<Ref<User | null>> = Symbol('user');
// 提供者组件
<script setup lang="ts">
import { provide, ref } from 'vue';
import { ConfigKey, UserKey } from './types';
const config: AppConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
const user = ref<User | null>(null);
provide(ConfigKey, config);
provide(UserKey, user);
</script>
// 消费者组件
<script setup lang="ts">
import { inject } from 'vue';
import { ConfigKey, UserKey } from './types';
const config = inject(ConfigKey);
const user = inject(UserKey);
// 完全类型安全!
console.log(config?.apiUrl);
console.log(user?.value?.name);
</script>Provide/Inject with Reactivity
响应式的Provide/Inject
typescript
<!-- App.vue -->
<script setup lang="ts">
import { provide, reactive, readonly } from 'vue';
interface State {
count: number;
user: { name: string };
}
const state = reactive<State>({
count: 0,
user: { name: 'John' }
});
function increment() {
state.count++;
}
// Provide readonly to prevent mutations
provide('state', readonly(state));
provide('increment', increment);
</script>
<!-- Consumer -->
<script setup lang="ts">
import { inject } from 'vue';
const state = inject('state');
const increment = inject('increment');
</script>
<template>
<div>
<p>Count: {{ state.count }}</p>
<button @click="increment">Increment</button>
</div>
</template>typescript
<!-- App.vue -->
<script setup lang="ts">
import { provide, reactive, readonly } from 'vue';
interface State {
count: number;
user: { name: string };
}
const state = reactive<State>({
count: 0,
user: { name: 'John' }
});
function increment() {
state.count++;
}
// 提供只读对象防止外部修改
provide('state', readonly(state));
provide('increment', increment);
</script>
<!-- 消费者组件 -->
<script setup lang="ts">
import { inject } from 'vue';
const state = inject('state');
const increment = inject('increment');
</script>
<template>
<div>
<p>计数: {{ state.count }}</p>
<button @click="increment">增加计数</button>
</div>
</template>Component Registration
组件注册
Global Registration
全局注册
typescript
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import BaseButton from './components/BaseButton.vue';
import BaseInput from './components/BaseInput.vue';
const app = createApp(App);
// Register globally
app.component('BaseButton', BaseButton);
app.component('BaseInput', BaseInput);
app.mount('#app');
// Use anywhere without importing
<template>
<BaseButton>Click</BaseButton>
<BaseInput v-model="text" />
</template>typescript
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import BaseButton from './components/BaseButton.vue';
import BaseInput from './components/BaseInput.vue';
const app = createApp(App);
// 全局注册组件
app.component('BaseButton', BaseButton);
app.component('BaseInput', BaseInput);
app.mount('#app');
// 无需导入即可在任意地方使用
<template>
<BaseButton>点击</BaseButton>
<BaseInput v-model="text" />
</template>Local Registration
局部注册
typescript
<script setup lang="ts">
import BaseButton from './components/BaseButton.vue';
import BaseInput from './components/BaseInput.vue';
// Automatically registered in this component
</script>
<template>
<BaseButton>Click</BaseButton>
<BaseInput v-model="text" />
</template>typescript
<script setup lang="ts">
import BaseButton from './components/BaseButton.vue';
import BaseInput from './components/BaseInput.vue';
// 自动在当前组件中注册
</script>
<template>
<BaseButton>点击</BaseButton>
<BaseInput v-model="text" />
</template>Auto-Import Components
自动导入组件
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
export default defineConfig({
plugins: [
vue(),
Components({
// Auto import from components directory
dirs: ['src/components'],
// Generate types
dts: true
})
]
});
// Now use components without importing
<template>
<BaseButton>No import needed!</BaseButton>
</template>typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
export default defineConfig({
plugins: [
vue(),
Components({
// 从components目录自动导入
dirs: ['src/components'],
// 生成类型声明文件
dts: true
})
]
});
// 现在无需导入即可使用组件
<template>
<BaseButton>无需导入!</BaseButton>
</template>Async Components
异步组件
typescript
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
// Basic async component
const AsyncComponent = defineAsyncComponent(() =>
import('./components/Heavy.vue')
);
// With loading and error states
const AsyncWithOptions = defineAsyncComponent({
loader: () => import('./components/Heavy.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000
});
</script>
<template>
<Suspense>
<AsyncComponent />
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>typescript
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
// 基础异步组件
const AsyncComponent = defineAsyncComponent(() =>
import('./components/Heavy.vue')
);
// 带加载和错误状态的异步组件
const AsyncWithOptions = defineAsyncComponent({
loader: () => import('./components/Heavy.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000
});
</script>
<template>
<Suspense>
<AsyncComponent />
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>Teleport for Modals and Portals
Teleport 模态框与传送门
typescript
<!-- Modal.vue -->
<script setup lang="ts">
import { ref } from 'vue';
interface Props {
show: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{
close: [];
}>();
</script>
<template>
<Teleport to="body">
<div v-if="show" class="modal-backdrop" @click="emit('close')">
<div class="modal" @click.stop>
<slot />
</div>
</div>
</Teleport>
</template>
<style scoped>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background: white;
padding: 2rem;
border-radius: 8px;
}
</style>
<!-- Usage -->
<script setup lang="ts">
import { ref } from 'vue';
import Modal from './Modal.vue';
const showModal = ref(false);
</script>
<template>
<button @click="showModal = true">Open Modal</button>
<Modal :show="showModal" @close="showModal = false">
<h2>Modal Content</h2>
<p>This is teleported to body!</p>
</Modal>
</template>typescript
<!-- Modal.vue -->
<script setup lang="ts">
import { ref } from 'vue';
interface Props {
show: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{
close: [];
}>();
</script>
<template>
<Teleport to="body">
<div v-if="show" class="modal-backdrop" @click="emit('close')">
<div class="modal" @click.stop>
<slot />
</div>
</div>
</Teleport>
</template>
<style scoped>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background: white;
padding: 2rem;
border-radius: 8px;
}
</style>
<!-- 使用示例 -->
<script setup lang="ts">
import { ref } from 'vue';
import Modal from './Modal.vue';
const showModal = ref(false);
</script>
<template>
<button @click="showModal = true">打开模态框</button>
<Modal :show="showModal" @close="showModal = false">
<h2>模态框内容</h2>
<p>该内容被传送到body节点下!</p>
</Modal>
</template>KeepAlive for Component Caching
KeepAlive 组件缓存
typescript
<script setup lang="ts">
import { ref } from 'vue';
import TabA from './TabA.vue';
import TabB from './TabB.vue';
import TabC from './TabC.vue';
const currentTab = ref('TabA');
const tabs = {
TabA,
TabB,
TabC
};
</script>
<template>
<div>
<button
v-for="(_, tab) in tabs"
:key="tab"
@click="currentTab = tab"
>
{{ tab }}
</button>
<!-- Cache inactive components -->
<KeepAlive>
<component :is="tabs[currentTab]" />
</KeepAlive>
<!-- Include/exclude specific components -->
<KeepAlive :include="['TabA', 'TabB']">
<component :is="tabs[currentTab]" />
</KeepAlive>
<!-- Max cached instances -->
<KeepAlive :max="3">
<component :is="tabs[currentTab]" />
</KeepAlive>
</div>
</template>typescript
<script setup lang="ts">
import { ref } from 'vue';
import TabA from './TabA.vue';
import TabB from './TabB.vue';
import TabC from './TabC.vue';
const currentTab = ref('TabA');
const tabs = {
TabA,
TabB,
TabC
};
</script>
<template>
<div>
<button
v-for="(_, tab) in tabs"
:key="tab"
@click="currentTab = tab"
>
{{ tab }}
</button>
<!-- 缓存非活跃组件 -->
<KeepAlive>
<component :is="tabs[currentTab]" />
</KeepAlive>
<!-- 包含/排除特定组件 -->
<KeepAlive :include="['TabA', 'TabB']">
<component :is="tabs[currentTab]" />
</KeepAlive>
<!-- 最大缓存实例数 -->
<KeepAlive :max="3">
<component :is="tabs[currentTab]" />
</KeepAlive>
</div>
</template>Higher-Order Components
高阶组件
typescript
// withLoading.ts
import { defineComponent, h, ref, onMounted } from 'vue';
export function withLoading(Component: any, loadFn: () => Promise<void>) {
return defineComponent({
setup(props, { attrs, slots }) {
const loading = ref(true);
const error = ref<Error | null>(null);
onMounted(async () => {
try {
await loadFn();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
});
return () => {
if (loading.value) {
return h('div', 'Loading...');
}
if (error.value) {
return h('div', `Error: ${error.value.message}`);
}
return h(Component, { ...props, ...attrs }, slots);
};
}
});
}
// Usage
const UserProfile = withLoading(
UserProfileComponent,
async () => {
// Load user data
}
);typescript
// withLoading.ts
import { defineComponent, h, ref, onMounted } from 'vue';
export function withLoading(Component: any, loadFn: () => Promise<void>) {
return defineComponent({
setup(props, { attrs, slots }) {
const loading = ref(true);
const error = ref<Error | null>(null);
onMounted(async () => {
try {
await loadFn();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
});
return () => {
if (loading.value) {
return h('div', '加载中...');
}
if (error.value) {
return h('div', `错误: ${error.value.message}`);
}
return h(Component, { ...props, ...attrs }, slots);
};
}
});
}
// 使用示例
const UserProfile = withLoading(
UserProfileComponent,
async () => {
// 加载用户数据
}
);When to Use This Skill
何时使用本技能
Use vue-component-patterns when building modern, production-ready
applications that require:
- Reusable component libraries
- Complex component communication
- Type-safe component APIs
- Flexible content projection with slots
- Deep prop passing without prop drilling
- Modal and portal management
- Component performance optimization
- Large-scale component architectures
当你构建现代、生产级应用且需要以下能力时,使用vue-component-patterns:
- 可复用组件库
- 复杂组件通信
- 类型安全的组件API
- 基于插槽的灵活内容投影
- 无需Props透传的深层传值
- 模态框与传送门管理
- 组件性能优化
- 大规模组件架构
Component Design Best Practices
组件设计最佳实践
- Single Responsibility - Each component should do one thing well
- Props down, events up - Data flows down via props, changes flow up via events
- Use TypeScript - Type-safe props and emits prevent bugs
- Validate props - Use runtime validation for critical props
- Provide defaults - Use for optional props
withDefaults - Use scoped slots - Share component state with consumers
- Avoid prop drilling - Use provide/inject for deep passing
- Use for two-way binding - Especially for form inputs
v-model - Compose with slots - Make components flexible and reusable
- Keep components small - Extract complex logic to composables
- 单一职责 - 每个组件应该只做好一件事
- Props向下,事件向上 - 数据通过Props向下传递,变更通过事件向上通知
- 使用TypeScript - 类型安全的Props和Emits可以避免bug
- 验证Props - 对关键Props使用运行时验证
- 提供默认值 - 对可选Props使用
withDefaults - 使用作用域插槽 - 与消费者共享组件状态
- 避免Props透传 - 使用provide/inject进行深层传值
- 使用v-model实现双向绑定 - 尤其适用于表单输入组件
- 通过插槽组合组件 - 让组件更灵活、可复用
- 保持组件小巧 - 将复杂逻辑提取到组合式函数中
Component Anti-Patterns
组件设计反模式
- Mutating props - Props are readonly, emit events instead
- Tight coupling - Components shouldn't know about their parents
- Global state in components - Use composables or stores instead
- Too many props - Consider slots or composition
- Nested v-model - Can cause confusion, be explicit
- Not using TypeScript - Loses type safety and DX
- Overusing provide/inject - Use for app-level state, not everything
- No prop validation - Can lead to runtime errors
- Mixing concerns - Separate UI, logic, and data fetching
- Not cleaning up - Remove event listeners in
onUnmounted
- 修改Props - Props是只读的,应通过事件通知变更
- 紧耦合 - 组件不应该依赖父组件的具体实现
- 组件内使用全局状态 - 应使用组合式函数或状态管理库
- Props过多 - 考虑使用插槽或组合式函数
- 嵌套v-model - 容易造成混淆,应保持显式
- 不使用TypeScript - 失去类型安全和开发体验
- 过度使用provide/inject - 仅用于应用级状态,而非所有场景
- 不验证Props - 可能导致运行时错误
- 职责混合 - 分离UI、逻辑和数据获取
- 不清理资源 - 在中移除事件监听器
onUnmounted
Common Component Patterns
常见组件模式
Form Input Component
表单输入组件
typescript
<script setup lang="ts">
interface Props {
modelValue: string;
label?: string;
error?: string;
placeholder?: string;
required?: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{
'update:modelValue': [value: string];
blur: [];
}>();
</script>
<template>
<div class="form-field">
<label v-if="label">
{{ label }}
<span v-if="required" class="required">*</span>
</label>
<input
:value="modelValue"
:placeholder="placeholder"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
@blur="emit('blur')"
/>
<span v-if="error" class="error">{{ error }}</span>
</div>
</template>typescript
<script setup lang="ts">
interface Props {
modelValue: string;
label?: string;
error?: string;
placeholder?: string;
required?: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{
'update:modelValue': [value: string];
blur: [];
}>();
</script>
<template>
<div class="form-field">
<label v-if="label">
{{ label }}
<span v-if="required" class="required">*</span>
</label>
<input
:value="modelValue"
:placeholder="placeholder"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
@blur="emit('blur')"
/>
<span v-if="error" class="error">{{ error }}</span>
</div>
</template>Data Table Component
数据表格组件
typescript
<script setup lang="ts" generic="T">
interface Column<T> {
key: keyof T;
label: string;
sortable?: boolean;
}
interface Props {
data: T[];
columns: Column<T>[];
}
const props = defineProps<Props>();
const emit = defineEmits<{
sort: [column: keyof T];
rowClick: [item: T];
}>();
</script>
<template>
<table>
<thead>
<tr>
<th
v-for="col in columns"
:key="String(col.key)"
@click="col.sortable && emit('sort', col.key)"
>
{{ col.label }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in data"
:key="index"
@click="emit('rowClick', item)"
>
<td v-for="col in columns" :key="String(col.key)">
<slot :name="`cell-${String(col.key)}`" :item="item">
{{ item[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>typescript
<script setup lang="ts" generic="T">
interface Column<T> {
key: keyof T;
label: string;
sortable?: boolean;
}
interface Props {
data: T[];
columns: Column<T>[];
}
const props = defineProps<Props>();
const emit = defineEmits<{
sort: [column: keyof T];
rowClick: [item: T];
}>();
</script>
<template>
<table>
<thead>
<tr>
<th
v-for="col in columns"
:key="String(col.key)"
@click="col.sortable && emit('sort', col.key)"
>
{{ col.label }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in data"
:key="index"
@click="emit('rowClick', item)"
>
<td v-for="col in columns" :key="String(col.key)">
<slot :name="`cell-${String(col.key)}`" :item="item">
{{ item[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>