🍎Vue官方Skills深度解读:那些被悄悄藏起来的宝藏

Vue官方Skills是一套被严重低估的最佳实践指南。本文深度解读vue-best-practicesSkill,涵盖从响应式核心、组件模式到性能优化的22个实用技巧,每个技巧都配有清晰的正确/错误对比。这些技巧不是"推荐做法",而是Vue团队多年沉淀下来的正确做事方式。


引言:为什么这套Skills值得你花时间

Vue的文档已经很完善了,但文档告诉你的是"怎么用"。而这套Skills告诉你的是"怎么用对"。

举几个例子:

  • shallowRef vs ref,什么时候该用哪个?
  • v-if vs v-show,真的只是"条件渲染vs显示隐藏"这么简单?
  • 为什么你写的watch(useAttrs()...)从来不触发?

这套Skills的独特之处在于:它不是教你概念,而是直接告诉你错误模式和正确模式。每个知识点都有BAD/GOOD对比,看完就能用。

本文基于vue-best-practicesSkill的所有参考资料编写,涵盖22个最佳实践,覆盖组件、响应式、性能、动画等维度。

原skill

md 复制代码
---
name: vue-best-practices
description: MUST be used for Vue.js tasks. Strongly recommends Composition API with `<script setup>` and TypeScript as the standard approach. Covers Vue 3, SSR, Volar, vue-tsc. Load for any Vue, .vue files, Vue Router, Pinia, or Vite with Vue work. ALWAYS use Composition API unless the project explicitly requires Options API.
license: MIT
metadata:
  author: github.com/vuejs-ai
  version: "18.0.0"
---

# Vue 最佳实践工作流

使用此 Skill 作为指令集。除非用户明确要求不同顺序,否则请按顺序遵循此工作流。

## 核心原则

- **保持状态可预测:** 一个真实数据源,派生其他一切。
- **让数据流显式化:** Props 向下传递,Events 向上传递,适用于大多数场景。
- **倾向于小而聚焦的组件:** 更容易测试、复用和维护。
- **避免不必要的重渲染:** 明智地使用 computed 属性和 watchers。
- **可读性很重要:** 编写清晰、自文档化的代码。

## 1) 编码前先确认架构(必须)

- 默认技术栈:Vue 3 + Composition API + `<script setup lang="ts">`。
- 如果项目明确使用 Options API,在有相关 Skill 的情况下加载 `vue-options-api-best-practices` Skill。
- 如果项目明确使用 JSX,在有相关 Skill 的情况下加载 `vue-jsx-best-practices` Skill。

### 1.1 必须阅读的核心参考资料(必须)

- 在实现任何 Vue 任务之前,请务必阅读并应用这些核心参考资料:
  - `references/reactivity.md`
  - `references/sfc.md`
  - `references/component-data-flow.md`
  - `references/composables.md`
- 在整个任务期间保持这些参考资料处于活跃的工作上下文中,而不仅仅是在出现特定问题时才查阅。

### 1.2 编码前规划组件边界(必须)

对于任何非平凡的功能,在实现前创建简要的组件地图。

- 用一句话定义每个组件的单一职责。
- 将入口/根和路由级视图组件默认作为组合层。
- 将功能 UI 和功能逻辑从入口/根/视图组件中移出,除非任务本身就是一个有意为之的小型单文件演示。
- 在组件地图中定义每个子组件的 props/emits 契约。
- 当添加超过一个组件时,倾向于按功能文件夹布局(`components/<feature>/...`、`composables/use<Feature>.ts`)。

## 2) 应用必要的 Vue 基础知识(必须)

这些是必要的、必须掌握的基础知识。使用在第 `1.1` 节中已加载的核心参考资料,将它们应用到每个 Vue 任务中。

### 响应式

- 来自 `1.1` 的必读参考:[reactivity](references/reactivity.md)
- 保持源状态最小化(`ref`/`reactive`),用 `computed` 派生所有可派生值。
- 需要时用 watchers 处理副作用。
- 避免在模板中重新计算昂贵的逻辑。

### SFC 结构和模板安全

- 来自 `1.1` 的必读参考:[sfc](references/sfc.md)
- 保持 SFC 各部分按此顺序排列:`<script>` → `<template>` → `<style>`。
- 保持 SFC 职责聚焦;拆分大型组件。
- 保持模板声明式;将分支/派生逻辑移到 script 中。
- 应用 Vue 模板安全规则(`v-html`、列表渲染、条件渲染选择)。

### 保持组件职责聚焦

