AgentSkillsCN

vue3-best-practices

Vue 3 开发最佳实践指南,涵盖 Composition API、Script Setup、Pinia、TypeScript 集成及性能优化。

SKILL.md
--- frontmatter
name: vue3-best-practices
description: Vue 3 开发最佳实践指南,涵盖 Composition API, Script Setup, Pinia, TypeScript 集成及性能优化。

Vue 3 Best Practices

🌟 技能核心

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

核心原则

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

📁 推荐项目结构

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

命名规范

类型规范示例
组件PascalCaseUserProfile.vue
ComposablescamelCase + use 前缀useAuth.ts
StorescamelCase + Store 后缀userStore.ts
工具函数camelCaseformatDate.ts

🧠 核心原则

1. Script Setup 与 Composition API

vue
<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避免不必要的深度响应
typescript
// ✅ 推荐
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 定义(带默认值)

typescript
// 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 定义

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

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

v-model(Vue 3.4+)

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

Slots 类型化

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

Expose

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

4. 组件命名 (defineOptions)

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

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

何时需要命名

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

5. 属性透传 (inheritAttrs)

vue
<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+)

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

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

🧩 逻辑复用 (Composables)

基本模式

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

typescript
// 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(推荐)

typescript
// 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条件渲染组件模态框、抽屉
typescript
// 路由懒加载
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 APIComposition API
data()ref() / reactive()
computed: {}computed()
methods: {}普通函数
watch: {}watch() / watchEffect()
created<script setup> 顶层代码
mountedonMounted()
this.xxx直接访问变量

🐛 常见错误排查

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

📂 示例文件

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

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

🎨 常用指令示例

bash
# 生成 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 分析这个列表组件的性能问题,建议优化方案。