vue-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVue.js Best Practices
Vue.js 最佳实践
Comprehensive best practices guide for Vue.js 3 applications. Contains guidelines across multiple categories to ensure idiomatic, maintainable, and scalable Vue.js code, including Tailwind CSS integration patterns for utility-first styling and PrimeVue component library best practices.
这是一份针对 Vue.js 3 应用的全面最佳实践指南,包含多个类别的规范,帮助你编写符合惯用风格、可维护且可扩展的 Vue.js 代码,其中还包括 utility-first 样式方案的 Tailwind CSS 集成模式,以及 PrimeVue 组件库的最佳实践。
When to Apply
适用场景
Reference these guidelines when:
- Writing new Vue components or composables
- Implementing features with Composition API
- Reviewing code for Vue.js patterns compliance
- Refactoring existing Vue.js code
- Setting up component architecture
- Working with Nuxt.js applications
- Styling Vue components with Tailwind CSS utility classes
- Creating design systems with Tailwind and Vue
- Using PrimeVue component library natively
- Customizing PrimeVue theme through design tokens and definePreset()
在以下场景中可以参考本指南:
- 编写新的 Vue 组件或 composables
- 基于 Composition API 实现功能
- 评审代码是否符合 Vue.js 规范
- 重构现有 Vue.js 代码
- 搭建组件架构
- 开发 Nuxt.js 应用
- 使用 Tailwind CSS 工具类为 Vue 组件设置样式
- 基于 Tailwind 和 Vue 创建设计系统
- 原生使用 PrimeVue 组件库
- 通过设计令牌和 definePreset() 自定义 PrimeVue 主题
Rule Categories
规则分类
| Category | Focus | Prefix |
|---|---|---|
| Composition API | Proper use of Composition API patterns | |
| Component Design | Component structure and organization | |
| Reactivity | Reactive state management patterns | |
| Props & Events | Component communication patterns | |
| Template Patterns | Template syntax best practices | |
| Code Organization | Project and code structure | |
| TypeScript | Type-safe Vue.js patterns | |
| Error Handling | Error boundaries and handling | |
| Tailwind CSS | Utility-first styling patterns | |
| PrimeVue | Component library integration patterns | |
| 分类 | 关注点 | 前缀 |
|---|---|---|
| Composition API | Composition API 模式的正确用法 | |
| 组件设计 | 组件结构与组织规范 | |
| 响应式 | 响应式状态管理模式 | |
| Props & Events | 组件通信模式 | |
| 模板模式 | 模板语法最佳实践 | |
| 代码组织 | 项目与代码结构规范 | |
| TypeScript | Vue.js 类型安全模式 | |
| 错误处理 | 错误边界与处理方案 | |
| Tailwind CSS | utility-first 样式模式 | |
| PrimeVue | 组件库集成模式 | |
Quick Reference
快速参考
1. Composition API Best Practices
1. Composition API 最佳实践
- - Always use
composition-script-setupfor single-file components<script setup> - - Use
composition-ref-vs-reactivefor primitives,ref()for objectsreactive() - - Use
composition-computed-derivedfor all derived statecomputed() - - Use
composition-watch-side-effects/watch()only for side effectswatchEffect() - - Extract reusable logic into composables
composition-composables - - Place lifecycle hooks after reactive state declarations
composition-lifecycle-order - - Never use
composition-avoid-thisin Composition APIthis
- - 单文件组件始终使用
composition-script-setup<script setup> - - 基础类型值用
composition-ref-vs-reactive,对象用ref()reactive() - - 所有派生状态都使用
composition-computed-derivedcomputed() - -
composition-watch-side-effects/watch()仅用于处理副作用watchEffect() - - 将可复用逻辑抽离为 composables
composition-composables - - 生命周期钩子放在响应式状态声明之后
composition-lifecycle-order - - Composition API 中永远不要使用
composition-avoid-thisthis
2. Component Design
2. 组件设计
- - One component, one purpose
component-single-responsibility - - Use PascalCase for components, kebab-case in templates
component-naming-convention - - Keep components under 200 lines
component-small-focused - - Separate logic from presentation when beneficial
component-presentational-container - - Use slots for flexible component composition
component-slots-flexibility - - Only expose what's necessary via
component-expose-minimaldefineExpose()
- - 一个组件只承担一个职责
component-single-responsibility - - 组件名使用 PascalCase,模板中使用 kebab-case
component-naming-convention - - 组件代码行数控制在200行以内
component-small-focused - - 适当时将逻辑与展示层分离
component-presentational-container - - 使用插槽实现灵活的组件组合
component-slots-flexibility - - 通过
component-expose-minimal仅暴露必要的内容defineExpose()
3. Reactivity Patterns
3. 响应式模式
- - Always declare refs with
reactive-const-refsconst - - Let Vue unwrap refs in templates (no
reactive-unwrap-template).value - - Use
reactive-shallow-large-data/shallowRef()for large non-reactive datashallowReactive() - - Use
reactive-readonly-propsto prevent mutationsreadonly() - - Use
reactive-toRefs-destructurewhen destructuring reactive objectstoRefs() - - Prefer immutable updates for complex state
reactive-avoid-mutation
- - 始终使用
reactive-const-refs声明 refsconst - - 让 Vue 在模板中自动解包 refs(无需写
reactive-unwrap-template).value - - 大型非响应式数据使用
reactive-shallow-large-data/shallowRef()shallowReactive() - - 使用
reactive-readonly-props防止状态被意外修改readonly() - - 解构响应式对象时使用
reactive-toRefs-destructuretoRefs() - - 复杂状态优先使用不可变更新
reactive-avoid-mutation
4. Props & Events
4. Props & Events
- - Always define prop types with
props-define-typesdefineProps<T>() - - Be explicit about required vs optional props
props-required-explicit - - Provide sensible defaults with
props-default-valueswithDefaults() - - Never mutate props directly
props-immutable - - Use validator functions for complex prop validation
props-validation - - Always define emits with
events-define-emitsdefineEmits<T>() - - Use kebab-case for event names in templates
events-naming - - Pass objects for events with multiple values
events-payload-objects
- - 始终使用
props-define-types定义 prop 类型defineProps<T>() - - 明确区分必填和可选 props
props-required-explicit - - 使用
props-default-values提供合理的默认值withDefaults() - - 永远不要直接修改 props
props-immutable - - 复杂 prop 校验使用校验函数
props-validation - - 始终使用
events-define-emits定义 emitsdefineEmits<T>() - - 模板中事件名使用 kebab-case
events-naming - - 多参数事件传递对象作为 payload
events-payload-objects
5. Template Patterns
5. 模板模式
- - Use
template-v-if-v-showfor conditional rendering,v-iffor togglingv-show - - Always use unique, stable
template-v-for-keywith:keyv-for - - Never use
template-v-if-v-forandv-ifon the same elementv-for - - Move complex expressions to computed properties
template-computed-expressions - - Use event modifiers (
template-event-modifiers,.prevent) appropriately.stop - - Use shorthand syntax (
template-v-bind-shorthandfor:,v-bindfor@)v-on - - Use v-model modifiers (
template-v-model-modifiers,.trim,.number).lazy
- - 条件渲染用
template-v-if-v-show,频繁切换用v-ifv-show - -
template-v-for-key始终搭配唯一稳定的v-for使用:key - - 永远不要在同一个元素上同时使用
template-v-if-v-for和v-ifv-for - - 复杂表达式抽离到计算属性中
template-computed-expressions - - 合理使用事件修饰符(
template-event-modifiers、.prevent等).stop - - 使用简写语法(
template-v-bind-shorthand用v-bind,:用v-on)@ - - 使用 v-model 修饰符(
template-v-model-modifiers、.trim、.number).lazy
6. Code Organization
6. 代码组织
- - Organize by feature, not by type
organization-feature-folders - - Keep composables in dedicated
organization-composables-folderfoldercomposables/ - - Use index files for clean imports
organization-barrel-exports - - Follow consistent naming conventions
organization-consistent-naming - - Colocate related files (component, tests, styles)
organization-colocation
- - 按功能模块组织文件,而非按文件类型
organization-feature-folders - - composables 统一放在专门的
organization-composables-folder目录下composables/ - - 使用 index 文件简化导入路径
organization-barrel-exports - - 遵循统一的命名规范
organization-consistent-naming - - 关联文件放在同一位置(组件、测试、样式等)
organization-colocation
7. TypeScript Integration
7. TypeScript 集成
- - Use generics for reusable typed components
typescript-generic-components - - Use TypeScript interfaces for prop definitions
typescript-prop-types - - Type emit payloads explicitly
typescript-emit-types - - Specify types for refs when not inferred
typescript-ref-typing - - Type template refs with
typescript-template-refsref<InstanceType<typeof Component> | null>(null)
- - 可复用的带类型组件使用泛型
typescript-generic-components - - prop 定义使用 TypeScript 接口
typescript-prop-types - - 显式为 emit 的 payload 定义类型
typescript-emit-types - - 当类型无法自动推断时,为 refs 指定类型
typescript-ref-typing - - 模板 ref 按照
typescript-template-refs方式定义类型ref<InstanceType<typeof Component> | null>(null)
8. Error Handling
8. 错误处理
- - Use
error-boundariesfor component error boundariesonErrorCaptured() - - Handle errors in async operations explicitly
error-async-handling - - Provide fallback UI for error states
error-provide-fallbacks - - Log errors appropriately for debugging
error-logging
- - 使用
error-boundaries实现组件错误边界onErrorCaptured() - - 显式处理异步操作中的错误
error-async-handling - - 为错误状态提供降级 UI
error-provide-fallbacks - - 合理记录错误便于调试
error-logging
9. Tailwind CSS
9. Tailwind CSS
- - Apply utility classes directly in templates, avoid custom CSS
tailwind-utility-first - - Use consistent class ordering (layout → spacing → typography → visual)
tailwind-class-order - - Use mobile-first responsive design (
tailwind-responsive-mobile-first,sm:,md:)lg: - - Extract repeated utility patterns into Vue components
tailwind-component-extraction - - Use computed properties or helper functions for dynamic classes
tailwind-dynamic-classes - - Always use complete class strings, never concatenate
tailwind-complete-class-strings - - Use state variants (
tailwind-state-variants,hover:,focus:) for interactionsactive: - - Use
tailwind-dark-modeprefix for dark mode supportdark: - - Configure design tokens in Tailwind config for consistency
tailwind-design-tokens - - Limit
tailwind-avoid-apply-overuseusage; prefer Vue components for abstraction@apply
- - 直接在模板中使用工具类,避免编写自定义 CSS
tailwind-utility-first - - 保持统一的类名顺序(布局 → 间距 → 排版 → 视觉 → 交互)
tailwind-class-order - - 使用移动端优先的响应式设计(
tailwind-responsive-mobile-first、sm:、md:)lg: - - 将重复的工具类模式抽离为 Vue 组件
tailwind-component-extraction - - 动态类名使用计算属性或辅助函数生成
tailwind-dynamic-classes - - 始终使用完整的类名字符串,不要拼接
tailwind-complete-class-strings - - 使用状态变体(
tailwind-state-variants、hover:、focus:)实现交互效果active: - - 使用
tailwind-dark-mode前缀适配暗黑模式dark: - - 在 Tailwind 配置中定义设计令牌保证一致性
tailwind-design-tokens - - 限制
tailwind-avoid-apply-overuse的使用,优先用 Vue 组件做抽象@apply
10. PrimeVue
10. PrimeVue
- - Use PrimeVue components as-is with their documented props and API
primevue-use-natively - - Customize appearance exclusively through design tokens and
primevue-design-tokensdefinePreset() - - NEVER use PassThrough (pt) API to restyle components
primevue-no-pt-overrides - - NEVER use unstyled mode to strip and rebuild component styles
primevue-no-unstyled-mode - - NEVER wrap PrimeVue components just to override their styling
primevue-no-wrapper-components - - Use built-in props (severity, size, outlined, rounded, raised, text) for variants
primevue-props-api - - Configure CSS layer ordering for clean Tailwind coexistence
primevue-css-layers - - Leverage PrimeVue's TypeScript support for type safety
primevue-typed-components - - Maintain WCAG compliance with proper aria attributes
primevue-accessibility - - Use async components for large PrimeVue imports
primevue-lazy-loading
- - 直接使用 PrimeVue 组件及其官方文档提供的 props 和 API
primevue-use-natively - - 仅通过设计令牌和
primevue-design-tokens自定义外观definePreset() - - 永远不要使用 PassThrough (pt) API 重写组件样式
primevue-no-pt-overrides - - 永远不要使用无样式模式剥离并重建组件样式
primevue-no-unstyled-mode - - 永远不要只为了覆盖样式而封装 PrimeVue 组件
primevue-no-wrapper-components - - 使用内置 props(severity、size、outlined、rounded、raised、text)实现不同变体
primevue-props-api - - 配置 CSS 层级顺序,保证和 Tailwind 共存无冲突
primevue-css-layers - - 利用 PrimeVue 的 TypeScript 支持实现类型安全
primevue-typed-components - - 通过正确的 aria 属性保持 WCAG 合规
primevue-accessibility - - 大型 PrimeVue 导入使用异步组件
primevue-lazy-loading
Key Principles
核心原则
Composition API Best Practices
Composition API 最佳实践
The Composition API is the recommended approach for Vue.js 3. Follow these patterns:
- Always use : More concise, better TypeScript inference, and improved performance
<script setup> - Organize code by logical concern: Group related state, computed properties, and functions together
- Extract reusable logic to composables: Follow the prefix convention (e.g.,
use,useAuth)useFetch - Keep setup code readable: Order: props/emits, reactive state, computed, watchers, methods, lifecycle hooks
Composition API 是 Vue.js 3 推荐的开发方式,遵循以下模式:
- 始终使用 :语法更简洁,TypeScript 类型推断更好,性能更优
<script setup> - 按逻辑关注点组织代码:将相关的状态、计算属性、函数分组放在一起
- 可复用逻辑抽离为 composables:遵循 前缀命名规范(如
use、useAuth)useFetch - 保持 setup 代码可读性:代码顺序:props/emits → 响应式状态 → 计算属性 → watchers → 方法 → 生命周期钩子
Component Design Principles
组件设计原则
Well-designed components are the foundation of maintainable Vue applications:
- Single Responsibility: Each component should do one thing well
- Props Down, Events Up: Follow unidirectional data flow
- Prefer Composition over Inheritance: Use composables and slots for code reuse
- Keep Components Small: If a component exceeds 200 lines, consider splitting it
设计良好的组件是可维护 Vue 应用的基础:
- 单一职责:每个组件只做好一件事
- Props 向下,Events 向上:遵循单向数据流
- 优先组合而非继承:使用 composables 和插槽实现代码复用
- 保持组件精简:如果组件代码超过200行,考虑拆分
Reactivity Guidelines
响应式指南
Understanding Vue's reactivity system is crucial:
- ref vs reactive: Use for primitives and values you'll reassign; use
ref()for objects you'll mutatereactive() - Computed for derived state: Never store derived state in refs; use instead
computed() - Watch for side effects: Only use for side effects like API calls or localStorage
watch() - Be mindful of reactivity loss: Don't destructure reactive objects without
toRefs()
理解 Vue 的响应式系统至关重要:
- ref vs reactive:基础类型和需要重新赋值的变量用 ;会直接修改的对象用
ref()reactive() - 派生状态用 computed:永远不要在 refs 中存储派生状态,改用
computed() - watch 仅用于副作用:只用于处理 API 调用、localStorage 操作等副作用
watch() - 注意响应式丢失:没有使用 时不要解构响应式对象
toRefs()
Props & Events Patterns
Props & Events 模式
Proper component communication ensures maintainable code:
- Type your props: Use TypeScript interfaces with
defineProps<T>() - Validate complex props: Use validator functions for business logic validation
- Emit typed events: Use for type-safe event handling
defineEmits<T>() - Use v-model for two-way binding: Implement prop and
modelValueemitupdate:modelValue
合理的组件通信保证代码可维护:
- 为 props 定义类型:结合 使用 TypeScript 接口
defineProps<T>() - 校验复杂 props:使用校验函数实现业务逻辑层面的校验
- 定义带类型的 emits:使用 实现类型安全的事件处理
defineEmits<T>() - 双向绑定用 v-model:实现 prop 和
modelValueemitupdate:modelValue
Common Patterns
常用模式
Script Setup Structure
Script Setup 结构
Recommended structure for :
<script setup>vue
<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><script setup>vue
<script setup lang="ts">
// 1. 导入
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import type { User } from '@/types'
// 2. Props 和 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. 响应式状态
const formData = ref({ name: '', email: '' })
const isEditing = ref(false)
// 5. 计算属性
const isValid = computed(() => {
return formData.value.name.length > 0 && formData.value.email.includes('@')
})
// 6. Watchers(仅用于处理副作用)
watch(() => props.userId, (newId) => {
fetchUserData(newId)
})
// 7. 方法
function handleSubmit() {
if (isValid.value) {
emit('submit', formData.value)
}
}
// 8. 生命周期钩子
onMounted(() => {
if (props.initialData) {
formData.value = { ...props.initialData }
}
})
</script>Composable Pattern
Composable 模式
Correct: Well-structured composable
typescript
// 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))
}
}正确示例:结构清晰的 composable
typescript
// 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) {
// 状态
const user = ref<User | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
// 计算属性
const fullName = computed(() => {
if (!user.value) return ''
return `${user.value.firstName} ${user.value.lastName}`
})
// 方法
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
}
}
// userId 变化时自动获取(如果是响应式变量)
if (isRef(userId)) {
watch(userId, (newId) => fetchUser(newId), { immediate: true })
} else {
fetchUser(userId)
}
// 返回值
return {
user: readonly(user),
fullName,
loading: readonly(loading),
error: readonly(error),
refresh: () => fetchUser(unref(userId))
}
}Props with Defaults
带默认值的 Props
Correct: Typed props with defaults
vue
<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>正确示例:带默认值的带类型 props
vue
<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: () => [] // 数组/对象默认值使用工厂函数
})
</script>Event Handling
事件处理
Correct: Typed emits with payloads
vue
<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>正确示例:带 payload 的带类型 emits
vue
<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 Implementation
v-model 实现
Correct: Custom v-model with defineModel (Vue 3.4+)
vue
<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>Correct: Custom v-model (Vue 3.3 and earlier)
vue
<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>正确示例:Vue 3.4+ 使用 defineModel 实现自定义 v-model
vue
<script setup lang="ts">
const model = defineModel<string>({ required: true })
// 或带默认值
const modelWithDefault = defineModel<string>({ default: '' })
</script>
<template>
<input :value="model" @input="model = $event.target.value" />
</template>正确示例:Vue 3.3 及更早版本自定义 v-model
vue
<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>Template Ref Typing
模板 Ref 类型定义
Correct: Typed template refs
vue
<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>正确示例:带类型的模板 refs
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'
// DOM 元素 ref
const inputRef = ref<HTMLInputElement | null>(null)
// 组件 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 with Types
带类型的 Provide/Inject
Correct: Type-safe provide/inject
typescript
// 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')
}正确示例:类型安全的 provide/inject
typescript
// types/injection-keys.ts
import type { InjectionKey, Ref } from 'vue'
import type { User } from './user'
export const UserKey: InjectionKey<Ref<User>> = Symbol('user')
// 父组件
import { provide, ref } from 'vue'
import { UserKey } from '@/types/injection-keys'
const user = ref<User>({ id: '1', name: 'John' })
provide(UserKey, user)
// 子组件
import { inject } from 'vue'
import { UserKey } from '@/types/injection-keys'
const user = inject(UserKey)
if (!user) {
throw new Error('User not provided')
}Error Boundary Component
错误边界组件
Correct: Error boundary with onErrorCaptured
vue
<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>正确示例:使用 onErrorCaptured 实现错误边界
vue
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'
const error = ref<Error | null>(null)
onErrorCaptured((err) => {
error.value = err
// 返回 false 停止错误冒泡
return false
})
function reset() {
error.value = null
}
</script>
<template>
<div v-if="error" class="error-boundary">
<p>出现错误:{{ error.message }}</p>
<button @click="reset">重试</button>
</div>
<slot v-else />
</template>Async Component Loading
异步组件加载
Correct: Async components with loading/error states
typescript
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
})正确示例:带加载/错误状态的异步组件
typescript
import { defineAsyncComponent } from 'vue'
const AsyncDashboard = defineAsyncComponent({
loader: () => import('./Dashboard.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // 200ms 后显示加载状态
timeout: 10000 // 10s 后超时
})Tailwind CSS Best Practices
Tailwind CSS 最佳实践
Vue's component-based architecture pairs naturally with Tailwind's utility-first approach. Follow these patterns for maintainable, consistent styling.
Vue 的组件化架构和 Tailwind 的 utility-first 方式天然适配,遵循以下模式实现可维护、统一的样式。
Utility-First Approach
Utility-First 方式
Apply Tailwind utility classes directly in Vue templates for rapid, consistent styling:
Correct: Utility classes in template
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">{{ 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>直接在 Vue 模板中使用 Tailwind 工具类,实现快速、统一的样式开发:
正确示例:模板中使用工具类
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">{{ 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>Class Ordering Convention
类名顺序规范
Maintain consistent class ordering for readability. Recommended order:
- Layout - ,
flex,grid,blockhidden - Positioning - ,
relative,absolutefixed - Box Model - ,
w-,h-,m-p- - Typography - ,
text-,font-leading- - Visual - ,
bg-,border-,rounded-shadow- - Interactive - ,
hover:,focus:active:
Use the official Prettier plugin () to automatically sort classes.
prettier-plugin-tailwindcss保持统一的类名顺序提升可读性,推荐顺序:
- 布局 - 、
flex、grid、blockhidden - 定位 - 、
relative、absolutefixed - 盒模型 - 、
w-、h-、m-p- - 排版 - 、
text-、font-leading- - 视觉 - 、
bg-、border-、rounded-shadow- - 交互 - 、
hover:、focus:active:
使用官方 Prettier 插件()自动排序类名。
prettier-plugin-tailwindcssResponsive Design (Mobile-First)
响应式设计(移动端优先)
Use Tailwind's responsive prefixes for mobile-first responsive design:
Correct: Mobile-first responsive layout
vue
<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>Breakpoint Reference:
- - 640px and up
sm: - - 768px and up
md: - - 1024px and up
lg: - - 1280px and up
xl: - - 1536px and up
2xl:
使用 Tailwind 响应式前缀实现移动端优先的响应式设计:
正确示例:移动端优先的响应式布局
vue
<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>断点参考:
- - 640px 及以上
sm: - - 768px 及以上
md: - - 1024px 及以上
lg: - - 1280px 及以上
xl: - - 1536px 及以上
2xl:
State Variants
状态变体
Use state variants for interactive elements:
Correct: State variants for buttons
vue
<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>交互元素使用状态变体:
正确示例:按钮的状态变体
vue
<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 ? '加载中...' : '提交' }}
</button>
</template>Dark Mode Support
暗黑模式支持
Use the prefix for dark mode styles:
dark:Correct: Dark mode support
vue
<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>使用 前缀实现暗黑模式样式:
dark:正确示例:暗黑模式支持
vue
<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>Dynamic Classes with Computed Properties
结合计算属性实现动态类名
Use computed properties for conditional class binding:
Correct: Computed classes for variants
vue
<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>使用计算属性实现条件类绑定:
正确示例:计算属性实现变体类名
vue
<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) Pattern
Class Variance Authority (CVA) 模式
For complex component variants, use the CVA pattern with a helper library:
Correct: CVA-style variant management
vue
<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>复杂组件变体可以使用辅助库实现 CVA 模式:
正确示例:CVA 风格的变体管理
vue
<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>Component Extraction for Reusable Patterns
可复用模式抽离为组件
Extract repeated utility patterns into Vue components:
Correct: Reusable card component
vue
<!-- components/BaseCard.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>重复的工具类模式抽离为 Vue 组件:
正确示例:可复用卡片组件
vue
<!-- components/BaseCard.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 Configuration with Design Tokens
结合设计令牌配置 Tailwind
Define design tokens in your Tailwind config for consistency:
Correct: tailwind.config.js with design tokens
javascript
/** @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 配置中定义设计令牌保证一致性:
正确示例:包含设计令牌的 tailwind.config.js
javascript
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
// 语义化颜色令牌
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8'
},
surface: {
light: '#ffffff',
dark: '#1f2937'
}
},
spacing: {
// 自定义间距令牌
'4.5': '1.125rem',
'18': '4.5rem'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif']
},
borderRadius: {
'4xl': '2rem'
}
}
},
plugins: []
}Tailwind CSS v4 Configuration
Tailwind CSS v4 配置
For Tailwind CSS v4, use the CSS-first configuration approach:
Correct: Tailwind v4 CSS configuration
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;
}Tailwind CSS v4 采用 CSS 优先的配置方式:
正确示例:Tailwind v4 CSS 配置
css
/* main.css */
@import "tailwindcss";
@theme {
/* 自定义颜色 */
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
/* 自定义间距 */
--spacing-4-5: 1.125rem;
--spacing-18: 4.5rem;
/* 自定义字体 */
--font-family-sans: 'Inter', system-ui, sans-serif;
}Using cn()
Helper for Conditional Classes
cn()使用 cn()
辅助函数处理条件类名
cn()Use a class merging utility for conditional classes:
Correct: cn() helper with clsx and tailwind-merge
typescript
// utils/cn.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}Usage in component:
vue
<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>使用类合并工具处理条件类名:
正确示例:基于 clsx 和 tailwind-merge 实现的 cn() 辅助函数
typescript
// utils/cn.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}组件中使用:
vue
<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 Best Practices
PrimeVue 最佳实践
PrimeVue is a comprehensive Vue UI component library with 90+ components. Use it natively. Do not fight the framework.
PrimeVue 是一个功能全面的 Vue UI 组件库,包含90+组件。请原生使用,不要对抗框架。
Core Principle
核心原则
PrimeVue v4 has a complete theming system based on design tokens and presets. This is the ONLY supported customization path. Do not bypass it.
PrimeVue v4 拥有基于设计令牌和预设的完整主题系统,这是唯一支持的自定义路径,不要绕过它。
Installation & Setup
安装与配置
Correct: PrimeVue v4 setup with theme preset
typescript
// 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')Correct: Component registration (tree-shakeable)
typescript
// 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 v4 配置
typescript
// 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',
// 保证和 Tailwind 共存无冲突
cssLayer: {
name: 'primevue',
order: 'tailwind-base, primevue, tailwind-utilities'
}
}
}
})
app.mount('#app')正确示例:支持 tree-shaking 的组件注册
typescript
// main.ts - 仅注册你使用的组件
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)Theme Customization with Design Tokens
基于设计令牌的主题自定义
Customize PrimeVue appearance through the design token system, NOT by overriding component internals:
Correct: Custom preset using definePreset()
typescript
// 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 MyPresetUsage in main.ts:
typescript
import MyPreset from './theme/my-preset'
app.use(PrimeVue, {
theme: {
preset: MyPreset
}
})通过设计令牌系统自定义 PrimeVue 外观,不要重写组件内部样式:
正确示例:使用 definePreset() 实现自定义预设
typescript
// 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 MyPresetmain.ts 中使用:
typescript
import MyPreset from './theme/my-preset'
app.use(PrimeVue, {
theme: {
preset: MyPreset
}
})Using Components Natively
原生使用组件
Use PrimeVue components with their built-in props — do not restyle them:
Correct: Use built-in severity and variant props
vue
<template>
<!-- Use severity prop for color variants -->
<Button label="Save" severity="success" />
<Button label="Delete" severity="danger" />
<Button label="Info" severity="info" outlined />
<!-- Use size prop -->
<Button label="Small" size="small" />
<Button label="Large" size="large" />
<!-- Use style variant props -->
<Button label="Text" text />
<Button label="Outlined" outlined />
<Button label="Rounded" rounded />
<Button label="Raised" raised />
<!-- Use icon prop -->
<Button label="Search" icon="pi pi-search" />
<Button icon="pi pi-check" rounded aria-label="Confirm" />
</template>Correct: Use slot API for content customization
vue
<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 组件自带的 props,不要重写样式:
正确示例:使用内置的 severity 和 variant props
vue
<template>
<!-- 使用 severity prop 实现颜色变体 -->
<Button label="保存" severity="success" />
<Button label="删除" severity="danger" />
<Button label="信息" severity="info" outlined />
<!-- 使用 size prop -->
<Button label="小尺寸" size="small" />
<Button label="大尺寸" size="large" />
<!-- 使用样式变体 props -->
<Button label="文字按钮" text />
<Button label="描边按钮" outlined />
<Button label="圆角按钮" rounded />
<Button label="凸起按钮" raised />
<!-- 使用 icon prop -->
<Button label="搜索" icon="pi pi-search" />
<Button icon="pi pi-check" rounded aria-label="确认" />
</template>正确示例:使用 slot API 自定义内容
vue
<template>
<DataTable :value="users" stripedRows paginator :rows="10">
<Column field="name" header="姓名" sortable />
<Column field="status" header="状态">
<template #body="{ data }">
<Tag :value="data.status" :severity="getStatusSeverity(data.status)" />
</template>
</Column>
</DataTable>
</template>What NEVER to Do with PrimeVue
PrimeVue 绝对禁止的操作
NEVER use PassThrough (pt) to restyle components:
vue
<!-- INCORRECT: Fighting the framework -->
<Button
label="Submit"
:pt="{
root: { class: 'bg-blue-600 rounded-lg px-4 py-2' },
label: { class: 'font-medium' }
}"
/>
<!-- CORRECT: Use the component natively -->
<Button label="Submit" />NEVER use unstyled mode to rebuild components:
typescript
// INCORRECT: Stripping the framework and rebuilding it
app.use(PrimeVue, { unstyled: true })
// CORRECT: Use a theme preset
app.use(PrimeVue, { theme: { preset: Aura } })NEVER create wrapper components to override styling:
vue
<!-- INCORRECT: Unnecessary abstraction -->
<!-- components/AppButton.vue -->
<Button v-bind="$attrs" :pt="{ root: { class: customClasses } }">
<slot />
</Button>
<!-- CORRECT: Use Button directly everywhere -->
<Button label="Submit" severity="primary" />NEVER target PrimeVue internal CSS classes:
css
/* INCORRECT: Fragile, breaks on updates */
.p-button-label { font-weight: 700; }
.p-datatable-header { background: #f0f0f0; }
/* CORRECT: Customize through design tokens in definePreset() */永远不要使用 PassThrough (pt) 重写组件样式:
vue
<!-- 错误示例:对抗框架 -->
<Button
label="提交"
:pt="{
root: { class: 'bg-blue-600 rounded-lg px-4 py-2' },
label: { class: 'font-medium' }
}"
/>
<!-- 正确示例:原生使用组件 -->
<Button label="提交" />永远不要使用无样式模式重建组件:
typescript
// 错误示例:剥离框架样式自行重建
app.use(PrimeVue, { unstyled: true })
// 正确示例:使用主题预设
app.use(PrimeVue, { theme: { preset: Aura } })永远不要创建仅用于覆盖样式的包装组件:
vue
<!-- 错误示例:不必要的抽象 -->
<!-- components/AppButton.vue -->
<Button v-bind="$attrs" :pt="{ root: { class: customClasses } }">
<slot />
</Button>
<!-- 正确示例:所有地方直接使用 Button -->
<Button label="提交" severity="primary" />永远不要直接修改 PrimeVue 内部 CSS 类:
css
/* 错误示例:脆弱,版本更新会失效 */
.p-button-label { font-weight: 700; }
.p-datatable-header { background: #f0f0f0; }
/* 正确示例:在 definePreset() 中通过设计令牌自定义 */Tailwind and PrimeVue Coexistence
Tailwind 和 PrimeVue 共存
Tailwind and PrimeVue serve different roles — do not mix their responsibilities:
- PrimeVue owns: component appearance, internal spacing, interactive states, component variants
- Tailwind owns: page layout, spacing between components, custom non-library elements, typography on non-library elements
Correct: Tailwind for layout, PrimeVue components used natively
vue
<template>
<!-- Tailwind handles the page layout -->
<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>
<!-- PrimeVue components used as-is -->
<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>
<!-- Tailwind for spacing between PrimeVue components -->
<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>INCORRECT: Using Tailwind to restyle PrimeVue components
vue
<template>
<!-- WRONG: Tailwind classes overriding PrimeVue button appearance -->
<Button label="Submit" class="rounded-full bg-indigo-600 px-8 py-3 shadow-xl" />
</template>Tailwind 和 PrimeVue 职责不同,不要混淆两者的功能边界:
- PrimeVue 负责:组件外观、内部间距、交互状态、组件变体
- Tailwind 负责:页面布局、组件间间距、自定义非库元素、非库元素的排版
正确示例:Tailwind 处理布局,原生使用 PrimeVue 组件
vue
<template>
<!-- Tailwind 处理页面布局 -->
<div class="mx-auto max-w-4xl space-y-6 p-6">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">用户管理</h1>
<!-- 原生使用 PrimeVue 组件 -->
<DataTable :value="users" stripedRows paginator :rows="10">
<Column field="name" header="姓名" sortable />
<Column field="email" header="邮箱" sortable />
<Column field="role" header="角色" />
</DataTable>
<!-- Tailwind 处理 PrimeVue 组件之间的间距 -->
<div class="flex gap-3">
<Button label="添加用户" icon="pi pi-plus" />
<Button label="导出" icon="pi pi-download" severity="secondary" outlined />
</div>
</div>
</template>错误示例:使用 Tailwind 重写 PrimeVue 组件样式
vue
<template>
<!-- 错误:Tailwind 类覆盖 PrimeVue 按钮外观 -->
<Button label="提交" class="rounded-full bg-indigo-600 px-8 py-3 shadow-xl" />
</template>DataTable Best Practices
DataTable 最佳实践
Correct: Typed DataTable with Composition API
vue
<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>正确示例:结合 Composition API 的带类型 DataTable
vue
<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[]>([])
// 分页
const first = ref(0)
const rows = ref(10)
// 排序
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="姓名" sortable />
<Column field="email" header="邮箱" sortable />
<Column field="role" header="角色" sortable />
<Column field="status" header="状态">
<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>Form Components Pattern
表单组件模式
Correct: Form with validation using PrimeVue
vue
<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>正确示例:使用 PrimeVue 实现带校验的表单
vue
<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: '管理员', value: 'admin' },
{ label: '普通用户', value: 'user' },
{ label: '访客', value: 'guest' }
]
const isValid = computed(() => {
return Object.keys(errors.value).length === 0
})
function validate(): boolean {
errors.value = {}
if (!formData.value.email) {
errors.value.email = '邮箱不能为空'
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.value.email)) {
errors.value.email = '邮箱格式不正确'
}
if (!formData.value.password) {
errors.value.password = '密码不能为空'
} else if (formData.value.password.length < 8) {
errors.value.password = '密码长度不能少于8位'
}
if (!formData.value.role) {
errors.value.role = '请选择角色'
}
return Object.keys(errors.value).length === 0
}
function handleSubmit() {
submitted.value = true
if (validate()) {
// 提交表单
console.log('表单提交数据:', 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">邮箱</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">密码</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">角色</label>
<Dropdown
id="role"
v-model="formData.role"
:options="roles"
optionLabel="label"
optionValue="value"
placeholder="选择角色"
: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="提交" class="w-full" />
</form>
</template>Dialog & Overlay Patterns
弹窗与浮层模式
Correct: Confirmation dialog with composable
typescript
// 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
}
}Usage:
vue
<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>正确示例:基于 composable 的确认弹窗
typescript
// 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: '确认删除',
icon: 'pi pi-exclamation-triangle',
rejectClass: 'p-button-secondary p-button-outlined',
acceptClass: 'p-button-danger',
rejectLabel: '取消',
acceptLabel: '删除',
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
}
}使用方式:
vue
<script setup lang="ts">
import { useConfirmDialog } from '@/composables/useConfirmDialog'
import ConfirmDialog from 'primevue/confirmdialog'
const { confirmDelete } = useConfirmDialog()
function handleDelete(item: Item) {
confirmDelete(
`确认要删除 "${item.name}" 吗?`,
() => deleteItem(item.id)
)
}
</script>
<template>
<ConfirmDialog />
<Button label="删除" severity="danger" @click="handleDelete(item)" />
</template>Toast Notifications
Toast 通知
Correct: Toast service with composable
typescript
// 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 }
}正确示例:基于 composable 的 Toast 服务
typescript
// 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 }
}Accessibility Best Practices
可访问性最佳实践
PrimeVue components are WCAG 2.0 compliant. Ensure proper usage:
Correct: Accessible form fields
vue
<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 组件默认符合 WCAG 2.0 规范,请确保正确使用:
正确示例:可访问的表单字段
vue
<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>Lazy Loading Components
组件懒加载
Correct: Async component loading for large PrimeVue components
typescript
// 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
})正确示例:大型 PrimeVue 组件异步加载
typescript
// 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
})Anti-Patterns to Avoid
需要避免的反模式
Don't Mutate Props
不要直接修改 Props
Incorrect:
vue
<script setup>
const props = defineProps(['items'])
function addItem(item) {
props.items.push(item) // Never mutate props!
}
</script>Correct:
vue
<script setup>
const props = defineProps(['items'])
const emit = defineEmits(['update:items'])
function addItem(item) {
emit('update:items', [...props.items, item])
}
</script>错误示例:
vue
<script setup>
const props = defineProps(['items'])
function addItem(item) {
props.items.push(item) // 永远不要直接修改 props!
}
</script>正确示例:
vue
<script setup>
const props = defineProps(['items'])
const emit = defineEmits(['update:items'])
function addItem(item) {
emit('update:items', [...props.items, item])
}
</script>Don't Use v-if with v-for
不要同时使用 v-if 和 v-for
Incorrect:
vue
<template>
<div v-for="item in items" v-if="item.isActive" :key="item.id">
{{ item.name }}
</div>
</template>Correct:
vue
<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>错误示例:
vue
<template>
<div v-for="item in items" v-if="item.isActive" :key="item.id">
{{ item.name }}
</div>
</template>正确示例:
vue
<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>Don't Store Derived State
不要单独存储派生状态
Incorrect:
vue
<script setup>
const items = ref([])
const itemCount = ref(0) // Derived state stored separately
watch(items, () => {
itemCount.value = items.value.length // Manually syncing
})
</script>Correct:
vue
<script setup>
const items = ref([])
const itemCount = computed(() => items.value.length) // Computed property
</script>错误示例:
vue
<script setup>
const items = ref([])
const itemCount = ref(0) // 派生状态单独存储
watch(items, () => {
itemCount.value = items.value.length // 手动同步
})
</script>正确示例:
vue
<script setup>
const items = ref([])
const itemCount = computed(() => items.value.length) // 使用计算属性
</script>Don't Destructure Reactive Objects
不要直接解构响应式对象
Incorrect:
vue
<script setup>
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state // Loses reactivity!
</script>Correct:
vue
<script setup>
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state) // Preserves reactivity
</script>错误示例:
vue
<script setup>
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state // 丢失响应式!
</script>正确示例:
vue
<script setup>
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state) // 保留响应式
</script>Don't Concatenate Tailwind Class Names
不要拼接 Tailwind 类名
Dynamic class concatenation breaks Tailwind's compiler and classes get purged in production:
Incorrect:
vue
<script setup>
const color = ref('blue')
</script>
<template>
<!-- Classes will be purged in production! -->
<div :class="`bg-${color}-500 text-${color}-900`">
Content
</div>
</template>Correct:
vue
<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>动态拼接类名会破坏 Tailwind 编译器,生产环境类会被清理:
错误示例:
vue
<script setup>
const color = ref('blue')
</script>
<template>
<!-- 生产环境类会被清理! -->
<div :class="`bg-${color}-500 text-${color}-900`">
内容
</div>
</template>正确示例:
vue
<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">
内容
</div>
</template>Don't Overuse @apply
不要过度使用 @apply
Excessive usage defeats the purpose of utility-first CSS:
@applyIncorrect:
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;
}Correct: Use Vue components instead
vue
<!-- components/Card.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>过度使用 违背了 utility-first CSS 的设计初衷:
@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 组件实现
vue
<!-- components/Card.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>Don't Use Conflicting Utilities
不要使用冲突的工具类
Applying multiple utilities that target the same CSS property causes unpredictable results:
Incorrect:
vue
<template>
<!-- Both flex and grid target display property -->
<div class="flex grid">Content</div>
<!-- Multiple margin utilities conflict -->
<div class="m-4 mx-6">Content</div>
</template>Correct:
vue
<template>
<div :class="isGrid ? 'grid' : 'flex'">Content</div>
<!-- Use specific margin utilities -->
<div class="mx-6 my-4">Content</div>
</template>同时应用多个作用于同一个 CSS 属性的工具类会导致不可预测的结果:
错误示例:
vue
<template>
<!-- flex 和 grid 都作用于 display 属性 -->
<div class="flex grid">内容</div>
<!-- 多个 margin 工具类冲突 -->
<div class="m-4 mx-6">内容</div>
</template>正确示例:
vue
<template>
<div :class="isGrid ? 'grid' : 'flex'">内容</div>
<!-- 使用明确的 margin 工具类 -->
<div class="mx-6 my-4">内容</div>
</template>Don't Ignore Accessibility
不要忽略可访问性
Always include proper accessibility attributes alongside visual styling:
Incorrect:
vue
<template>
<button class="rounded bg-blue-600 p-2 text-white">
<IconX />
</button>
</template>Correct:
vue
<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>除了视觉样式,始终添加正确的可访问性属性:
错误示例:
vue
<template>
<button class="rounded bg-blue-600 p-2 text-white">
<IconX />
</button>
</template>正确示例:
vue
<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="关闭弹窗"
>
<IconX aria-hidden="true" />
</button>
</template>Don't Create Overly Long Class Strings
不要创建过长的类名字符串
Break down complex class combinations into logical groups or components:
Incorrect:
vue
<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">
<!-- 15+ utilities on one element -->
</div>
</template>Correct: Extract to component or use computed
vue
<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>将复杂的类名组合拆分为逻辑组或组件:
错误示例:
vue
<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">
<!-- 单个元素上有15+个工具类 -->
</div>
</template>正确示例:抽离为组件或使用计算属性
vue
<script setup>
const containerClasses = [
// 布局
'mx-auto max-w-4xl flex flex-col sm:flex-row',
'items-center justify-between',
'gap-4 sm:gap-6 lg:gap-8',
// 间距
'mt-8 p-6 md:p-8',
// 视觉
'rounded-xl border bg-white shadow-lg',
'border-gray-200 dark:border-gray-700 dark:bg-gray-800',
// 交互
'transition-all duration-300',
'hover:border-blue-500 hover:shadow-xl'
]
</script>
<template>
<div :class="containerClasses">
<slot />
</div>
</template>Don't Override PrimeVue Styles with CSS
不要用 CSS 覆盖 PrimeVue 样式
Using CSS overrides bypasses the design system and causes maintenance issues:
Incorrect:
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;
}Correct: Use design tokens or PassThrough
typescript
// 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' }
}
}
})使用 CSS 覆盖会绕过设计系统,导致维护问题:
错误示例:
css
/* styles.css - 避免这种写法 */
.p-button {
background-color: #3b82f6 !important;
border-radius: 8px !important;
}
.p-datatable .p-datatable-thead > tr > th {
background: #f3f4f6 !important;
}正确示例:使用设计令牌或 PassThrough
typescript
// main.ts - 使用设计令牌
app.use(PrimeVue, {
theme: {
preset: Aura,
options: {
cssLayer: {
name: 'primevue',
order: 'tailwind-base, primevue, tailwind-utilities'
}
}
},
pt: {
button: {
root: { class: 'rounded-lg' }
}
}
})Don't Import Entire PrimeVue Library
不要导入整个 PrimeVue 库
Importing everything bloats bundle size:
Incorrect:
typescript
// 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)
})Correct: Import only what you need
typescript
// 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)导入所有组件会导致包体积过大:
错误示例:
typescript
// main.ts - 不要这么写
import PrimeVue from 'primevue/config'
import * as PrimeVueComponents from 'primevue' // 导入所有组件!
Object.entries(PrimeVueComponents).forEach(([name, component]) => {
app.component(name, component)
})正确示例:仅导入需要的组件
typescript
// main.ts - 支持 tree-shaking 的导入
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)Don't Mix Styled and Unstyled Inconsistently
不要混合使用有样式和无样式模式
Mixing modes creates visual inconsistency:
Incorrect:
typescript
// main.ts
app.use(PrimeVue, {
unstyled: true // Global unstyled
})
// SomeComponent.vue - Using styled component anyway
<Button label="Click" /> // No styles applied, looks brokenCorrect: Choose one approach consistently
typescript
// 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
}
})混合模式会导致视觉不一致:
错误示例:
typescript
// main.ts
app.use(PrimeVue, {
unstyled: true // 全局无样式模式
})
// SomeComponent.vue - 仍然使用有样式组件
<Button label="点击" /> // 没有样式,显示异常正确示例:始终选择同一种模式
typescript
// 方案1:有样式模式 + PT 自定义
app.use(PrimeVue, {
theme: { preset: Aura },
pt: { /* 全局自定义 */ }
})
// 方案2:无样式模式 + 完整 PT 样式
app.use(PrimeVue, {
unstyled: true,
pt: {
button: {
root: { class: 'px-4 py-2 bg-primary-600 text-white rounded-lg' }
}
// ... 所有组件的完整样式配置
}
})Don't Ignore Accessibility Attributes
不要忽略可访问性属性
PrimeVue provides accessibility out of the box, don't disable or ignore it:
Incorrect:
vue
<template>
<!-- Missing aria attributes and label -->
<Button icon="pi pi-trash" @click="deleteItem" />
<!-- No error message association -->
<InputText v-model="email" :class="{ 'p-invalid': hasError }" />
<span class="error">Invalid email</span>
</template>Correct: Maintain accessibility
vue
<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>PrimeVue 原生提供可访问性支持,不要禁用或忽略:
错误示例:
vue
<template>
<!-- 缺少 aria 属性和 label -->
<Button icon="pi pi-trash" @click="deleteItem" />
<!-- 没有关联错误提示 -->
<InputText v-model="email" :class="{ 'p-invalid': hasError }" />
<span class="error">邮箱格式错误</span>
</template>正确示例:保证可访问性
vue
<template>
<Button
icon="pi pi-trash"
aria-label="删除条目"
@click="deleteItem"
/>
<div class="flex flex-col gap-2">
<label for="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">
邮箱格式错误
</small>
</div>
</template>Don't Hardcode PassThrough in Every Component
不要在每个组件中硬编码 PassThrough 配置
Repeating PT configuration across components creates duplication:
Incorrect:
vue
<!-- ComponentA.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />
<!-- ComponentB.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />
<!-- ComponentC.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />Correct: Use global PT or wrapper components
typescript
// main.ts - Global configuration
app.use(PrimeVue, {
pt: {
button: {
root: { class: 'rounded-lg shadow-md' }
}
}
})
// Or use wrapper components (see Wrapper Components Pattern above)跨组件重复 PT 配置会导致代码冗余:
错误示例:
vue
<!-- ComponentA.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />
<!-- ComponentB.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />
<!-- ComponentC.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />正确示例:使用全局 PT 或包装组件
typescript
// main.ts - 全局配置
app.use(PrimeVue, {
pt: {
button: {
root: { class: 'rounded-lg shadow-md' }
}
}
})
// 或者使用包装组件(参考上面的包装组件模式)Nuxt.js Specific Guidelines
Nuxt.js 专属规范
When using Nuxt.js, follow these additional patterns:
- Auto-imports: Leverage Nuxt's auto-imports for Vue APIs and composables
- useFetch/useAsyncData: Use Nuxt's data fetching composables for SSR-compatible data loading
- definePageMeta: Use for page-level metadata and middleware
- Server routes: Use for API endpoints
server/api/ - Runtime config: Use for environment variables
useRuntimeConfig()
使用 Nuxt.js 时,额外遵循以下模式:
- 自动导入:利用 Nuxt 的自动导入特性引入 Vue API 和 composables
- useFetch/useAsyncData:使用 Nuxt 数据获取 composables 实现 SSR 兼容的数据加载
- definePageMeta:用于页面级元数据和中间件配置
- 服务端路由:使用 目录编写 API 接口
server/api/ - 运行时配置:使用 获取环境变量
useRuntimeConfig()