当组件有 **超过一个明确职责** 时进行拆分(例如:数据编排 + UI,或多个独立的 UI 区域)。

- 倾向于 **更小的组件 + composables**,而不是一个"超大组件"
- 将 **UI 区域** 移入子组件(props 入,events 出)。
- 将 **状态/副作用** 移入 composables(`useXxx()`)。

应用客观的拆分触发条件。当 **任意** 条件为真时拆分组件:

- 它同时拥有编排/状态和多个区域的大量展示性标记。
- 它有 3+ 个不同的 UI 区域(例如:表单、过滤器、列表、底部/状态)。
- 模板块被重复或可能变得可复用(条目行、卡片、列表条目)。

入口/根和路由视图规则:

- 保持入口/根和路由视图组件精简:应用 shell/布局、提供者连接、功能组合。
- 当功能包含独立部分时,不要将完整功能实现放在入口/根/视图组件中。
- 对于 CRUD/列表功能(待办、表格、目录、收件箱),至少拆分到:
  - 功能容器组件
  - 输入/表单组件
  - 列表(和/或条目)组件
  - 底部/操作或过滤器/状态组件
- 只允许非常小的临时演示使用单文件实现;如果选择了单文件,明确说明为什么不需要拆分。

### 组件数据流

- 来自 `1.1` 的必读参考:[component-data-flow](references/component-data-flow.md)
- 使用 props 向下、events 向上的模型作为主要模式。
- 只对真正的双向组件契约使用 `v-model`。
- 只对深层树依赖或共享上下文使用 provide/inject。
- 用 `defineProps`、`defineEmits` 和 `InjectionKey` 保持契约显式且带类型。

### Composables

- 来自 `1.1` 的必读参考:[composables](references/composables.md)
- 当逻辑被复用、是状态化的或副作用较重时,提取到 composables 中。
- 保持 composable API 小巧、带类型且可预测。
- 将功能逻辑与展示组件分离。

## 3) 只有在需求明确时才考虑可选功能

### 3.1 标准可选功能

不要默认添加这些。只在需求存在时加载匹配的参考。

- Slots:父组件需要控制子组件内容/布局时 -> [component-slots](references/component-slots.md)
- Fallthrough attributes:包装器/基础组件必须安全转发 attrs/events 时 -> [component-fallthrough-attrs](references/component-fallthrough-attrs.md)
- 内置组件 `<KeepAlive>` 用于有状态视图缓存 -> [component-keep-alive](references/component-keep-alive.md)
- 内置组件 `<Teleport>` 用于浮层/传送门 -> [component-teleport](references/component-teleport.md)
- 内置组件 `<Suspense>` 用于异步子树回退边界 -> [component-suspense](references/component-suspense.md)
- 动画相关功能:选择与所需运动行为最匹配的简单方法。
  - 内置组件 `<Transition>` 用于入场/离场效果 -> [transition](references/component-transition.md)
  - 内置组件 `<TransitionGroup>` 用于列表变动的动画 -> [transition-group](references/component-transition-group.md)
  - 基于类的动画用于非入场/离场效果 -> [animation-class-based-technique](references/animation-class-based-technique.md)
  - 状态驱动动画用于用户输入驱动的动画 -> [animation-state-driven-technique](references/animation-state-driven-technique.md)

### 3.2 较少使用的可选功能

只在有明确的产品或技术需求时才使用这些。

- 指令:当行为是 DOM 特定的,且不适合用 composable/组件实现时 -> [directives](references/directives.md)
- 异步组件:重型/很少使用的 UI 应该懒加载 -> [component-async](references/component-async.md)
- 渲染函数:只在模板无法表达需求时使用 -> [render-functions](references/render-functions.md)
- 插件:当行为必须在应用范围内安装时 -> [plugins](references/plugins.md)
- 状态管理模式:跨功能边界的应用级共享状态 -> [state-management](references/state-management.md)

## 4) 在行为正确后运行性能优化

性能工作是功能完成后的处理。在核心行为实现并验证之前不要优化。

- 大列表渲染瓶颈 -> [perf-virtualize-large-lists](references/perf-virtualize-large-lists.md)
- 静态子树不必要地重渲染 -> [perf-v-once-v-memo-directives](references/perf-v-once-v-memo-directives.md)
- 热路径中过度抽象 -> [perf-avoid-component-abstraction-in-lists](references/perf-avoid-component-abstraction-in-lists.md)
- 昂贵的更新被触发过于频繁 -> [updated-hook-performance](references/updated-hook-performance.md)

## 5) 完成前的最终自检

