mcpField
V
vue-best-practices

vue-best-practices

github.com/dedalus-erp-pas开发252 次安装
代码仓库

关于 vue-best-practices

面向 Vue.js 3 开发者的最佳实践技能,适用于构建、审查或重构 Vue 应用,也涵盖 Nuxt.js 项目。内容按组合式 API、响应性、组件设计、TypeScript、Tailwind CSS 和 PrimeVue 分类。

主要功能

  1. 01组合式 API 与组合式函数指南
  2. 02组件设计与响应性模式
  3. 03类型化 Props、事件和模板
  4. 04错误处理与异步组件
  5. 05Tailwind CSS 样式实践
  6. 06PrimeVue 集成与主题配置

适用场景

  1. 01构建 Vue 组件和组合式函数
  2. 02审查或重构 Vue 应用代码
  3. 03规划组件架构
  4. 04使用 Tailwind CSS 和 PrimeVue 编写样式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dedalus-erp-pas/foundation-skills --skill vue-best-practices

安装后在 Claude Code 中输入/vue-best-practices确认已加载。

详细介绍

Vue.js 最佳实践

Vue.js 3 应用的全面最佳实践指南。涵盖多个类别的指导原则,帮助编写符合惯例、易于维护且可扩展的 Vue.js 代码,包括用于实用优先样式的 Tailwind CSS 集成模式,以及 PrimeVue 组件库最佳实践。

适用场景

遇到以下情况时,请参考这些指导原则:

  • 编写新的 Vue 组件或组合式函数
  • 使用 Composition API 实现功能
  • 检查代码是否符合 Vue.js 模式规范
  • 重构现有的 Vue.js 代码
  • 搭建组件架构
  • 使用 Nuxt.js 应用
  • 使用 Tailwind CSS 工具类为 Vue 组件设置样式
  • 使用 Tailwind 和 Vue 创建设计系统
  • 原生使用 PrimeVue 组件库
  • 通过设计令牌和 definePreset() 自定义 PrimeVue 主题

规则类别

类别重点前缀
Composition API正确使用 Composition API 模式composition-
组件设计组件结构与组织component-
响应性响应式状态管理模式reactive-
Props 和事件组件通信模式props-
模板模式模板语法最佳实践template-
代码组织项目和代码结构organization-
TypeScript类型安全的 Vue.js 模式typescript-
错误处理错误边界与处理error-
Tailwind CSS实用优先样式模式tailwind-
PrimeVue组件库集成模式primevue-

快速参考

1. Composition API 最佳实践
  • composition-script-setup - 单文件组件始终使用 <script setup>
  • composition-ref-vs-reactive - 基本类型使用 ref(),对象使用 reactive()
  • composition-computed-derived - 所有派生状态都使用 computed()
  • composition-watch-side-effects - 仅将 watch()/watchEffect() 用于副作用
  • composition-composables - 将可复用逻辑提取到组合式函数中
  • composition-lifecycle-order - 在声明响应式状态后放置生命周期钩子
  • composition-avoid-this - 在 Composition API 中绝不使用 this
2. 组件设计
  • component-single-responsibility - 一个组件,一个用途
  • component-naming-convention - 组件使用 PascalCase,模板中使用 kebab-case
  • component-small-focused - 组件控制在 200 行以内
  • component-presentational-container - 在有益时将逻辑与表现分离
  • component-slots-flexibility - 使用插槽灵活组合组件
  • component-expose-minimal - 仅通过 defineExpose() 暴露必要内容
3. 响应式模式
  • reactive-const-refs - 始终使用 const 声明 ref
  • reactive-unwrap-template - 让 Vue 在模板中自动解包 ref(无需 .value)
  • reactive-shallow-large-data - 对大型非响应式数据使用 shallowRef()/shallowReactive()
  • reactive-readonly-props - 使用 readonly() 防止修改
  • reactive-toRefs-destructure - 解构响应式对象时使用 toRefs()
  • reactive-avoid-mutation - 复杂状态优先使用不可变更新
4. Props 和事件
  • props-define-types - 始终使用 defineProps<T>() 定义 prop 类型
  • props-required-explicit - 明确区分必需和可选的 prop
  • props-default-values - 使用 withDefaults() 提供合理的默认值
  • props-immutable - 绝不直接修改 props
  • props-validation - 对复杂的 prop 验证使用校验函数
  • events-define-emits - 始终使用 defineEmits<T>() 定义 emits
  • events-naming - 模板中的事件名称使用 kebab-case
  • events-payload-objects - 多值事件使用对象传递数据
5. 模板模式
  • template-v-if-v-show - 条件渲染使用 v-if,切换显示状态使用 v-show
  • template-v-for-key - 使用 v-for 时,始终设置唯一且稳定的 :key
  • template-v-if-v-for - 不要在同一个元素上同时使用 v-if 和 v-for
  • template-computed-expressions - 将复杂表达式移至计算属性
  • template-event-modifiers - 适当使用事件修饰符(.prevent、.stop)
  • template-v-bind-shorthand - 使用简写语法(v-bind 用 :,v-on 用 @)
  • template-v-model-modifiers - 使用 v-model 修饰符(.trim、.number、.lazy)
6. 代码组织
  • organization-feature-folders - 按功能而非类型组织代码
  • organization-composables-folder - 将组合式函数放在专用的 composables/ 文件夹中
  • organization-barrel-exports - 使用 index 文件简化导入
  • organization-consistent-naming - 遵循统一的命名规范
  • organization-colocation - 将相关文件放在一起(组件、测试、样式)
