home / skills / long36708 / longmo-skills / vue3-best-practices

vue3-best-practices skill

/skills/vue3-best-practices

This skill helps you adopt modular, type-safe Vue 3 practices with composition API, Pinia, and performance tips for scalable apps.

npx playbooks add skill long36708/longmo-skills --skill vue3-best-practices

Review the files below or copy the command above to add this skill to your agents.

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

# 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

```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](https://vueuse.org/) 已有工具
- ❌ 不要在 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.prop` 或 `toRefs(props)` |
| 在 setup 中写 `created` 逻辑 | 直接写在 setup 顶层 |
| 忘记 `.value` | 始终在 script 中使用 `.value` |
| `reactive` 后解构 | 使用 `ref` 或 `toRefs` |
| 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 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](./examples/component-example.vue) | 递归树形组件,展示 defineOptions 命名、插槽透传 |
| [store-example.ts](./examples/store-example.ts) | Pinia 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 分析这个列表组件的性能问题,建议优化方案。
```

Overview

This skill is a practical Vue 3 best-practices guide focused on Composition API, <script setup>, Pinia, TypeScript integration, and performance optimization. It distills patterns for modular, type-safe, and high-performance Vue applications. The guidance is implementation-oriented with examples for composables, stores, reactivity, and routing.

How this skill works

It inspects and documents concrete patterns: project structure, naming conventions, reactive choices (ref/reactive/shallowRef), and component APIs (defineProps, defineEmits, defineExpose). It codifies composable design, Pinia setup stores, and migration steps from Options API, plus targeted performance techniques like v-memo, lazy routes, and shallowRef for heavy instances. Examples and small code snippets illustrate recommended usage and anti-patterns to avoid.

When to use it

  • Starting a new Vue 3 project to establish structure and conventions
  • Converting Options API components to Composition API and <script setup>
  • Designing reusable composables and typed component APIs with TypeScript
  • Implementing Pinia stores and consistent state patterns across the app
  • Diagnosing reactivity issues or improving frontend performance

Best practices

  • Prefer <script setup lang="ts"> with explicit imports and top-level await support
  • Name composables with use prefix and return an object of reactive state and methods
  • Use ref for primitives and nullable objects; reactive for non-destructured objects; use toRefs when destructuring
  • Favor Setup-style Pinia stores (defineStore with setup) and keep state flat with computed getters
  • Type props, emits, slots and components (generics supported) to ensure compile-time safety
  • Apply performance patterns: route lazy loading, defineAsyncComponent, v-memo, KeepAlive, and shallowRef for heavy external instances

Example use cases

  • Create a composable useFetch that accepts a ref or getter and returns data, error, loading, refresh
  • Refactor an Options API page into <script setup lang="ts"> with typed props/emits and composables
  • Implement a Pinia user store with token, user state, computed getters and async actions
  • Optimize a large list component using v-memo and route/lazy-load heavy child components
  • Build a generic list component using TypeScript generics to type items and select events

FAQ

When should I use ref vs reactive?

Use ref for primitives, nullable objects, or when you need to replace the whole value; use reactive for objects/arrays you will not destructure. If you destructure reactive, use toRefs to preserve reactivity.

Do I need to name components with defineOptions?

Name components when they are recursive, used with KeepAlive include/exclude, or when you want clearer DevTools debugging. For recursive components naming is required.