- 核心行为正常工作且符合需求。
- 所有必读参考资料都已阅读并应用。
- 响应式模型是最小化且可预测的。
- SFC 结构和模板规则得到遵循。
- 组件职责聚焦且拆分合理,必要时进行了拆分。
- 入口/根和路由视图组件保持作为组合层,除非有明确的小演示例外。
- 组件拆分决策是明确的且可辩护的(职责边界清晰)。
- 数据流契约是显式的且带类型的。
- 在复用/复杂度合理的地方使用了 composables。
- 适用的地方将状态/副作用移入了 composables。
- 只在需求明确时才使用可选功能。
- 性能更改只在功能完成后才应用。

第一部分:响应式核心(Reactivity Core)

github.com/vuejs-ai/sk...

1. shallowRef vs ref:不是选哪个的问题

很多人以为这是性能优化选项,其实不是。这是一个关于"更新语义"的选择

typescript 复制代码
// 场景:你经常替换整个值(重新获取、重置等)→ 用 ref
const user = ref(null)
user.value = await fetchUser() // 触发更新,整个user被替换

// 场景:你只替换顶层,但内部属性会变(不可变数据风格)→ 用 shallowRef
const user = shallowRef(null)
user.value = { name: 'Alice', age: 30 }
user.value.age = 31 // ❌ 不触发更新
user.value = { ...user.value, age: 31 } // ✅ 触发更新

// 场景:外部库实例、class实例 → 用 shallowRef
const canvas = shallowRef(new Canvas())
typescript 复制代码
// 什么时候用 reactive
// 场景:表单、单个状态对象,经常就地修改属性
const form = reactive({
  username: '',
  password: '',
  remember: false
})
form.username = 'alice' // 方便
// ❌ 避免:form = reactive({...}) 整个替换

核心原则ref()适合经常重新赋值的场景,reactive()适合就地修改的场景,shallowRef()适合你不希望Vue深层代理的对象。

2. computed的五个正确用法

① 永远不要在computed里写副作用

typescript 复制代码
// ❌ BAD
const doubled = computed(() => {
  if (count.value > 10) console.warn('太大了!')
  return count.value * 2
})

// ✅ GOOD - 用watch处理副作用
const doubled = computed(() => count.value * 2)
watch(count, (val) => {
  if (val > 10) console.warn('太大了!')
})

② 过滤/排序不要写在模板里

vue 复制代码
<!-- ❌ BAD - 每次渲染都重新计算 -->
<li v-for="item in items.filter(item => item.active).sort(...)">
  {{ item.name }}
</li>

<!-- ❌ BAD - 调用函数也是同样的问题 -->
<li v-for="item in getSortedItems()">

<!-- ✅ GOOD -->
<script setup>
const visibleItems = computed(() =>
  items.value
    .filter(item => item.active)
    .sort((a, b) => a.name.localeCompare(b.name))
)
</script>
<template>
  <li v-for="item in visibleItems">
</template>

③ 用computed处理动态class/style

vue 复制代码
<!-- ❌ BAD -->
<button :class="{ 'btn-primary': type === 'primary' && !disabled, 'btn-disabled': disabled }">

<!-- ✅ GOOD -->
<script setup>
const buttonClasses = computed(() => ({
  btn: true,
  [`btn-${props.type}`]: !props.disabled,
  'btn-disabled': props.disabled
}))
</script>
<template>
  <button :class="buttonClasses">
</template>

3. watch的正确姿势

① 用immediate: true替代重复的onMounted调用

typescript 复制代码
// ❌ BAD
const userId = ref(1)
onMounted(() => loadUser(userId.value))
watch(userId, (id) => loadUser(id))

// ✅ GOOD
watch(userId, (id) => loadUser(id), { immediate: true })

② 异步清理是搜索框的救星

typescript 复制代码
// 当用户快速输入时,取消上一个请求
const query = ref('')
const results = ref<string[]>([])

watch(query, async (q, _prev, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort()) // 关键:自动取消

  const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
    signal: controller.signal,
  })
  results.value = await res.json()
})

4. reactive的正确打开方式

禁止直接解构

typescript 复制代码
// ❌ BAD - 丢失响应式
const { count } = reactive({ count: 0 })

// ✅ GOOD - 用toRefs保持响应式
const state = reactive({ count: 0 })
const { count } = toRefs(state)
watch(count, ...) // 现在能watch了

watch的正确姿势

typescript 复制代码
// ❌ BAD - 传了非getter值
watch(state.count, () => {...})

// ✅ GOOD - 用getter
watch(() => state.count, () => {...})
// 或
watch(count, () => {...}) // 通过toRefs解构出来的ref