7. TypeScript 集成
  • typescript-generic-components - 使用泛型构建可复用的类型化组件
  • typescript-prop-types - 使用 TypeScript 接口定义属性
  • typescript-emit-types - 为 emit 负载显式指定类型
  • typescript-ref-typing - 无法推断类型时,为 ref 指定类型
  • typescript-template-refs - 使用 ref<InstanceType<typeof Component> | null>(null) 为模板 ref 指定类型
8. 错误处理
  • error-boundaries - 使用 onErrorCaptured() 设置组件错误边界
  • error-async-handling - 显式处理异步操作中的错误
  • error-provide-fallbacks - 为错误状态提供备用 UI
  • error-logging - 适当地记录错误,以便调试
9. Tailwind CSS
  • tailwind-utility-first - 直接在模板中应用工具类,避免使用自定义 CSS
  • tailwind-class-order - 保持类的顺序一致(布局 → 间距 → 排版 → 视觉效果)
  • tailwind-responsive-mobile-first - 采用移动端优先的响应式设计(sm:、md:、lg:)
  • tailwind-component-extraction - 将重复的工具类模式提取为 Vue 组件
  • tailwind-dynamic-classes - 使用计算属性或辅助函数处理动态类
  • tailwind-complete-class-strings - 始终使用完整的类字符串,切勿拼接
  • tailwind-state-variants - 使用状态变体(hover:、focus:、active:)处理交互
  • tailwind-dark-mode - 使用 dark: 前缀支持深色模式
  • tailwind-design-tokens - 在 Tailwind 配置中定义设计令牌,以保持一致性
  • tailwind-avoid-apply-overuse - 限制 @apply 的使用;优先使用 Vue 组件进行抽象
10. PrimeVue
  • primevue-use-natively - 按原样使用 PrimeVue 组件,并遵循其文档中的属性和 API
  • primevue-design-tokens - 仅通过设计令牌和 definePreset() 自定义外观
  • primevue-no-pt-overrides - 绝不要使用 PassThrough (pt) API 重新设置组件样式
  • primevue-no-unstyled-mode - 绝不要使用无样式模式来剥除并重建组件样式
  • primevue-no-wrapper-components - 绝不要仅为覆盖样式而封装 PrimeVue 组件
  • primevue-props-api - 使用内置属性(severity、size、outlined、rounded、raised、text)设置变体
  • primevue-css-layers - 配置 CSS 层的顺序,以便与 Tailwind 协调共存
  • primevue-typed-components - 利用 PrimeVue 的 TypeScript 支持确保类型安全
  • primevue-accessibility - 使用适当的 aria 属性,确保符合 WCAG 标准
  • primevue-lazy-loading - 使用异步组件加载大型 PrimeVue 导入

核心原则

Composition API 最佳实践

Composition API 是 Vue.js 3 推荐的开发方式。请遵循以下模式:

  • 始终使用 <script setup>:代码更简洁,TypeScript 类型推断更好,性能也更出色
  • 按逻辑关注点组织代码:将相关状态、计算属性和函数放在一起
  • 将可复用逻辑提取到组合式函数中:遵循 use 前缀约定(例如 useAuth、useFetch)
  • 保持 setup 代码易读:顺序为:props/emits、响应式状态、计算属性、侦听器、方法、生命周期钩子
组件设计原则

设计良好的组件是构建易于维护的 Vue 应用的基础:

  • 单一职责:每个组件都应专注做好一件事
  • Props 向下传递,事件向上触发:遵循单向数据流
  • 优先组合,而非继承:使用组合式函数和插槽复用代码
  • 保持组件精简:如果组件超过 200 行,请考虑拆分
响应式指南

理解 Vue 的响应式系统至关重要:

  • ref 与 reactive:对基本类型和会重新赋值的值使用 ref();对会修改的对象使用 reactive()
  • 使用计算属性派生状态:不要在 refs 中存储派生状态;改用 computed()
  • 用 watch 处理副作用:仅在 API 调用或 localStorage 操作等副作用场景中使用 watch()
  • 注意响应性丢失:不要在没有 toRefs() 的情况下解构响应式对象
Props 和事件模式

恰当的组件通信有助于确保代码易于维护:

  • 为 props 添加类型:使用 TypeScript 接口和 defineProps<T>()
  • 验证复杂 props:使用验证函数检查业务逻辑相关规则
  • 为事件指定类型:使用 defineEmits<T>() 实现类型安全的事件处理
  • 使用 v-model 实现双向绑定:实现 modelValue prop 和 update:modelValue emit

常见模式

Script Setup 结构

推荐的 <script setup> 结构:

<script setup lang="ts">
// 1. Imports
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import type { User } from '@/types'

// 2. Props and Emits
const props = defineProps<{
  userId: string
  initialData?: User
}>()

const emit = defineEmits<{
  submit: [user: User]
  cancel: []
}>()

// 3. Composables
const router = useRouter()
const { user, loading, error } = useUser(props.userId)

// 4. Reactive State
const formData = ref({ name: '', email: '' })
const isEditing = ref(false)

// 5. Computed Properties
const isValid = computed(() => {
  return formData.value.name.length > 0 && formData.value.email.includes('@')
})

// 6. Watchers (for side effects only)
watch(() => props.userId, (newId) => {
  fetchUserData(newId)
})

// 7. Methods
function handleSubmit() {
  if (isValid.value) {
    emit('submit', formData.value)
  }
}

// 8. Lifecycle Hooks
onMounted(() => {
  if (props.initialData) {
    formData.value = { ...props.initialData }
  }
})
</script>
组合式函数模式

正确:结构良好的组合式函数

