vue-component-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Vue 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

组件设计最佳实践

  1. Single Responsibility - Each component should do one thing well
  2. Props down, events up - Data flows down via props, changes flow up via events
  3. Use TypeScript - Type-safe props and emits prevent bugs
  4. Validate props - Use runtime validation for critical props
  5. Provide defaults - Use
    withDefaults
    for optional props
  6. Use scoped slots - Share component state with consumers
  7. Avoid prop drilling - Use provide/inject for deep passing
  8. Use
    v-model
    for two-way binding
    - Especially for form inputs
  9. Compose with slots - Make components flexible and reusable
  10. Keep components small - Extract complex logic to composables
  1. 单一职责 - 每个组件应该只做好一件事
  2. Props向下,事件向上 - 数据通过Props向下传递,变更通过事件向上通知
  3. 使用TypeScript - 类型安全的Props和Emits可以避免bug
  4. 验证Props - 对关键Props使用运行时验证
  5. 提供默认值 - 对可选Props使用
    withDefaults
  6. 使用作用域插槽 - 与消费者共享组件状态
  7. 避免Props透传 - 使用provide/inject进行深层传值
  8. 使用v-model实现双向绑定 - 尤其适用于表单输入组件
  9. 通过插槽组合组件 - 让组件更灵活、可复用
  10. 保持组件小巧 - 将复杂逻辑提取到组合式函数中

Component Anti-Patterns

组件设计反模式

  1. Mutating props - Props are readonly, emit events instead
  2. Tight coupling - Components shouldn't know about their parents
  3. Global state in components - Use composables or stores instead
  4. Too many props - Consider slots or composition
  5. Nested v-model - Can cause confusion, be explicit
  6. Not using TypeScript - Loses type safety and DX
  7. Overusing provide/inject - Use for app-level state, not everything
  8. No prop validation - Can lead to runtime errors
  9. Mixing concerns - Separate UI, logic, and data fetching
  10. Not cleaning up - Remove event listeners in
    onUnmounted
  1. 修改Props - Props是只读的,应通过事件通知变更
  2. 紧耦合 - 组件不应该依赖父组件的具体实现
  3. 组件内使用全局状态 - 应使用组合式函数或状态管理库
  4. Props过多 - 考虑使用插槽或组合式函数
  5. 嵌套v-model - 容易造成混淆,应保持显式
  6. 不使用TypeScript - 失去类型安全和开发体验
  7. 过度使用provide/inject - 仅用于应用级状态,而非所有场景
  8. 不验证Props - 可能导致运行时错误
  9. 职责混合 - 分离UI、逻辑和数据获取
  10. 不清理资源 - 在
    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>

Resources

参考资源