第二部分:组件模式(Component Patterns)

github.com/vuejs-ai/sk...

5. Slots的五个正确做法

① 用简写语法 # 替代 v-slot:

vue 复制代码
<!-- ❌ BAD -->
<MyComponent>
  <template v-slot:header>...</template>
</MyComponent>

<!-- ✅ GOOD -->
<MyComponent>
  <template #header>...</template>
</MyComponent>

② 可选插槽要加v-if检查

vue 复制代码
<!-- ❌ BAD - 没有header时依然渲染空的header标签 -->
<template>
  <article class="card">
    <header class="card-header">
      <slot name="header" />
    </header>
  </article>
</template>

<!-- ✅ GOOD -->
<template>
  <article class="card">
    <header v-if="$slots.header" class="card-header">
      <slot name="header" />
    </header>
  </article>
</template>

③ TypeScript项目用defineSlots定义插槽类型

vue 复制代码
<script setup lang="ts">
defineProps<{ products: Product[] }>()

defineSlots<{
  default(props: { product: Product; index: number }): any
  empty(): any
}>()
</script>

④ 给插槽提供兜底内容

vue 复制代码
<!-- ❌ BAD -->
<button type="submit" class="btn-primary">
  <slot />
</button>

<!-- ✅ GOOD -->
<button type="submit" class="btn-primary">
  <slot>Submit</slot>
</button>

⑤ 纯逻辑复用优先用Composables

vue 复制代码
<!-- ❌ BAD - 用renderless组件做纯逻辑复用 -->
<MouseTracker v-slot="{ x, y }">
  <p>{{ x }}, {{ y }}</p>
</MouseTracker>

<!-- ✅ GOOD -->
<script setup>
const { x, y } = useMouse()
</script>
<template>
  <p>{{ x }}, {{ y }}</p>
</template>

6. 组件数据流的正确理解

Props是单向的,永远不要在子组件里修改props

vue 复制代码
<!-- ❌ BAD -->
<script setup>
const props = defineProps({ count: Number })
function increment() {
  props.count++ // ❌ 禁止
}
</script>

<!-- ✅ GOOD - 正确的做法 -->
<script setup>
const props = defineProps({ count: Number })
const emit = defineEmits(['update'])
function increment() {
  emit('update', props.count + 1)
}
</script>

defineModel简化v-model(Vue 3.4+)

vue 复制代码
<!-- ❌ BAD - Vue 3.4之前的写法 -->
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
</script>
<template>
  <input :value="props.modelValue" @input="emit('update:modelValue', $event.target.value)" />
</template>

<!-- ✅ GOOD - Vue 3.4+ -->
<script setup>
const model = defineModel({ type: String })
</script>
<template>
  <input v-model="model" />
</template>

Provide/Inject用Symbol Keys避免冲突

typescript 复制代码
// ❌ BAD - 字符串key可能冲突
provide('theme', theme)

// ✅ GOOD - Symbol唯一性 + TypeScript类型支持
import type { InjectionKey } from 'vue'
export const themeKey: InjectionKey<Theme> = Symbol('theme')
provide(themeKey, theme)

7. Fallthrough Attributes的正确访问方式

属性名要用方括号访问

vue 复制代码
<script setup>
const attrs = useAttrs()

// ❌ 这些都访问不到
attrs.data-testid    // 语法错误
attrs.dataTestid     // undefined
attrs['on-click']    // undefined
attrs['@click']      // undefined

// ✅ 正确的访问方式
attrs['data-testid']
attrs['aria-label']
attrs.onClick        // 事件监听器用onX
</script>

useAttrs()不是响应式的

vue 复制代码
<script setup>
// ❌ BAD - 这个watch永远不会触发
watch(() => attrs.class, (newVal) => {
  console.log(newVal) // Never runs
})

// ✅ GOOD - 用onUpdated
onUpdated(() => {
  console.log('Latest attrs:', attrs)
})

// ✅ 或者:promote到props
const props = defineProps({ class: String })
watch(() => props.class, (newVal) => {...})
</script>

第三部分:异步与缓存

github.com/vuejs-ai/sk...

8. 异步组件的懒加载策略

在SSR中使用延迟水合

vue 复制代码
<script setup>
import {
  defineAsyncComponent,
  hydrateOnVisible,   // 进入视口时水合
  hydrateOnIdle       // 空闲时水合
} from 'vue'

// 评论组件:用户滚动到视口才水合
const AsyncComments = defineAsyncComponent({
  loader: () => import('./Comments.vue'),
  hydrate: hydrateOnVisible({ rootMargin: '100px' })
})