// composables/useUser.ts
import { ref, computed, watch } from 'vue'
import type { Ref } from 'vue'
import type { User } from '@/types'

export function useUser(userId: Ref<string> | string) {
  // State
  const user = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)

  // Computed
  const fullName = computed(() => {
    if (!user.value) return ''
    return `${user.value.firstName} ${user.value.lastName}`
  })

  // Methods
  async function fetchUser(id: string) {
    loading.value = true
    error.value = null
    try {
      const response = await api.getUser(id)
      user.value = response.data
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  // Auto-fetch when userId changes (if reactive)
  if (isRef(userId)) {
    watch(userId, (newId) => fetchUser(newId), { immediate: true })
  } else {
    fetchUser(userId)
  }

  // Return
  return {
    user: readonly(user),
    fullName,
    loading: readonly(loading),
    error: readonly(error),
    refresh: () => fetchUser(unref(userId))
  }
}
带默认值的 Props

正确:带默认值的类型化 props

<script setup lang="ts">
interface Props {
  title: string
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  items?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  size: 'md',
  disabled: false,
  items: () => []  // Use factory function for arrays/objects
})
</script>
事件处理

正确:带载荷的类型化 emits

<script setup lang="ts">
interface FormData {
  name: string
  email: string
}

const emit = defineEmits<{
  submit: [data: FormData]
  cancel: []
  'update:modelValue': [value: string]
}>()

function handleSubmit(data: FormData) {
  emit('submit', data)
}
</script>
v-model 实现

正确:使用 defineModel 实现自定义 v-model(Vue 3.4+)

<script setup lang="ts">
const model = defineModel<string>({ required: true })

// Or with default
const modelWithDefault = defineModel<string>({ default: '' })
</script>

<template>
  <input :value="model" @input="model = $event.target.value" />
</template>

正确:自定义 v-model(Vue 3.3 及更早版本)

<script setup lang="ts">
const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

const value = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})
</script>

<template>
  <input v-model="value" />
</template>
模板 Ref 类型

正确:类型化的模板 refs

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'

// DOM element ref
const inputRef = ref<HTMLInputElement | null>(null)

// Component ref
const componentRef = ref<InstanceType<typeof MyComponent> | null>(null)

onMounted(() => {
  inputRef.value?.focus()
  componentRef.value?.someExposedMethod()
})
</script>

<template>
  <input ref="inputRef" />
  <MyComponent ref="componentRef" />
</template>
带类型的 Provide/Inject

正确:类型安全的 provide/inject

// types/injection-keys.ts
import type { InjectionKey, Ref } from 'vue'
import type { User } from './user'

export const UserKey: InjectionKey<Ref<User>> = Symbol('user')

// Parent component
import { provide, ref } from 'vue'
import { UserKey } from '@/types/injection-keys'

const user = ref<User>({ id: '1', name: 'John' })
provide(UserKey, user)

// Child component
import { inject } from 'vue'
import { UserKey } from '@/types/injection-keys'

const user = inject(UserKey)
if (!user) {
  throw new Error('User not provided')
}
错误边界组件

正确:使用 onErrorCaptured 实现错误边界

<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'

const error = ref<Error | null>(null)

onErrorCaptured((err) => {
  error.value = err
  // Return false to stop error propagation
  return false
})

function reset() {
  error.value = null
}
</script>

<template>
  <div v-if="error" class="error-boundary">
    <p>Something went wrong: {{ error.message }}</p>
    <button @click="reset">Try again</button>
  </div>
  <slot v-else />
</template>
异步组件加载

正确:带加载和错误状态的异步组件

import { defineAsyncComponent } from 'vue'

const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./Dashboard.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,  // Show loading after 200ms
  timeout: 10000  // Timeout after 10s
})

Tailwind CSS 最佳实践

Vue 基于组件的架构与 Tailwind 的实用工具优先方法天然契合。请遵循以下模式,确保样式易于维护且保持一致。

实用工具优先方法

在 Vue 模板中直接应用 Tailwind 实用工具类,实现快速且一致的样式:

正确:在模板中使用实用工具类

<template>
  <div class="mx-auto max-w-md rounded-xl bg-white p-6 shadow-lg">
    <h2 class="text-xl font-semibold text-gray-900">{{ title }}</h2>
    <p class="mt-2 text-gray-600">{{ description }}</p>
    <button class="mt-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
      {{ buttonText }}
    </button>
  </div>
</template>
类排序约定

保持类的排序一致,以便于阅读。推荐顺序:

  1. 布局 - flex、grid、block、hidden
  2. 定位 - relative、absolute、fixed
  3. 盒模型 - w-、h-、m-、p-
  4. 排版 - text-、font-、leading-
  5. 视觉 - bg-、border-、rounded-、shadow-
  6. 交互 - hover:、focus:、active:

使用官方 Prettier 插件(prettier-plugin-tailwindcss)自动排序类。

响应式设计(移动优先)

使用 Tailwind 的响应式前缀,实现移动优先的响应式设计:

正确:移动优先的响应式布局

<template>
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
    <article
      v-for="item in items"
      :key="item.id"
      class="p-4 text-sm sm:p-6 sm:text-base lg:text-lg"
    >
      <h3 class="font-medium">{{ item.title }}</h3>
    </article>
  </div>
</template>

断点参考:

  • sm: - 640px 及以上
  • md: - 768px 及以上
  • lg: - 1024px 及以上
  • xl: - 1280px 及以上
  • 2xl: - 1536px 及以上
状态变体

对交互元素使用状态变体:

正确:按钮的状态变体

