vue-patterns
affaan-m/ECC
为 Vue、Nuxt、Vite 或 Pinia 项目提供 Vue.js 3 组合式 API 模式、组件架构、响应式编程最佳实践、Pinia 状态管理、Vue Router 导航以及 Nuxt 服务器端渲染(SSR)模式。
...展开全部Vue.js 设计模式与最佳实践
使用 Composition API 进行 Vue.js 3 开发的综合指南()的开发,涵盖组件设计、响应式、状态管理、路由、测试以及服务器端渲染(SSR)模式。当与原生 Vue 存在差异时,指南中还包含针对 Nuxt 的具体指导。
何时启用
在以下情况下激活此技能:
- 项目使用 Vue.js(任何版本)、Nuxt、Vite + Vue 或 Pinia。
- 用户询问有关 Vue 组件架构、可组合函数、响应式或状态管理的问题。
- 审查 Vue 单文件组件(
.vue文件)。 - 配置 Vue Router、Pinia 状态存储或 Vite/Vitest 配置时。
- 讨论与 Vue 相关的性能、安全或服务器端渲染(SSR)模式。
1. 项目结构
推荐的布局(功能优先)
src/
├── api/ # API 客户端和端点定义
├── assets/ # 静态资源(图片、字体、图标)
├── components/ # 共享/可复用组件
│ ├── base/ # 基础 UI 原语(Button、Input、Modal)
│ └── features/ # 特定功能的共享组件
├── composables/ # 可复用的 Composition API 逻辑
├── layouts/ # 页面布局(可选)
├── pages/ # 路由级页面组件
├── router/ # Vue Router 配置
├── stores/ # Pinia 数据存储
├── types/ # TypeScript 类型定义
├── utils/ # 纯函数
└── App.vue # 根组件
文件命名规范
| 约定 | 何时使用 |
|---|---|
PascalCase.vue |
所有组件(由vue/multi-word-component-names 强制执行) |
useCamelCase.ts |
可组合函数 |
camelCase.ts |
实用工具、API 客户端、类型 |
kebab-case目录 |
路由片段、功能文件夹 |
2. 组件架构
单文件组件顺序
呈现组件与容器组件
- 容器组件:负责数据获取、状态管理及副作用。渲染表现层组件。
- 展示型组件:接收 props,触发事件。不进行 API 调用,不访问数据存储。仅进行渲染。
props 最佳实践
// 带默认值的基于类型的 props
interface Props {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
items: Item[];
}
const props = withDefaults(defineProps(), {
variant: "primary",
disabled: false,
});
- 始终指定
类型,并在适当情况下提供必填项和默认值。 - 布尔属性:
isXxx、hasXxx、canXxx。 - 切勿直接修改 props —— 应通过触发事件来处理。
- 对于 v-model 绑定,请使用
defineModel()(Vue 3.4+)或modelValue+update:modelValue。
事件
const emit = defineEmits<{
submit: [];
"update:modelValue": [value: string];
select: [id: string, index: number];
}>();
- 在模板中使用连字符命名法(
@update:model-value)。 - 在脚本中使用驼峰命名法(
emit("update:modelValue", val))。
3. 可组合函数(可复用逻辑)
结构
// composables/useDebounce.ts
export function useDebounce(value: MaybeRef, delay: number): Ref {
const debounced = ref(toValue(value)) as Ref;
let timer: ReturnType;
watch(
() => toValue(value),
(newVal) => {
clearTimeout(timer);
timer = setTimeout(() => { debounced.value = newVal; }, delay);
}
);
onUnmounted(() => clearTimeout(timer));
return readonly(debounced);
}
规则
- 必须以
use前缀开头。 - 返回响应式值(
ref、computed、reactive),绝不能返回普通基本类型。 - 通过
MaybeRef/toRef()/toValue()接受响应式输入。 - 在
onUnmounted或监听器的onCleanup中清理副作用。 - 不得产生模块作用域内的副作用。
与 Mixins 的对比
Composables 完全取代了 Vue 2 中的 Mixins:
- Mixins:数据流不透明、权威数据源冲突、命名冲突。
- Composables:显式导入、明确的返回值、可组合且支持树形抖动。
4. 状态管理
何时使用何种方案
| 模式 | 用例 |
|---|---|
ref()/reactive() |
本地组件状态 |
| Props + 事件 | 父子组件通信 |
| 提供 / 注入 | 主题、配置、插件 API |
| Pinia 状态存储 | 全局、共享、复杂状态 |
| 服务器状态可组合 | 带缓存的 API 数据(封装fetch/TanStack Query) |
Pinia 存储设置(推荐)
// stores/useCartStore.ts
export const useCartStore = defineStore("cart", () => {
const items = ref([]);
const isLoading = ref(false);
const totalPrice = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
);
const itemCount = computed(() =>
items.value.reduce((sum, i) => sum + i.quantity, 0)
);
async function addItem(productId: string) {
isLoading.value = true;
try {
const item = await fetchProduct(productId);
const existing = items.value.find(i => i.id === item.id);
if (existing) existing.quantity++;
else items.value.push({ ...item, quantity: 1 });
} finally {
isLoading.value = false;
}
}
return { items, isLoading, totalPrice, itemCount, addItem };
});
- 请使用 Setup Store 语法(而非 Options Store)。
- 建议使用动作(actions)处理业务级变异,并使用
$patch()进行分组更新。 - 每个异步操作:处理加载、成功和错误。
5. Vue Router
路由定义
const routes = [
{
path: "/users/:id",
name: "user-detail",
component: () => import("@/pages/UserDetail.vue"), // 延迟加载
props: true, // 将参数作为 props 传递
meta: { requiresAuth: true },
},
];
导航守护
router.beforeEach((to, from) => {
const { isLoggedIn } = useAuthStore();
if (to.meta.requiresAuth && !isLoggedIn) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
响应式路由参数
当组件保持挂载状态但路由参数发生变化时:
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));
6. 模板模式
模板语法
加载中...
错误:{{ error }}
{{ content }}
切换内容
{{ item.name}}
{{ item.name }}
7. 性能
| 技术 | 何时使用 |
|---|---|
v-memo |
极少变化的列表项 |
v-once |
仅渲染一次且永久静态的内容 |
shallowRef() |
整体替换大型数据结构 |
shallowReactive() |
仅顶级属性具有响应性 |
v-show优先于v-if |
频繁切换可见性 |
|
缓存已切换的视图 |
| 懒加载路由 | () => import(...)用于非关键路由 |
Suspense |
带备用方案的异步组件加载 |
8. 测试
Stack
- Vitest用于单元测试和组件测试
- Vue Test Utils用于组件挂载和交互测试
- @pinia/testing用于模拟存储
- Playwright用于端到端测试
组件测试模式
import { mount } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import UserCard from "./UserCard.vue";
beforeEach(() => { setActivePinia(createPinia()); });
it("渲染并触发事件", async () => {
const wrapper = mount(UserCard, {
props: { user: { id: "1", name: "Alice" } },
});
expect(wrapper.text()).toContain("Alice");
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("select")![0]).toEqual(["1"]);
});
9. Nuxt 特有的模式
自动导入
Nuxt 会自动导入ref、computed、watch、useFetch、useAsyncData 等。直接使用即可,无需显式导入。对于非 Nuxt 项目,请务必显式导入。
useAsyncData / useFetch
const { data: user, pending, error, refresh } = await useAsyncData(
"user", // 缓存的唯一键
() => $fetch(`/api/users/${id}`),
);
const { data: posts } = await useFetch("/api/posts", {
query: { page: 1 },
key: "posts-page-1", // 避免重复请求
});
服务器路由
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, z.object({
id: z.string().uuid(),
}).parse);
// ... 获取数据并返回
});
运行时配置
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// 仅限服务器
apiSecret: "",
// 公共配置(对客户端公开)
public: {
apiBase: "https://api.example.com",
},
},
});
10. Vue 3.5+ 新 API
响应式 props 解构
Vue 3.5 正式支持响应式 props 解构——来自 `defineProps()`的解构变量会自动成为响应式的:
// Vue 3.5+:解构的 props 是响应式的(无需 toRefs)
const { count = 0, msg = "hello" } = defineProps<{
count?: number;
msg?: string;
}>();
// 限制:无法直接监听解构后的 props
watch(() => count, (newVal) => { ... }); // 通过,需要 getter
useTemplateRef()
对于模板引用,请使用useTemplateRef()替换名称匹配的普通 ref:
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef("input");
// "input" 匹配模板中的 ref="input" 属性,而非变量名
支持动态 ref ID:useTemplateRef(dynamicRefId)。
onWatcherCleanup()
可全局导入的监听器清理 API(Vue 3.5 及以上)。必须在监听器回调内部以同步方式调用:
import { watch, onWatcherCleanup } from "vue";
watch(userId, async (newId) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort());
// ... 通过信号进行数据获取
});
useId()
适用于表单元素且支持无障碍访问的 SSR 稳定唯一 ID 生成:
import { useId } from "vue";
const id = useId();
延迟传送
支持传送至在同一渲染周期内渲染的目标:
内容
延迟加载(SSR)
defineAsyncComponent()现支持hydrate策略:
import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
loader: () => import("./Comp.vue"),
hydrate: hydrateOnVisible(),
});
反模式
| 反模式 | 为什么这是错误的 | 解决方案 |
|---|---|---|
对defineProps()进行解构(Vue < 3.5) |
捕获快照,失去响应性 | 通过props.xxx访问或使用toRefs() |
对解构后的 props 调用watch()(Vue 3.5+) |
编译时错误 — 无法直接监听解构后的 props | 使用获取器包装器:watch(() => count, ...) |
在同一元素上同时使用v-if和v-for |
执行顺序不明确 | 使用经过过滤的计算属性数组 |
v-forkey = index |
重新排序时状态失效 | 使用稳定的数据库 ID |
| 修改 props | 违反单向数据流 | 触发事件或使用v-model |
包含用户内容的v-html |
XSS 漏洞 | 使用 DOMPurify 进行数据净化 |
| Vue 3 中的混合函数 | 不透明,易发生冲突 | 用可组合函数替换 |
| 可组合函数中的模块作用域副作用 | 在实例间共享 | onMounted和onUnmounted中的作用域 |
reactive()用于可替换状态 |
替换会破坏响应性 | 请改用ref() |
| 未进行清理的监听器 | 内存泄漏、竞争条件 | 请使用onCleanup或onWatcherCleanup()(Vue 3.5+) |
| 新 Vue 3 代码中的 Options API | 生态系统正向 Composition API 迁移 | 使用 |
| 普通 ref 作为模板引用 | 不支持动态 ref,名称匹配不稳定 | 使用useTemplateRef()(Vue 3.5 及以上版本) |
相关技能
无障碍设计— ARIA、语义化 HTML、焦点管理前端模式— 跨框架前端架构TypeScript— 应用于 Vue 项目的 TypeScript 最佳实践编码规范— 通用代码质量标准
---
name: vue-patterns
description: Provides Vue.js 3 Composition API patterns, component architecture, reactivity best practices, Pinia state management, Vue Router navigation, and Nuxt SSR patterns for Vue, Nuxt, Vite, or Pinia projects.
---
# Vue.js Patterns and Best Practices
Comprehensive guide for Vue.js 3 development using Composition API (`<script setup>`), covering component design, reactivity, state management, routing, testing, and SSR patterns. Nuxt-specific guidance is included where it differs from vanilla Vue.
## When to Activate
Activate this skill when:
- The project uses Vue.js (any version), Nuxt, Vite + Vue, or Pinia.
- The user asks about Vue component architecture, composables, reactivity, or state management.
- Reviewing Vue Single-File Components (`.vue` files).
- Setting up Vue Router, Pinia stores, or Vite/Vitest configuration.
- Discussing Vue-specific performance, security, or SSR patterns.
---
## 1. Project Structure
### Recommended Layout (Feature-First)
```
src/
├── api/ # API client and endpoint definitions
├── assets/ # Static assets (images, fonts, icons)
├── components/ # Shared/reusable components
│ ├── base/ # Base UI primitives (Button, Input, Modal)
│ └── features/ # Feature-specific shared components
├── composables/ # Reusable Composition API logic
├── layouts/ # Page layouts (optional)
├── pages/ # Route-level page components
├── router/ # Vue Router configuration
├── stores/ # Pinia stores
├── types/ # TypeScript type definitions
├── utils/ # Pure utility functions
└── App.vue # Root component
```
### File Naming
| Convention | When to Use |
|-----------|-------------|
| `PascalCase.vue` | All components (enforced by `vue/multi-word-component-names`) |
| `useCamelCase.ts` | Composables |
| `camelCase.ts` | Utilities, API clients, types |
| `kebab-case` directories | Route segments, feature folders |
---
## 2. Component Architecture
### Single-File Component Order
```vue
<script setup lang="ts">
// 1. Imports (vue → ecosystem → absolute → relative)
// 2. Props & Emits & Slots
// 3. Composables
// 4. Local state (ref/reactive)
// 5. Computed properties
// 6. Methods
// 7. Watchers
// 8. Lifecycle hooks
</script>
<template>
<!-- Template content -->
</template>
<style scoped>
/* Scoped styles */
</style>
```
### Presentational vs Container
- **Container components**: Own data fetching, state, and side effects. Render presentational components.
- **Presentational components**: Receive props, emit events. No API calls, no store access. Pure rendering.
### Props Best Practices
```ts
// Type-based props with defaults
interface Props {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
items: Item[];
}
const props = withDefaults(defineProps<Props>(), {
variant: "primary",
disabled: false,
});
```
- Always provide `type`, and `required`/`default` where appropriate.
- Boolean props: `isXxx`, `hasXxx`, `canXxx`.
- Never mutate props — emit events instead.
- For v-model binding, use `defineModel()` (Vue 3.4+) or `modelValue` + `update:modelValue`.
### Events
```ts
const emit = defineEmits<{
submit: [];
"update:modelValue": [value: string];
select: [id: string, index: number];
}>();
```
- Use kebab-case in templates (`@update:model-value`).
- Use camelCase in script (`emit("update:modelValue", val)`).
---
## 3. Composables (Reusable Logic)
### Structure
```ts
// composables/useDebounce.ts
export function useDebounce<T>(value: MaybeRef<T>, delay: number): Ref<T> {
const debounced = ref(toValue(value)) as Ref<T>;
let timer: ReturnType<typeof setTimeout>;
watch(
() => toValue(value),
(newVal) => {
clearTimeout(timer);
timer = setTimeout(() => { debounced.value = newVal; }, delay);
}
);
onUnmounted(() => clearTimeout(timer));
return readonly(debounced);
}
```
### Rules
- Must start with `use` prefix.
- Return reactive values (`ref`, `computed`, `reactive`), never plain primitives.
- Accept reactive inputs via `MaybeRef` / `toRef()` / `toValue()`.
- Clean up side effects in `onUnmounted` or watcher `onCleanup`.
- No module-scope side effects.
### vs Mixins
Composables replace Vue 2 mixins entirely:
- **Mixins**: Opaque data flow, source-of-truth collisions, name conflicts.
- **Composables**: Explicit imports, clear return values, composable and tree-shakable.
---
## 4. State Management
### When to Use What
| Pattern | Use Case |
|---------|----------|
| `ref()` / `reactive()` | Local component state |
| Props + Emits | Parent-child communication |
| Provide / Inject | Theme, config, plugin API |
| Pinia store | Global, shared, complex state |
| Server state composable | API data with caching (wrap `fetch`/TanStack Query) |
### Pinia Setup Store (Preferred)
```ts
// stores/useCartStore.ts
export const useCartStore = defineStore("cart", () => {
const items = ref<CartItem[]>([]);
const isLoading = ref(false);
const totalPrice = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
);
const itemCount = computed(() =>
items.value.reduce((sum, i) => sum + i.quantity, 0)
);
async function addItem(productId: string) {
isLoading.value = true;
try {
const item = await fetchProduct(productId);
const existing = items.value.find(i => i.id === item.id);
if (existing) existing.quantity++;
else items.value.push({ ...item, quantity: 1 });
} finally {
isLoading.value = false;
}
}
return { items, isLoading, totalPrice, itemCount, addItem };
});
```
- Use Setup Store syntax (not Options Store).
- Prefer actions for business-level mutations and `$patch()` for grouped updates.
- Every async action: handle loading + success + error.
---
## 5. Vue Router
### Route Definitions
```ts
const routes = [
{
path: "/users/:id",
name: "user-detail",
component: () => import("@/pages/UserDetail.vue"), // lazy
props: true, // pass params as props
meta: { requiresAuth: true },
},
];
```
### Navigation Guards
```ts
router.beforeEach((to, from) => {
const { isLoggedIn } = useAuthStore();
if (to.meta.requiresAuth && !isLoggedIn) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
```
### Reactive Route Params
When a component stays mounted but route params change:
```ts
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));
```
---
## 6. Template Patterns
### Template Syntax
```vue
<!-- v-if/v-else-if/v-else -->
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>{{ content }}</div>
<!-- v-show for frequent toggles -->
<div v-show="isOpen">Toggled content</div>
<!-- v-for with stable keys -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
<!-- Computed filtered list (not v-if + v-for on same element) -->
<div v-for="item in activeItems" :key="item.id">{{ item.name }}</div>
<!-- Event handling -->
<form @submit.prevent="handleSubmit">
<button type="submit">Save</button>
</form>
<!-- v-model -->
<input v-model="name" />
<CustomInput v-model="value" v-model:title="title" />
```
---
## 7. Performance
| Technique | When to Use |
|-----------|-------------|
| `v-memo` | List items that rarely change |
| `v-once` | Content rendered once and static forever |
| `shallowRef()` | Large data structures replaced wholesale |
| `shallowReactive()` | Only top-level properties are reactive |
| `v-show` over `v-if` | Frequent visibility toggles |
| `<KeepAlive :max="10">` | Cache toggled views |
| Lazy routes | `() => import(...)` for non-critical routes |
| `Suspense` | Async component loading with fallback |
---
## 8. Testing
### Stack
- **Vitest** for unit and component tests
- **Vue Test Utils** for mounting and interaction
- **@pinia/testing** for store mocking
- **Playwright** for E2E
### Component Test Pattern
```ts
import { mount } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import UserCard from "./UserCard.vue";
beforeEach(() => { setActivePinia(createPinia()); });
it("renders and emits", async () => {
const wrapper = mount(UserCard, {
props: { user: { id: "1", name: "Alice" } },
});
expect(wrapper.text()).toContain("Alice");
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("select")![0]).toEqual(["1"]);
});
```
---
## 9. Nuxt-Specific Patterns
### Auto-Imports
Nuxt auto-imports `ref`, `computed`, `watch`, `useFetch`, `useAsyncData`, etc. Use them directly without importing. For non-Nuxt projects, always import explicitly.
### useAsyncData / useFetch
```ts
const { data: user, pending, error, refresh } = await useAsyncData(
"user", // unique key for caching
() => $fetch(`/api/users/${id}`),
);
const { data: posts } = await useFetch("/api/posts", {
query: { page: 1 },
key: "posts-page-1", // dedupes requests
});
```
### Server Routes
```ts
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, z.object({
id: z.string().uuid(),
}).parse);
// ... fetch and return
});
```
### Runtime Config
```ts
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// server-only
apiSecret: "",
// public (exposed to client)
public: {
apiBase: "https://api.example.com",
},
},
});
```
---
## 10. Vue 3.5+ New APIs
### Reactive Props Destructure
Vue 3.5 stabilized reactive props destructure — destructured variables from `defineProps()` are automatically reactive:
```ts
// Vue 3.5+: destructured props are reactive (no need for toRefs)
const { count = 0, msg = "hello" } = defineProps<{
count?: number;
msg?: string;
}>();
// Limitation: cannot watch destructured prop directly
watch(() => count, (newVal) => { ... }); // PASS getter required
```
### `useTemplateRef()`
Replace name-matched plain refs with `useTemplateRef()` for template references:
```ts
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef<HTMLInputElement>("input");
// "input" matches the ref="input" attribute in template, not the variable name
```
Supports dynamic ref IDs: `useTemplateRef(dynamicRefId)`.
### `onWatcherCleanup()`
Globally importable watcher cleanup API (Vue 3.5+). It must be called synchronously inside the watcher callback:
```ts
import { watch, onWatcherCleanup } from "vue";
watch(userId, async (newId) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort());
// ... fetch with signal
});
```
### `useId()`
SSR-stable unique ID generation for form elements and accessibility:
```ts
import { useId } from "vue";
const id = useId();
```
### `defer` Teleport
`<Teleport defer>` allows teleporting to targets rendered in the same cycle:
```vue
<Teleport defer to="#container">Content</Teleport>
<div id="container"></div>
```
### Lazy Hydration (SSR)
`defineAsyncComponent()` now supports `hydrate` strategy:
```ts
import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
loader: () => import("./Comp.vue"),
hydrate: hydrateOnVisible(),
});
```
---
## Anti-Patterns
| Anti-Pattern | Why It's Wrong | The Fix |
|-------------|---------------|---------|
| Destructuring `defineProps()` (Vue < 3.5) | Captures snapshot, loses reactivity | Access via `props.xxx` or use `toRefs()` |
| `watch()` on destructured prop (Vue 3.5+) | Compile-time error — destructured props can't be watched directly | Use getter wrapper: `watch(() => count, ...)` |
| `v-if` + `v-for` on same element | Ambiguous execution order | Use computed filtered array |
| `v-for` key = index | Broken state on reorder | Use stable database IDs |
| Mutating props | Violates one-way data flow | Emit events or use `v-model` |
| `v-html` with user content | XSS vulnerability | Sanitize with DOMPurify |
| Mixins in Vue 3 | Opaque, collision-prone | Replace with composables |
| Module-scope side effects in composable | Shared across instances | Scope in `onMounted` + `onUnmounted` |
| `reactive()` for replaceable state | Replacement breaks reactivity | Use `ref()` instead |
| Watcher without cleanup | Memory leaks, race conditions | Use `onCleanup` or `onWatcherCleanup()` (Vue 3.5+) |
| Options API in new Vue 3 code | Ecosystem move to Composition API | Use `<script setup>` |
| Plain ref for template references | No dynamic ref support, name-matching fragile | Use `useTemplateRef()` (Vue 3.5+) |
## Related Skills
- `accessibility` — ARIA, semantic HTML, focus management
- `frontend-patterns` — Cross-framework frontend architecture
- `typescript` — TypeScript best practices applied to Vue projects
- `coding-standards` — General code quality standards
所有文件
1 个文件安装 vue-patterns
下载技能文件并将其解压到 .claude/skills/ 目录中。
下载ZIP克隆仓库并复制技能文件到您的项目中。
git clone https://github.com/affaan-m/ECC/tree/main/skills/vue-patterns # Copy SKILL.md to your .claude/skills/ directory
复制





首页