// 页脚:5ms空闲时水合
const AsyncFooter = defineAsyncComponent({
  loader: () => import('./Footer.vue'),
  hydrate: hydrateOnIdle(5000)
})
</script>

防止加载闪烁

vue 复制代码
<script setup>
// ❌ BAD - delay:0 会在网络快时闪一下
const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./Dashboard.vue'),
  loadingComponent: LoadingSpinner,
  delay: 0  // 太短了
})

// ✅ GOOD - delay:200 足够短不显眼,足够长不闪动
const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./Dashboard.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,      // 默认值,通常最合适
  timeout: 30000
})
</script>

9. KeepAlive的正确使用

不是所有缓存都是好的

vue 复制代码
<!-- ✅ 应该用KeepAlive的场景: -->
<!-- 标签页切换 -->
<KeepAlive>
  <component :is="currentTab" />
</KeepAlive>

<!-- ❌ 不该用KeepAlive的场景: -->
<!-- 搜索页 - 用户期望每次新鲜结果 -->
<!-- 地图/大表格 - 内存消耗大 -->
<!-- 敏感流程 - 退出时必须清理 -->

必须设置max限制缓存大小

vue 复制代码
<!-- ❌ BAD - 无限缓存 -->
<KeepAlive>
  <component :is="currentView" />
</KeepAlive>

<!-- ✅ GOOD - 最多缓存5个 -->
<KeepAlive :max="5">
  <component :is="currentView" />
</KeepAlive>

缓存失效的正确方式

vue 复制代码
<script setup>
const currentView = ref('Dashboard')
const viewKeys = reactive({ Dashboard: 0, Settings: 0 })

function invalidateCache(view) {
  viewKeys[view]++
}
</script>

<template>
  <KeepAlive>
    <component :is="currentView" :key="`${currentView}-${viewKeys[currentView]}`" />
  </KeepAlive>
</template>

10. Suspense的嵌套与触发机制

必须给default和fallback都包一个根节点

vue 复制代码
<!-- ❌ BAD -->
<Suspense>
  <AsyncHeader />
  <AsyncList />
  <template #fallback>
    <LoadingSpinner />
    <LoadingHint />
  </template>
</Suspense>

<!-- ✅ GOOD -->
<Suspense>
  <div> <!-- 必须包一层 -->
    <AsyncHeader />
    <AsyncList />
  </div>
  <template #fallback>
    <div>
      <LoadingSpinner />
      <LoadingHint />
    </div>
  </template>
</Suspense>

Pending状态只在根节点变化时触发

vue 复制代码
<!-- ❌ BAD - 异步工作发生在深层,但Suspense只跟踪根节点 -->
<Suspense>
  <TabContainer>
    <AsyncDashboard v-if="tab === 'dashboard'" />
    <AsyncSettings v-else />
  </TabContainer>
</Suspense>

<!-- ✅ GOOD - 根节点是动态的 -->
<Suspense>
  <component :is="tabs[tab]" :key="tab" />
</Suspense>

组件嵌套顺序

vue 复制代码
<!-- ✅ 正确的嵌套顺序:RouterView -> Transition -> KeepAlive -> Suspense -->
<RouterView v-slot="{ Component }">
  <Transition mode="out-in">
    <KeepAlive>
      <Suspense>
        <component :is="Component" />
        <template #fallback>Loading...</template>
      </Suspense>
    </KeepAlive>
  </Transition>
</RouterView>

第四部分:性能优化

github.com/vuejs-ai/sk...

11. v-oncev-memo:跳过不必要的更新

v-once:静态内容只渲染一次

vue 复制代码
<template>
  <!-- ❌ BAD - 每次渲染都检查 -->
  <div class="terms-content">
    <h1>Terms of Service</h1>
    <p>Version: {{ termsVersion }}</p>
  </div>

  <!-- ✅ GOOD - 渲染一次后跳过 -->
  <div class="terms-content" v-once>
    <h1>Terms of Service</h1>
    <p>Version: {{ termsVersion }}</p>
  </div>
</template>

v-memo:列表项选择性更新

vue 复制代码
<template>
  <!-- ❌ BAD - selectedId变化时所有1000项都重渲染 -->
  <div v-for="item in list" :key="item.id">
    <div :class="{ selected: item.id === selectedId }">
      <ExpensiveComponent :data="item" />
    </div>
  </div>

  <!-- ✅ GOOD - 只有选中项变化的那两项重渲染 -->
  <div
    v-for="item in list"
    :key="item.id"
    v-memo="[item.id === selectedId]"
  >
    <div :class="{ selected: item.id === selectedId }">
      <ExpensiveComponent :data="item" />
    </div>
  </div>