<template>
  <button
    class="rounded-lg bg-indigo-600 px-4 py-2 font-medium text-white
           transition-colors duration-150
           hover:bg-indigo-700
           focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2
           active:bg-indigo-800
           disabled:cursor-not-allowed disabled:opacity-50"
    :disabled="isLoading"
  >
    {{ isLoading ? 'Loading...' : 'Submit' }}
  </button>
</template>
深色模式支持

使用 dark: 前缀设置深色模式样式:

正确:支持深色模式

<template>
  <div class="bg-white dark:bg-gray-900">
    <h1 class="text-gray-900 dark:text-white">{{ title }}</h1>
    <p class="text-gray-600 dark:text-gray-400">{{ content }}</p>
    <div class="border-gray-200 dark:border-gray-700 rounded-lg border p-4">
      <slot />
    </div>
  </div>
</template>
使用计算属性设置动态类

使用计算属性进行条件类绑定:

正确:使用计算属性设置变体类

<script setup lang="ts">
import { computed } from 'vue'

type ButtonVariant = 'primary' | 'secondary' | 'danger'
type ButtonSize = 'sm' | 'md' | 'lg'

const props = withDefaults(defineProps<{
  variant?: ButtonVariant
  size?: ButtonSize
}>(), {
  variant: 'primary',
  size: 'md'
})

const variantClasses = computed(() => {
  const variants: Record<ButtonVariant, string> = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
    secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
    danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500'
  }
  return variants[props.variant]
})

const sizeClasses = computed(() => {
  const sizes: Record<ButtonSize, string> = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg'
  }
  return sizes[props.size]
})

const buttonClasses = computed(() => [
  'inline-flex items-center justify-center rounded-md font-medium',
  'transition-colors duration-150',
  'focus:outline-none focus:ring-2 focus:ring-offset-2',
  variantClasses.value,
  sizeClasses.value
])
</script>

<template>
  <button :class="buttonClasses">
    <slot />
  </button>
</template>
Class Variance Authority(CVA)模式

对于复杂的组件变体,使用 CVA 模式和辅助库:

正确:使用 CVA 风格管理变体

<script setup lang="ts">
import { computed } from 'vue'
import { cva, type VariantProps } from 'class-variance-authority'

const button = cva(
  'inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
  {
    variants: {
      intent: {
        primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
        secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
        danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500'
      },
      size: {
        sm: 'px-3 py-1.5 text-sm',
        md: 'px-4 py-2 text-base',
        lg: 'px-6 py-3 text-lg'
      }
    },
    defaultVariants: {
      intent: 'primary',
      size: 'md'
    }
  }
)

type ButtonProps = VariantProps<typeof button>

const props = defineProps<{
  intent?: ButtonProps['intent']
  size?: ButtonProps['size']
}>()

const classes = computed(() => button({ intent: props.intent, size: props.size }))
</script>

<template>
  <button :class="classes">
    <slot />
  </button>
</template>
提取组件以复用模式

将重复的实用类模式提取到 Vue 组件中:

正确:可复用的卡片组件


<script setup lang="ts">
withDefaults(defineProps<{
  padding?: 'none' | 'sm' | 'md' | 'lg'
  shadow?: 'none' | 'sm' | 'md' | 'lg'
}>(), {
  padding: 'md',
  shadow: 'md'
})
</script>

<template>
  <div
    class="rounded-xl bg-white dark:bg-gray-800"
    :class="[
      {
        'p-0': padding === 'none',
        'p-4': padding === 'sm',
        'p-6': padding === 'md',
        'p-8': padding === 'lg'
      },
      {
        'shadow-none': shadow === 'none',
        'shadow-sm': shadow === 'sm',
        'shadow-md': shadow === 'md',
        'shadow-lg': shadow === 'lg'
      }
    ]"
  >
    <slot />
  </div>
</template>
使用设计令牌配置 Tailwind

在 Tailwind 配置中定义设计令牌,以保持一致性:

正确:包含设计令牌的 tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: {
        // Semantic color tokens
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8'
        },
        surface: {
          light: '#ffffff',
          dark: '#1f2937'
        }
      },
      spacing: {
        // Custom spacing tokens
        '4.5': '1.125rem',
        '18': '4.5rem'
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif']
      },
      borderRadius: {
        '4xl': '2rem'
      }
    }
  },
  plugins: []
}
Tailwind CSS v4 配置

对于 Tailwind CSS v4,使用 CSS 优先的配置方式:

正确:Tailwind v4 CSS 配置

/* main.css */
@import "tailwindcss";

@theme {
  /* Custom colors */
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-primary-700: #1d4ed8;

  /* Custom spacing */
  --spacing-4-5: 1.125rem;
  --spacing-18: 4.5rem;

  /* Custom fonts */
  --font-family-sans: 'Inter', system-ui, sans-serif;
}
使用 cn() 辅助函数设置条件类

使用类合并工具设置条件类:

正确:结合 clsx 和 tailwind-merge 使用 cn() 辅助函数

// utils/cn.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

在组件中使用:

<script setup lang="ts">
import { cn } from '@/utils/cn'

const props = defineProps<{
  class?: string
  isActive?: boolean
}>()
</script>

<template>
  <div
    :class="cn(
      'rounded-lg border p-4 transition-colors',
      isActive ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-white',
      props.class
    )"
  >
    <slot />
  </div>
</template>

PrimeVue 最佳实践

PrimeVue 是一个全面的 Vue UI 组件库,包含 90 多个组件。原生使用它,不要与框架对着干。

核心原则

PrimeVue v4 拥有一套基于设计令牌和预设的完整主题系统。这是唯一受支持的自定义方式。不要绕过它。

