关于 vue-best-practices
面向 Vue.js 3 开发者的最佳实践技能,适用于构建、审查或重构 Vue 应用,也涵盖 Nuxt.js 项目。内容按组合式 API、响应性、组件设计、TypeScript、Tailwind CSS 和 PrimeVue 分类。
主要功能
- 01组合式 API 与组合式函数指南
- 02组件设计与响应性模式
- 03类型化 Props、事件和模板
- 04错误处理与异步组件
- 05Tailwind CSS 样式实践
- 06PrimeVue 集成与主题配置
适用场景
- 01构建 Vue 组件和组合式函数
- 02审查或重构 Vue 应用代码
- 03规划组件架构
- 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-casecomponent-small-focused- 组件控制在 200 行以内component-presentational-container- 在有益时将逻辑与表现分离component-slots-flexibility- 使用插槽灵活组合组件component-expose-minimal- 仅通过defineExpose()暴露必要内容
3. 响应式模式
reactive-const-refs- 始终使用const声明 refreactive-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- 明确区分必需和可选的 propprops-default-values- 使用withDefaults()提供合理的默认值props-immutable- 绝不直接修改 propsprops-validation- 对复杂的 prop 验证使用校验函数events-define-emits- 始终使用defineEmits<T>()定义 emitsevents-naming- 模板中的事件名称使用 kebab-caseevents-payload-objects- 多值事件使用对象传递数据
5. 模板模式
template-v-if-v-show- 条件渲染使用v-if,切换显示状态使用v-showtemplate-v-for-key- 使用v-for时,始终设置唯一且稳定的:keytemplate-v-if-v-for- 不要在同一个元素上同时使用v-if和v-fortemplate-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- 为错误状态提供备用 UIerror-logging- 适当地记录错误,以便调试
9. Tailwind CSS
tailwind-utility-first- 直接在模板中应用工具类,避免使用自定义 CSStailwind-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 组件,并遵循其文档中的属性和 APIprimevue-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 实现双向绑定:实现
modelValueprop 和update:modelValueemit
常见模式
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>
类排序约定
保持类的排序一致,以便于阅读。推荐顺序:
- 布局 -
flex、grid、block、hidden - 定位 -
relative、absolute、fixed - 盒模型 -
w-、h-、m-、p- - 排版 -
text-、font-、leading- - 视觉 -
bg-、border-、rounded-、shadow- - 交互 -
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
- Tailwind CSS 文档
- 使用工具类进行样式设置
- Tailwind CSS v4 发布说明
- Class Variance Authority (CVA)
- tailwind-merge
- prettier-plugin-tailwindcss
- Vue School - Tailwind CSS 基础知识