</template>

12. 大列表虚拟化:50+项就开始考虑

vue 复制代码
<!-- ❌ BAD - 渲染10000项 = 10000个DOM节点 -->
<template>
  <div class="user-list">
    <UserCard v-for="user in users" :key="user.id" :user="user" />
  </div>
</template>

<!-- ✅ GOOD - 只渲染可见的~20项 -->
<template>
  <RecycleScroller class="user-list" :items="users" :item-size="80" key-field="id" v-slot="{ item }">
    <UserCard :user="item" />
  </RecycleScroller>
</template>

<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
</script>

<style scoped>
.user-list {
  height: 600px; /* 必须有固定高度 */
}
</style>

推荐库对比

适用场景
vue-virtual-scroller 通用场景,最流行
@tanstack/vue-virtual 复杂布局,headless设计
vue-virtual-scroll-grid 网格布局

13. 避免列表中的过度组件抽象

vue 复制代码
<!-- ❌ BAD - 每项UserCard创建5个组件实例 -->
<!-- UserCard.vue -->
<template>
  <Card>           <!-- 组件#1 -->
    <CardHeader>   <!-- 组件#2 -->
      <UserAvatar /> <!-- 组件#3 -->
    </CardHeader>
    <CardBody>     <!-- 组件#4 -->
      <Text>{{ user.name }}</Text>
    </CardBody>
  </Card>
</template>

<!-- 100个用户 = 500个组件实例 -->

<!-- ✅ GOOD - 扁平化结构 -->
<template>
  <div class="user-card">
    <div class="card-header">
      <img :src="user.avatar" :alt="user.name" class="avatar" />
    </div>
    <div class="card-body">
      <span class="user-name">{{ user.name }}</span>
    </div>
  </div>
</template>

<!-- 100个用户 = 100个组件实例 -->

14. updated钩子里的禁忌

vue 复制代码
<script setup>
// ❌ BAD - updated里调用API,每次渲染都触发
onUpdated(() => {
  fetch('/api/sync', { method: 'POST', body: JSON.stringify(items.value) })
})

// ❌ BAD - 在updated里修改状态 = 无限循环
onUpdated(() => {
  renderCount.value++ // 触发更新 → 再次调用onUpdated → 无限循环!
})

// ✅ GOOD - 用watch精确控制
watch(items, (newItems) => {
  syncToServer(newItems)
}, { deep: true })

// ✅ GOOD - 只用于DOM同步
onUpdated(() => {
  if (scrollContainer.value) {
    scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
  }
})
</script>

第五部分:样式与动画

github.com/vuejs-ai/sk...

15. SFC样式规范

始终用scoped样式

vue 复制代码
<!-- ❌ BAD -->
<style>
button { border-radius: 999px; } /* 全局污染 */
</style>

<!-- ✅ GOOD -->
<style scoped>
.btn-primary { border-radius: 999px; }
</style>

用class选择器而非元素选择器

vue 复制代码
<!-- ❌ BAD - 元素选择器性能差,且样式脆弱 -->
<style scoped>
article { max-width: 800px; }
h1 { font-size: 2rem; }
</style>

<!-- ✅ GOOD -->
<style scoped>
.article { max-width: 800px; }
.article-title { font-size: 2rem; }
</style>

v-if vs v-show的选择标准

vue 复制代码
<template>
  <!-- 频繁切换 → 用v-show,保持DOM只切换display -->
  <ComplexPanel v-show="isPanelOpen" />

  <!-- 很少显示/初始成本高 → 用v-if,初始渲染时才创建 -->
  <AdminPanel v-if="isAdmin" />
</template>

16. Transition组件的正确用法

只包裹单个元素

vue 复制代码
<!-- ❌ BAD -->
<Transition name="fade">
  <h3>Title</h3>
  <p>Description</p>
</Transition>

<!-- ✅ GOOD -->
<Transition name="fade">
  <div>
    <h3>Title</h3>
    <p>Description</p>
  </div>
</Transition>

同元素类型切换要加key

vue 复制代码
<!-- ❌ BAD - 相同<p>标签,Vue复用元素,不触发动画 -->
<Transition name="fade">
  <p v-if="isActive">Active</p>
  <p v-else>Inactive</p>
</Transition>

<!-- ✅ GOOD -->
<Transition name="fade" mode="out-in">
  <p v-if="isActive" key="active">Active</p>
  <p v-else key="inactive">Inactive</p>