安装与设置

正确:使用主题预设设置 PrimeVue v4

// main.ts
import { createApp } from 'vue'
import PrimeVue from 'primevue/config'
import Aura from '@primevue/themes/aura'
import App from './App.vue'

const app = createApp(App)

app.use(PrimeVue, {
  theme: {
    preset: Aura,
    options: {
      prefix: 'p',
      darkModeSelector: '.dark-mode',
      // Ensure clean coexistence with Tailwind
      cssLayer: {
        name: 'primevue',
        order: 'tailwind-base, primevue, tailwind-utilities'
      }
    }
  }
})

app.mount('#app')

正确:注册组件(支持摇树优化)

// main.ts - Register only components you use
import Button from 'primevue/button'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'

app.component('Button', Button)
app.component('DataTable', DataTable)
app.component('Column', Column)
使用设计令牌自定义主题

通过设计令牌系统自定义 PrimeVue 外观,切勿覆盖组件内部样式:

正确:使用 definePreset() 创建自定义预设

// theme/my-preset.ts
import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'

const MyPreset = definePreset(Aura, {
  semantic: {
    primary: {
      50: '{indigo.50}',
      100: '{indigo.100}',
      200: '{indigo.200}',
      300: '{indigo.300}',
      400: '{indigo.400}',
      500: '{indigo.500}',
      600: '{indigo.600}',
      700: '{indigo.700}',
      800: '{indigo.800}',
      900: '{indigo.900}',
      950: '{indigo.950}'
    },
    colorScheme: {
      light: {
        surface: {
          0: '#ffffff',
          50: '{slate.50}',
          100: '{slate.100}',
          // ...
        }
      },
      dark: {
        surface: {
          0: '#0a0a0a',
          50: '{slate.950}',
          // ...
        }
      }
    }
  }
})

export default MyPreset

在 main.ts 中使用:

import MyPreset from './theme/my-preset'

app.use(PrimeVue, {
  theme: {
    preset: MyPreset
  }
})
原生使用组件

使用带有内置属性的 PrimeVue 组件,不要重新设置它们的样式:

正确:使用内置的 severity 和 variant 属性

<template>
  
  <Button label="Save" severity="success" />
  <Button label="Delete" severity="danger" />
  <Button label="Info" severity="info" outlined />

  
  <Button label="Small" size="small" />
  <Button label="Large" size="large" />

  
  <Button label="Text" text />
  <Button label="Outlined" outlined />
  <Button label="Rounded" rounded />
  <Button label="Raised" raised />

  
  <Button label="Search" icon="pi pi-search" />
  <Button icon="pi pi-check" rounded aria-label="Confirm" />
</template>

正确:使用插槽 API 自定义内容

<template>
  <DataTable :value="users" stripedRows paginator :rows="10">
    <Column field="name" header="Name" sortable />
    <Column field="status" header="Status">
      <template #body="{ data }">
        <Tag :value="data.status" :severity="getStatusSeverity(data.status)" />
      </template>
    </Column>
  </DataTable>
</template>
使用 PrimeVue 时绝对不要做的事

绝对不要使用 PassThrough (pt) 重新设置组件样式:


<Button
  label="Submit"
  :pt="{
    root: { class: 'bg-blue-600 rounded-lg px-4 py-2' },
    label: { class: 'font-medium' }
  }"
/>


<Button label="Submit" />

绝对不要使用无样式模式重建组件:

// INCORRECT: Stripping the framework and rebuilding it
app.use(PrimeVue, { unstyled: true })

// CORRECT: Use a theme preset
app.use(PrimeVue, { theme: { preset: Aura } })

绝对不要创建包装组件来覆盖样式:



<Button v-bind="$attrs" :pt="{ root: { class: customClasses } }">
  <slot />
</Button>


<Button label="Submit" severity="primary" />

绝对不要定位 PrimeVue 内部 CSS 类:

/* INCORRECT: Fragile, breaks on updates */
.p-button-label { font-weight: 700; }
.p-datatable-header { background: #f0f0f0; }

/* CORRECT: Customize through design tokens in definePreset() */
Tailwind 与 PrimeVue 共存

Tailwind 和 PrimeVue 各司其职,不要混淆它们的职责:

  • PrimeVue 负责:组件外观、内部间距、交互状态、组件变体
  • Tailwind 负责:页面布局、组件之间的间距、自定义的非库元素、非库元素的排版

正确:使用 Tailwind 处理布局,原生使用 PrimeVue 组件

<template>
  
  <div class="mx-auto max-w-4xl space-y-6 p-6">
    <h1 class="text-2xl font-bold text-gray-900 dark:text-white">Users</h1>

    
    <DataTable :value="users" stripedRows paginator :rows="10">
      <Column field="name" header="Name" sortable />
      <Column field="email" header="Email" sortable />
      <Column field="role" header="Role" />
    </DataTable>

    
    <div class="flex gap-3">
      <Button label="Add User" icon="pi pi-plus" />
      <Button label="Export" icon="pi pi-download" severity="secondary" outlined />
    </div>
  </div>
</template>

错误:使用 Tailwind 重新设置 PrimeVue 组件样式

<template>
  
  <Button label="Submit" class="rounded-full bg-indigo-600 px-8 py-3 shadow-xl" />
</template>
DataTable 最佳实践

正确:使用组合式 API 的类型化 DataTable

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'

interface User {
  id: number
  name: string
  email: string
  role: string
  status: 'active' | 'inactive'
}

const users = ref<User[]>([])
const loading = ref(true)
const selectedUsers = ref<User[]>([])

// Pagination
const first = ref(0)
const rows = ref(10)

// Sorting
const sortField = ref<string>('name')
const sortOrder = ref<1 | -1>(1)

onMounted(async () => {
  loading.value = true
  try {
    users.value = await fetchUsers()
  } finally {
    loading.value = false
  }
})
</script>

<template>
  <DataTable
    v-model:selection="selectedUsers"
    :value="users"
    :loading="loading"
    :paginator="true"
    :rows="rows"
    :first="first"
    :sortField="sortField"
    :sortOrder="sortOrder"
    dataKey="id"
    stripedRows
    removableSort
    @page="(e) => first = e.first"
    @sort="(e) => { sortField = e.sortField; sortOrder = e.sortOrder }"
  >
    <Column selectionMode="multiple" headerStyle="width: 3rem" />
    <Column field="name" header="Name" sortable />
    <Column field="email" header="Email" sortable />
    <Column field="role" header="Role" sortable />
    <Column field="status" header="Status">
      <template #body="{ data }">
        <span
          :class="[
            'px-2 py-1 rounded-full text-xs font-medium',
            data.status === 'active'
              ? 'bg-green-100 text-green-800'
              : 'bg-red-100 text-red-800'
          ]"
        >
          {{ data.status }}
        </span>
      </template>
    </Column>
  </DataTable>
