Vue组件模式Skill vue-component-patterns

Vue组件模式技能专注于使用Vue.js框架构建高效、可复用的前端组件。它涵盖了属性定义与验证、事件处理、插槽使用、依赖注入等核心概念,帮助开发者创建类型安全、易于维护的现代Web应用。关键词:Vue.js, 组件开发, 前端技术, TypeScript, 可重用性。

前端开发 0 次安装 0 次浏览 更新于 3/25/2026

名称: vue-component-patterns 用户可调用: false 描述: 当需要Vue组件模式时使用,包括属性、事件、插槽和提供/注入。在构建可重用Vue组件时使用。 允许的工具:

  • Bash
  • Read

Vue 组件模式

掌握Vue组件模式,以构建可重用、可维护的组件 通过适当的属性验证、事件和组合。

属性模式

使用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>

高级属性类型

<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>

运行时属性验证

<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);
    }
  },

  // 函数属性
  onClick: {
    type: Function as PropType<(id: number) => void>,
    required: false
  }
});
</script>

带默认值的属性

<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: '默认标题',
  count: 0
});

// 对象/数组的函数默认值
const propsWithComplex = withDefaults(defineProps<Props>(), {
  title: '默认',
  count: 0,
  items: () => [],
  user: () => ({ name: '访客', email: 'guest@example.com' }),
  options: () => ({ enabled: true, timeout: 5000 })
});
</script>

事件模式

TypeScript事件

<script setup lang="ts">
// 定义事件类型
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>

运行时事件验证

<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('无效的提交载荷');
      return false;
    }
    return true;
  }
});
</script>

自定义v-model

<!-- 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>

多个v-model

<!-- 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>

插槽模式

基本插槽

<!-- 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>

作用域插槽

<!-- 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>

后备插槽内容

<!-- Button.vue -->
<template>
  <button>
    <slot>
      点击我
    </slot>
  </button>
</template>

<!-- 自定义内容 -->
<Button>自定义文本</Button>

<!-- 使用后备 -->
<Button />

动态插槽

<!-- 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>

无渲染组件与插槽

<!-- 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>

提供和注入用于深层传递

基本提供/注入

<!-- 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>

类型安全的提供/注入

// 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>

提供/注入与响应性

<!-- 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>

组件注册

全局注册

// 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>

局部注册

<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>

自动导入组件

// 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>

异步组件

<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用于模态框和门户

<!-- 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用于组件缓存

<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>

高阶组件

// 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 () => {
    // 加载用户数据
  }
);

何时使用此技能

在构建现代、生产就绪的应用程序时使用vue-component-patterns,这些应用程序需要:

  • 可重用组件库
  • 复杂组件通信
  • 类型安全组件API
  • 灵活的插槽内容投影
  • 无需属性钻孔的深层传递
  • 模态框和门户管理
  • 组件性能优化
  • 大规模组件架构

组件设计最佳实践

  1. 单一职责 - 每个组件应做好一件事
  2. 属性向下,事件向上 - 数据通过属性向下流动,变更通过事件向上流动
  3. 使用TypeScript - 类型安全的属性和事件防止错误
  4. 验证属性 - 对关键属性使用运行时验证
  5. 提供默认值 - 对可选属性使用withDefaults
  6. 使用作用域插槽 - 与消费者共享组件状态
  7. 避免属性钻孔 - 使用提供/注入进行深层传递
  8. 使用v-model进行双向绑定 - 特别是表单输入
  9. 用插槽组合 - 使组件灵活可重用
  10. 保持组件小型 - 将复杂逻辑提取到组合式函数

组件反模式

  1. 突变属性 - 属性是只读的,应发射事件
  2. 紧耦合 - 组件不应了解其父级
  3. 组件中的全局状态 - 使用组合式函数或存储代替
  4. 过多属性 - 考虑插槽或组合
  5. 嵌套v-model - 可能导致混淆,要明确
  6. 不使用TypeScript - 失去类型安全和开发体验
  7. 过度使用提供/注入 - 用于应用级状态,而非所有内容
  8. 无属性验证 - 可能导致运行时错误
  9. 混合关注点 - 分离UI、逻辑和数据获取
  10. 未清理 - 在onUnmounted中移除事件监听器

常见组件模式

表单输入组件

<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>

数据表格组件

<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>

资源