</Transition>

只使用transform和opacity做动画

vue 复制代码
<style>
/* ❌ BAD - 触发重排重绘,性能差 */
.slide-enter-active,
.slide-leave-active {
  transition: height 0.3s ease;
}

/* ✅ GOOD - GPU加速,只触发重绘 */
.slide-enter-active,
.slide-leave-active {
  transition: transform 0.3s ease, opacity 0.3s ease;
}
</style>

17. TransitionGroup的列表动画

必须用稳定唯一key

vue 复制代码
<!-- ❌ BAD - 用index做key会导致动画错位 -->
<TransitionGroup name="list" tag="ul">
  <li v-for="(item, index) in items" :key="index">
</TransitionGroup>

<!-- ✅ GOOD -->
<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item.id">
</TransitionGroup>

交错动画

vue 复制代码
<template>
  <TransitionGroup
    tag="ul"
    :css="false"
    @before-enter="onBeforeEnter"
    @enter="onEnter"
  >
    <li v-for="(item, index) in items" :key="item.id" :data-index="index">
      {{ item.name }}
    </li>
  </TransitionGroup>
</template>

<script setup>
function onBeforeEnter(el) {
  el.style.opacity = 0
  el.style.transform = 'translateY(12px)'
}

function onEnter(el, done) {
  const delay = Number(el.dataset.index) * 80 // 每项延迟80ms
  setTimeout(() => {
    el.style.transition = 'all 0.25s ease'
    el.style.opacity = 1
    el.style.transform = 'translateY(0)'
    setTimeout(done, 250)
  }, delay)
}
</script>

18. 状态驱动的CSS动画

鼠标跟随

vue 复制代码
<template>
  <div class="container" @mousemove="onMousemove">
    <div class="follower" :style="{ transform: `translate(${x}px, ${y}px)` }" />
  </div>
</template>

<script setup>
const x = ref(0)
const y = ref(0)

function onMousemove(e) {
  const rect = e.currentTarget.getBoundingClientRect()
  x.value = e.clientX - rect.left
  y.value = e.clientY - rect.top
}
</script>

<style>
.follower {
  transition: transform 0.1s ease-out; /* 平滑跟随 */
  pointer-events: none;
}
</style>

19. 基于类的反馈动画

shake、pulse等效果

vue 复制代码
<template>
  <div :class="{ shake: showError }">
    <button @click="submitForm">Submit</button>
    <span v-if="showError">Error occurred!</span>
  </div>
</template>

<script setup>
const showError = ref(false)

function submitForm() {
  if (!isValid()) {
    showError.value = true
    setTimeout(() => showError.value = false, 820) // 匹配动画时长
  }
}
</script>

<style>
.shake {
  animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
  transform: translate3d(0, 0, 0); /* 启用GPU加速 */
}
</style>

用animationend自动清理

vue 复制代码
<script setup>
const isAnimating = ref(false)
function triggerAnimation() {
  isAnimating.value = true
  // 动画结束时自动重置,不需要setTimeout
}
</script>

<template>
  <div :class="{ animate: isAnimating }" @animationend="isAnimating = false">
    Content
  </div>
</template>

第六部分:工具与扩展

github.com/vuejs-ai/sk...

20. 指令(Directives)的正确姿势

只用于DOM访问

typescript 复制代码
// ❌ BAD - 复杂行为应该用组件或composable
const vShowPassword = {
  mounted(el) {
    el.addEventListener('click', () => {
      el.type = el.type === 'password' ? 'text' : 'password'
    })
  }
}

// ✅ GOOD - 函数简写用于单钩子
const vFocus = (el) => el.focus()

// ✅ GOOD - 完整对象用于多钩子+清理
const vResize = {
  mounted(el) {
    const observer = new ResizeObserver(() => {})
    observer.observe(el)
    el._observer = observer
  },
  unmounted(el) {
    el._observer?.disconnect() // 必须清理
  }
}

TypeScript类型增强

typescript 复制代码
import type { Directive } from 'vue'

type HighlightValue = string

export const vHighlight = {
  mounted(el, binding) {
    el.style.backgroundColor = binding.value
  }
} satisfies Directive<HTMLElement, HighlightValue>

declare module 'vue' {
  interface ComponentCustomProperties {
    vHighlight: typeof vHighlight
  }
}

SSR要实现getSSRProps

typescript 复制代码
const vTooltip = {
  mounted(el, binding) {
    el.setAttribute('data-tooltip', binding.value)
    el.classList.add('has-tooltip')
  },
  getSSRProps(binding) { // 必须实现,避免水合不匹配
    return {
      'data-tooltip': binding.value,
      class: 'has-tooltip'
    }
  }
}