</template>
表单组件模式

正确:使用 PrimeVue 和验证功能构建表单

<script setup lang="ts">
import { ref, computed } from 'vue'
import InputText from 'primevue/inputtext'
import Password from 'primevue/password'
import Dropdown from 'primevue/dropdown'
import Button from 'primevue/button'
import Message from 'primevue/message'

interface FormData {
  email: string
  password: string
  role: string | null
}

const formData = ref<FormData>({
  email: '',
  password: '',
  role: null
})

const errors = ref<Partial<Record<keyof FormData, string>>>({})
const submitted = ref(false)

const roles = [
  { label: 'Admin', value: 'admin' },
  { label: 'User', value: 'user' },
  { label: 'Guest', value: 'guest' }
]

const isValid = computed(() => {
  return Object.keys(errors.value).length === 0
})

function validate(): boolean {
  errors.value = {}

  if (!formData.value.email) {
    errors.value.email = 'Email is required'
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.value.email)) {
    errors.value.email = 'Invalid email format'
  }

  if (!formData.value.password) {
    errors.value.password = 'Password is required'
  } else if (formData.value.password.length < 8) {
    errors.value.password = 'Password must be at least 8 characters'
  }

  if (!formData.value.role) {
    errors.value.role = 'Role is required'
  }

  return Object.keys(errors.value).length === 0
}

