vue3-best-practices

SKILL.md

Vue 3 Best Practices

🌟 技能核心

本技能指导开发者编写 模块化、类型安全、高性能 的 Vue 3 应用。

核心原则

  • Composition API First
  • 逻辑复用 (Composables)
  • 类型推导优先
  • 单一数据流

📁 推荐项目结构

src/
├── assets/              # 静态资源
├── components/          # 通用组件
│   ├── ui/              # 基础 UI 组件
│   └── business/        # 业务组件
├── composables/         # 组合式函数 (use*.ts)
├── stores/              # Pinia stores
├── views/               # 页面组件
├── router/              # 路由配置
├── types/               # TypeScript 类型定义
├── utils/               # 工具函数
├── api/                 # API 请求封装
└── App.vue

命名规范

类型 规范 示例
组件 PascalCase UserProfile.vue
Composables camelCase + use 前缀 useAuth.ts
Stores camelCase + Store 后缀 userStore.ts
工具函数 camelCase formatDate.ts

🧠 核心原则

1. Script Setup 与 Composition API

<script setup lang="ts">
// ✅ 推荐:显式导入,利于代码阅读和依赖追踪
import { ref, computed, watch, onMounted } from 'vue'
import { useUserStore } from '@/stores/userStore'

// 顶层 await 支持
const data = await fetchInitialData()

// 响应式状态
const count = ref(0)
const doubled = computed(() => count.value * 2)

// Store 使用
const userStore = useUserStore()
</script>

要点

  • 默认使用 <script setup lang="ts">,更简洁,运行时性能更好
  • 支持顶层 await
  • 显式导入 ref, computed, watch 等(而非依赖自动导入)

2. 响应式数据 (Reactivity)

场景 推荐 原因
基本类型 ref 清晰的 .value 访问
对象/数组(默认) reactive 更直观;解构需 toRefs
需要整体替换/可空对象 ref 便于赋新对象与类型约束
深层嵌套大对象 reactive 仅当不解构时使用
大型外部实例 shallowRef 避免不必要的深度响应
// ✅ 推荐
const user = ref<User | null>(null)
user.value = { name: 'John' }

// ⚠️ 谨慎使用 reactive
const state = reactive({ items: [] })
// 解构会丢失响应性!
const { items } = state // ❌ items 不再是响应式

// ✅ 使用 toRefs 解构
const { items } = toRefs(state)

3. 组件通信

Props 定义(带默认值)

// Vue 3.5+ 推荐写法
const { title, count = 0 } = defineProps<{
  title: string
  count?: number
}>()

// Vue 3.4 及以下
const props = withDefaults(defineProps<{
  title: string
  count?: number
}>(), {
  count: 0
})

注意:解构式 props 需要 Vue 3.5+(或编译选项 propsDestructure: true)。否则解构结果非响应式,建议使用 withDefaults 或保留 props.xxx 访问。

Emits 定义

const emit = defineEmits<{
  change: [id: number]
  update: [value: string]
}>()

// 使用
emit('change', 123)

v-model(Vue 3.4+)

// 简化双向绑定
const modelValue = defineModel<string>()
const count = defineModel<number>('count', { default: 0 })

Slots 类型化

defineSlots<{
  default: (props: { item: Item }) => any
  header: () => any
}>()

Expose

// 暴露给父组件的方法/属性
defineExpose({
  focus: () => inputRef.value?.focus(),
  reset
})

4. 组件命名 (defineOptions)

递归组件、调试、DevTools 中必须显式命名:

defineOptions({
  name: 'TreeNode',      // 递归组件必须
  inheritAttrs: false    // 禁用属性自动透传
})

何时需要命名

场景 必要性
递归组件 ⭐ 必须
DevTools 调试 推荐
KeepAlive include/exclude 必须
Transition 组件 推荐

5. 属性透传 (inheritAttrs)

<script setup lang="ts">
defineOptions({ inheritAttrs: false })

// 获取透传的属性
const attrs = useAttrs()
</script>

<template>
  <!-- 手动绑定到内部元素 -->
  <div class="wrapper">
    <input v-bind="attrs" />
  </div>
</template>

6. 泛型组件(Vue 3.3+)

<script setup lang="ts" generic="T extends { id: number }">
defineProps<{
  items: T[]
  selected?: T
}>()

const emit = defineEmits<{
  select: [item: T]
}>()
</script>

🧩 逻辑复用 (Composables)

基本模式

// composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initial = 0) {
  const count = ref(initial)
  const doubled = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  function reset() {
    count.value = initial
  }

  return {
    count,
    doubled,
    increment,
    reset
  }
}

带异步请求的 Composable

// composables/useFetch.ts
import { ref, shallowRef, watchEffect, toValue, type MaybeRefOrGetter } from 'vue'

export function useFetch<T>(url: MaybeRefOrGetter<string>) {
  const data = shallowRef<T | null>(null)
  const error = shallowRef<Error | null>(null)
  const loading = ref(false)

  async function execute() {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(toValue(url))
      data.value = await res.json()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  watchEffect(() => {
    execute()
  })

  return { data, error, loading, refresh: execute }
}

注意MaybeRefOrGetter/toValue 需要 Vue 3.3+。低版本可用 unref 或改为仅接收 Ref

最佳实践

  • ✅ 以 use 开头命名
  • ✅ 返回对象包含响应式状态和方法
  • ✅ 优先使用 VueUse 已有工具
  • ❌ 不要在 Composable 中使用 this

📦 状态管理 (Pinia)

Setup Store(推荐)

// stores/userStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref<User | null>(null)
  const token = ref('')

  // Getters
  const isLoggedIn = computed(() => !!token.value)
  const displayName = computed(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(credentials: LoginDTO) {
    const res = await api.login(credentials)
    user.value = res.user
    token.value = res.token
  }

  function logout() {
    user.value = null
    token.value = ''
  }

  return {
    user,
    token,
    isLoggedIn,
    displayName,
    login,
    logout
  }
})

要点

  • 优先使用 Setup Store,与组件写法一致
  • State 保持扁平化
  • Getters = computed
  • Actions 处理同步/异步逻辑

🚫 反模式对照表

❌ 错误做法 ✅ 正确做法
使用 Mixins 使用 Composables
const { prop } = props 解构 props.proptoRefs(props)
在 setup 中写 created 逻辑 直接写在 setup 顶层
忘记 .value 始终在 script 中使用 .value
reactive 后解构 使用 reftoRefs
Options API 混用 统一使用 Composition API

⚡ 性能优化

技术 场景 示例
v-memo 大型列表/表格 v-memo="[item.id, item.selected]"
shallowRef 大型外部实例 地图、图表实例
KeepAlive 缓存组件 标签页切换
路由懒加载 所有路由 () => import('./Page.vue')
defineAsyncComponent 条件渲染组件 模态框、抽屉
// 路由懒加载
const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue')
  }
]

// 异步组件
const HeavyModal = defineAsyncComponent(() => 
  import('./HeavyModal.vue')
)

🛠️ 技术栈推荐

分类 推荐
构建工具 Vite
路由 Vue Router 4
状态管理 Pinia
UI 组件库 Element Plus / Naive UI / Ant Design Vue
样式方案 UnoCSS / Tailwind CSS
测试 Vitest + Vue Test Utils
工具库 VueUse

🔄 迁移指南:Options → Composition

Options API Composition API
data() ref() / reactive()
computed: {} computed()
methods: {} 普通函数
watch: {} watch() / watchEffect()
created <script setup> 顶层代码
mounted onMounted()
this.xxx 直接访问变量

🐛 常见错误排查

问题 原因 解决
数据不更新 忘记 .value 检查 ref 访问
解构后不响应 reactive 解构 使用 toRefs()
computed 不执行 未访问 .value 确保访问响应式依赖
watch 不触发 监听了原始值 使用 getter 函数
Props 类型错误 缺少类型定义 添加泛型类型

📂 示例文件

本技能包含以下完整示例,位于 examples/ 目录:

文件 说明
component-example.vue 递归树形组件,展示 defineOptions 命名、插槽透传
composable-example.ts usePagination 分页逻辑封装
store-example.ts Pinia Setup Store 完整示例

🎨 常用指令示例

# 生成 Composable
/vue-coder 提取这段逻辑为一个名为 usePagination 的 Composable 函数。

# 转换 Options API
/vue-coder 将这个 Options API 组件重构为 <script setup lang="ts"> 写法。

# 优化响应式
/vue-coder 检查这段代码中 reactive 的使用是否合理,建议改为 ref。

# 添加类型
/vue-coder 为这个组件的 props 和 emits 添加完整的 TypeScript 类型。

# 性能优化
/vue-coder 分析这个列表组件的性能问题,建议优化方案。
Weekly Installs
6
First Seen
Jan 24, 2026
Installed on
antigravity6
claude-code6
opencode6
qoder5
gemini-cli5
codex4