21. 插件(Plugins)的正确写法

符合app.use()契约

typescript 复制代码
import type { App, Plugin } from 'vue'

interface MyOptions {
  apiKey: string
  debug?: boolean
}

// ✅ GOOD - 对象形式
const myPlugin: Plugin<[MyOptions]> = {
  install(app: App, options: MyOptions) {
    app.provide(serviceKey, createService(options))
  }
}

// ✅ GOOD - 函数形式
function simplePlugin(app: App, options?: { message: string }) {
  app.config.globalProperties.$greet = () => options?.message ?? 'Hello!'
}

app.use(myPlugin, { apiKey: 'xxx', debug: true })

用Symbol Keys防止冲突

typescript 复制代码
import type { InjectionKey } from 'vue'
import type { AxiosInstance } from 'axios'

export const httpKey: InjectionKey<AxiosInstance> = Symbol('http')
export const configKey: InjectionKey<AppConfig> = Symbol('appConfig')

// 提供注入helper,缺失时抛出明确错误
export function useAuth(): AuthService {
  const auth = inject(authKey)
  if (!auth) {
    throw new Error('Auth plugin not installed. Did you forget app.use(authPlugin)?')
  }
  return auth
}

22. 渲染函数(Render Functions)的必要模式

优先用模板,只在必要时用渲染函数

vue 复制代码
<!-- ❌ BAD - 简单场景用渲染函数 -->
<script setup>
import { h, ref } from 'vue'
const count = ref(0)
const render = () => h('div', `Count: ${count.value}`)
</script>

<!-- ✅ GOOD - 用模板 -->
<script setup>
const count = ref(0)
</script>
<template>
  <div>Count: {{ count }}</div>
</template>

列表必须有key

javascript 复制代码
// ❌ BAD
return () => h('ul',
  items.value.map(item => h('li', item.name))
)

// ✅ GOOD
return () => h('ul',
  items.value.map(item => h('li', { key: item.id }, item.name))
)

事件修饰符要用withModifiers

javascript 复制代码
import { h, withModifiers, withKeys } from 'vue'

// ❌ BAD
const handleClick = (e) => {
  e.stopPropagation()
  e.preventDefault()
}

// ✅ GOOD
h('button', {
  onClick: withModifiers(handleClick, ['stop', 'prevent'])
}, 'Click')

总结:这些技巧的共同主题

通读这22个最佳实践,你会发现几个贯穿始终的主题:

1. 响应式要精确

  • 什么时候用ref/reactive/shallowRef不是随意的,是根据更新模式决定的
  • watch要精准触发,computed要纯净无副作用

2. 组件边界要清晰

  • Props down, events up是铁律
  • Slots是API设计,不是实现技巧
  • Provide/Inject用Symbol避免魔法字符串

3. 性能要测量后优化

  • v-once/v-memo是给真正需要优化的地方用的
  • 大列表虚拟化有明确阈值(50+项)
  • 组件抽象不是越少越好,也不是越多越好

4. SSR有额外的坑

  • 异步组件要考虑水合策略
  • 指令需要getSSRProps
  • 状态管理不能用运行时单例

5. 动画要选对工具

  • 入离场 → Transition
  • 列表 → TransitionGroup
  • 停留在DOM里的反馈 → class绑定

这套Skills的真正价值在于:它把Vue团队的踩坑经验整理成了可以直接照着做的模式。下次你写Vue代码时,可以对照检查清单看看是否用对了这些模式。


延伸阅读

相关推荐
小小前端仔LC2 小时前
第五篇:前端任务状态管理与实时反馈 (SSE 客户端篇)
前端
LIO2 小时前
Axios Token 无感刷新机制:原理、实现与最佳实践
前端·axios
「已注销」2 小时前
面试分享:二本靠7轮面试成功拿下大厂P6
前端·javascript·面试
Lee川2 小时前
深入浅出:用 React 打造高性能懒加载无限滚动组件
前端·react.js
walking9572 小时前
重新学习前端之JavaScript
前端·vue.js·面试
walking9572 小时前
重新学习前端之HTML
前端·vue.js·面试
walking9572 小时前
重新学习前端之Vue
前端·vue.js·面试
牛奶2 小时前
开发者的"奇技淫巧":那些让你效率翻倍的实战技巧
前端·后端·程序员
咸鱼翻身更入味2 小时前
Vue创建一个简单的Agent聊天——工具调用
前端