function handleSubmit() {
  submitted.value = true
  if (validate()) {
    // Submit form
    console.log('Form submitted:', formData.value)
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit" class="space-y-4">
    <div class="flex flex-col gap-2">
      <label for="email" class="font-medium">Email</label>
      <InputText
        id="email"
        v-model="formData.email"
        :class="{ 'p-invalid': errors.email }"
        aria-describedby="email-error"
      />
      <Message v-if="errors.email" severity="error" :closable="false">
        {{ errors.email }}
      </Message>
    </div>

    <div class="flex flex-col gap-2">
      <label for="password" class="font-medium">Password</label>
      <Password
        id="password"
        v-model="formData.password"
        :class="{ 'p-invalid': errors.password }"
        toggleMask
        :feedback="false"
        aria-describedby="password-error"
      />
      <Message v-if="errors.password" severity="error" :closable="false">
        {{ errors.password }}
      </Message>
    </div>

    <div class="flex flex-col gap-2">
      <label for="role" class="font-medium">Role</label>
      <Dropdown
        id="role"
        v-model="formData.role"
        :options="roles"
        optionLabel="label"
        optionValue="value"
        placeholder="Select a role"
        :class="{ 'p-invalid': errors.role }"
        aria-describedby="role-error"
      />
      <Message v-if="errors.role" severity="error" :closable="false">
        {{ errors.role }}
      </Message>
    </div>

    <Button type="submit" label="Submit" class="w-full" />
  </form>
</template>
对话框与覆盖层模式

正确:使用可组合函数创建确认对话框

// composables/useConfirmDialog.ts
import { useConfirm } from 'primevue/useconfirm'

export function useConfirmDialog() {
  const confirm = useConfirm()

  function confirmDelete(
    message: string,
    onAccept: () => void,
    onReject?: () => void
  ) {
    confirm.require({
      message,
      header: 'Confirm Delete',
      icon: 'pi pi-exclamation-triangle',
      rejectClass: 'p-button-secondary p-button-outlined',
      acceptClass: 'p-button-danger',
      rejectLabel: 'Cancel',
      acceptLabel: 'Delete',
      accept: onAccept,
      reject: onReject
    })
  }

  function confirmAction(options: {
    message: string
    header: string
    onAccept: () => void
    onReject?: () => void
  }) {
    confirm.require({
      message: options.message,
      header: options.header,
      icon: 'pi pi-info-circle',
      rejectClass: 'p-button-secondary p-button-outlined',
      acceptClass: 'p-button-primary',
      accept: options.onAccept,
      reject: options.onReject
    })
  }

  return {
    confirmDelete,
    confirmAction
  }
}

用法:

<script setup lang="ts">
import { useConfirmDialog } from '@/composables/useConfirmDialog'
import ConfirmDialog from 'primevue/confirmdialog'

const { confirmDelete } = useConfirmDialog()

function handleDelete(item: Item) {
  confirmDelete(
    `Are you sure you want to delete "${item.name}"?`,
    () => deleteItem(item.id)
  )
}
</script>

<template>
  <ConfirmDialog />
  <Button label="Delete" severity="danger" @click="handleDelete(item)" />
</template>
Toast 通知

正确:使用可组合函数调用 Toast 服务

// composables/useNotifications.ts
import { useToast } from 'primevue/usetoast'

export function useNotifications() {
  const toast = useToast()

  function success(summary: string, detail?: string) {
    toast.add({
      severity: 'success',
      summary,
      detail,
      life: 3000
    })
  }

  function error(summary: string, detail?: string) {
    toast.add({
      severity: 'error',
      summary,
      detail,
      life: 5000
    })
  }

  function warn(summary: string, detail?: string) {
    toast.add({
      severity: 'warn',
      summary,
      detail,
      life: 4000
    })
  }

  function info(summary: string, detail?: string) {
    toast.add({
      severity: 'info',
      summary,
      detail,
      life: 3000
    })
  }

  return { success, error, warn, info }
}
无障碍最佳实践

PrimeVue 组件符合 WCAG 2.0 标准。请确保正确使用:

正确:无障碍表单字段

<template>
  <div class="flex flex-col gap-2">
    <label :for="id" class="font-medium">
      {{ label }}
      <span v-if="required" class="text-red-500" aria-hidden="true">*</span>
    </label>
    <InputText
      :id="id"
      v-model="modelValue"
      :aria-required="required"
      :aria-invalid="!!error"
      :aria-describedby="error ? `${id}-error` : undefined"
    />
    <small
      v-if="error"
      :id="`${id}-error`"
      class="text-red-500"
      role="alert"
    >
      {{ error }}
    </small>
  </div>
</template>
延迟加载组件

正确:异步加载大型 PrimeVue 组件

// components/lazy/index.ts
import { defineAsyncComponent } from 'vue'

export const LazyDataTable = defineAsyncComponent({
  loader: () => import('primevue/datatable'),
  loadingComponent: () => import('@/components/ui/TableSkeleton.vue'),
  delay: 200
})

export const LazyEditor = defineAsyncComponent({
  loader: () => import('primevue/editor'),
  loadingComponent: () => import('@/components/ui/EditorSkeleton.vue'),
  delay: 200
})

export const LazyChart = defineAsyncComponent({
  loader: () => import('primevue/chart'),
  loadingComponent: () => import('@/components/ui/ChartSkeleton.vue'),
  delay: 200
})

应避免的反模式

不要修改属性

错误:

<script setup>
const props = defineProps(['items'])

function addItem(item) {
  props.items.push(item)  // Never mutate props!
}
</script>

正确:

<script setup>
const props = defineProps(['items'])
const emit = defineEmits(['update:items'])

function addItem(item) {
  emit('update:items', [...props.items, item])
}
</script>
不要同时使用 v-if 和 v-for

错误:

<template>
  <div v-for="item in items" v-if="item.isActive" :key="item.id">
    {{ item.name }}
  </div>
</template>

正确:

<script setup>
const activeItems = computed(() => items.value.filter(item => item.isActive))
</script>

<template>
  <div v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </div>
</template>
不要存储派生状态

错误:

<script setup>
const items = ref([])
const itemCount = ref(0)  // Derived state stored separately

watch(items, () => {
  itemCount.value = items.value.length  // Manually syncing
})
</script>

正确:

<script setup>
const items = ref([])
const itemCount = computed(() => items.value.length)  // Computed property
</script>
不要解构响应式对象

错误:

<script setup>
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state  // Loses reactivity!
</script>

正确:

<script setup>
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)  // Preserves reactivity
</script>
不要拼接 Tailwind 类名

动态拼接类名会导致 Tailwind 编译器出错,并在生产环境中清除这些类:

错误示例:

<script setup>
const color = ref('blue')
</script>

<template>
  
  <div :class="`bg-${color}-500 text-${color}-900`">
    Content
  </div>
</template>

正确示例:

<script setup>
const color = ref<'blue' | 'green' | 'red'>('blue')

const colorClasses = computed(() => {
  const colors = {
    blue: 'bg-blue-500 text-blue-900',
    green: 'bg-green-500 text-green-900',
    red: 'bg-red-500 text-red-900'
  }
  return colors[color.value]
})
</script>

<template>
  <div :class="colorClasses">
    Content
  </div>
</template>
不要过度使用 @apply

过度使用 @apply 会违背实用优先 CSS 的初衷:

错误示例:

/* styles.css */
.card {
  @apply mx-auto max-w-md rounded-xl bg-white p-6 shadow-lg;
}

.card-title {
  @apply text-xl font-semibold text-gray-900;
}

.card-description {
  @apply mt-2 text-gray-600;
}

.card-button {
  @apply mt-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700;
}

正确示例:改用 Vue 组件


<template>
  <div class="mx-auto max-w-md rounded-xl bg-white p-6 shadow-lg">
    <h2 class="text-xl font-semibold text-gray-900">
      <slot name="title" />
    </h2>
    <p class="mt-2 text-gray-600">
      <slot name="description" />
    </p>
    <div class="mt-4">
      <slot name="actions" />
    </div>
  </div>
</template>
不要使用冲突的工具类

同时应用多个针对同一 CSS 属性的工具类会导致结果难以预测:

错误示例:

<template>
  
  <div class="flex grid">Content</div>

  
  <div class="m-4 mx-6">Content</div>
</template>

正确示例:

<template>
  <div :class="isGrid ? 'grid' : 'flex'">Content</div>

  
  <div class="mx-6 my-4">Content</div>
</template>
不要忽视无障碍访问

除了视觉样式,始终要包含适当的无障碍属性:

错误示例:

<template>
  <button class="rounded bg-blue-600 p-2 text-white">
    <IconX />
  </button>
</template>

正确示例:

<template>
  <button
    class="rounded bg-blue-600 p-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
    aria-label="Close dialog"
  >
    <IconX aria-hidden="true" />
  </button>
</template>
不要创建过长的类名字符串

将复杂的类名组合拆分为逻辑分组或组件:

错误示例:

<template>
  <div class="mx-auto mt-8 flex max-w-4xl flex-col items-center justify-between gap-4 rounded-xl border border-gray-200 bg-white p-6 shadow-lg transition-all duration-300 hover:border-blue-500 hover:shadow-xl dark:border-gray-700 dark:bg-gray-800 sm:flex-row sm:gap-6 md:p-8 lg:gap-8">
    
  </div>
</template>

正确示例:提取为组件或使用 computed

<script setup>
const containerClasses = [
  // Layout
  'mx-auto max-w-4xl flex flex-col sm:flex-row',
  'items-center justify-between',
  'gap-4 sm:gap-6 lg:gap-8',
  // Spacing
  'mt-8 p-6 md:p-8',
  // Visual
  'rounded-xl border bg-white shadow-lg',
  'border-gray-200 dark:border-gray-700 dark:bg-gray-800',
  // Interactive
  'transition-all duration-300',
  'hover:border-blue-500 hover:shadow-xl'
]
</script>

<template>
  <div :class="containerClasses">
    <slot />
  </div>
</template>
不要用 CSS 覆盖 PrimeVue 样式

使用 CSS 覆盖会绕过设计系统,并导致维护问题:

错误示例:

/* styles.css - Avoid this approach */
.p-button {
  background-color: #3b82f6 !important;
  border-radius: 8px !important;
}

.p-datatable .p-datatable-thead > tr > th {
  background: #f3f4f6 !important;
}

正确示例:使用设计令牌或 PassThrough

// main.ts - Use design tokens
app.use(PrimeVue, {
  theme: {
    preset: Aura,
    options: {
      cssLayer: {
        name: 'primevue',
        order: 'tailwind-base, primevue, tailwind-utilities'
      }
    }
  },
  pt: {
    button: {
      root: { class: 'rounded-lg' }
    }
  }
})
不要导入整个 PrimeVue 库

导入所有内容会使打包体积膨胀:

错误示例:

// main.ts - Don't do this
import PrimeVue from 'primevue/config'
import * as PrimeVueComponents from 'primevue'  // Imports everything!

Object.entries(PrimeVueComponents).forEach(([name, component]) => {
  app.component(name, component)
})

正确示例:只导入所需内容

// main.ts - Tree-shakeable imports
import Button from 'primevue/button'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'

app.component('Button', Button)
app.component('DataTable', DataTable)
app.component('Column', Column)
不要不一致地混用有样式和无样式模式

混用不同模式会造成视觉不一致:

错误示例:

// main.ts
app.use(PrimeVue, {
  unstyled: true  // Global unstyled
})

// SomeComponent.vue - Using styled component anyway
<Button label="Click" />  // No styles applied, looks broken

正确示例:始终采用同一种方式

// Option 1: Styled mode with PT customization
app.use(PrimeVue, {
  theme: { preset: Aura },
  pt: { /* global customizations */ }
})

// Option 2: Unstyled mode with complete PT styling
app.use(PrimeVue, {
  unstyled: true,
  pt: {
    button: {
      root: { class: 'px-4 py-2 bg-primary-600 text-white rounded-lg' }
    }
    // ... complete styling for all components
  }
})
不要忽视无障碍属性

PrimeVue 开箱即提供无障碍支持,不要禁用或忽视这些功能:

错误示例:

<template>
  
  <Button icon="pi pi-trash" @click="deleteItem" />

  
  <InputText v-model="email" :class="{ 'p-invalid': hasError }" />
  <span class="error">Invalid email</span>
</template>

正确示例:保持无障碍支持

<template>
  <Button
    icon="pi pi-trash"
    aria-label="Delete item"
    @click="deleteItem"
  />

  <div class="flex flex-col gap-2">
    <label for="email">Email</label>
    <InputText
      id="email"
      v-model="email"
      :class="{ 'p-invalid': hasError }"
      :aria-invalid="hasError"
      aria-describedby="email-error"
    />
    <small id="email-error" v-if="hasError" class="text-red-500" role="alert">
      Invalid email
    </small>
  </div>
</template>
不要在每个组件中硬编码 PassThrough

在多个组件中重复 PT 配置会造成冗余:

错误示例:


<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />


<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />


<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />

正确示例:使用全局 PT 或封装组件

// main.ts - Global configuration
app.use(PrimeVue, {
  pt: {
    button: {
      root: { class: 'rounded-lg shadow-md' }
    }
  }
})

// Or use wrapper components (see Wrapper Components Pattern above)

Nuxt.js 特定指南

使用 Nuxt.js 时,请遵循以下额外模式:

  • 自动导入:利用 Nuxt 的自动导入功能导入 Vue API 和组合式函数
  • useFetch/useAsyncData:使用 Nuxt 的数据获取组合式函数,以加载兼容 SSR 的数据
  • definePageMeta:用于页面级元数据和中间件
  • 服务端路由:使用 server/api/ 定义 API 端点
  • 运行时配置:使用 useRuntimeConfig() 配置环境变量

参考资料

Vue.js
Tailwind CSS
PrimeVue

评论

支持 Markdown