vue-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVue.js Patterns and Best Practices
Vue.js 模式与最佳实践
Comprehensive guide for Vue.js 3 development using Composition API (), covering component design, reactivity, state management, routing, testing, and SSR patterns. Nuxt-specific guidance is included where it differs from vanilla Vue.
<script setup>这是一份使用 Composition API()进行 Vue.js 3 开发的综合指南,涵盖组件设计、响应式、状态管理、路由、测试以及 SSR 模式。针对 Nuxt 与原生 Vue 不同的地方,也提供了专属指导。
<script setup>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 (files).
.vue - Setting up Vue Router, Pinia stores, or Vite/Vitest configuration.
- Discussing Vue-specific performance, security, or SSR patterns.
在以下场景启用该技能:
- 项目使用 Vue.js(任意版本)、Nuxt、Vite + Vue 或 Pinia。
- 用户询问 Vue 组件架构、组合式函数、响应式或状态管理相关问题。
- 评审 Vue 单文件组件(文件)。
.vue - 配置 Vue Router、Pinia 状态仓库或 Vite/Vitest 环境。
- 讨论 Vue 专属的性能、安全或 SSR 模式。
1. Project Structure
1. 项目结构
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 componentsrc/
├── 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 # 根组件File Naming
文件命名规范
| Convention | When to Use |
|---|---|
| All components (enforced by |
| Composables |
| Utilities, API clients, types |
| Route segments, feature folders |
| 命名规则 | 使用场景 |
|---|---|
| 所有组件(由 |
| 组合式函数 |
| 工具函数、API 客户端、类型定义 |
| 路由片段、功能文件夹 |
2. Component Architecture
2. 组件架构
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>vue
<script setup lang="ts">
// 1. 导入(vue → 生态库 → 绝对路径 → 相对路径)
// 2. Props & Emits & Slots
// 3. 组合式函数
// 4. 本地状态(ref/reactive)
// 5. 计算属性
// 6. 方法
// 7. 监听器
// 8. 生命周期钩子
</script>
<template>
<!-- 模板内容 -->
</template>
<style scoped>
/* 作用域样式 */
</style>Presentational vs Container
展示型组件 vs 容器型组件
- 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,触发事件。不调用 API,不访问状态仓库,仅负责渲染。
Props Best Practices
Props 最佳实践
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 , and
type/requiredwhere appropriate.default - Boolean props: ,
isXxx,hasXxx.canXxx - Never mutate props — emit events instead.
- For v-model binding, use (Vue 3.4+) or
defineModel()+modelValue.update:modelValue
ts
// 基于类型的 Props 与默认值
interface Props {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
items: Item[];
}
const props = withDefaults(defineProps<Props>(), {
variant: "primary",
disabled: false,
});- 始终提供 ,并在合适时设置
type/required。default - 布尔类型 props 使用 、
isXxx、hasXxx命名。canXxx - 绝不要直接修改 props —— 应触发事件通知父组件。
- 双向绑定使用 (Vue 3.4+)或
defineModel()+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)
ts
const emit = defineEmits<{
submit: [];
"update:modelValue": [value: string];
select: [id: string, index: number];
}>();- 模板中使用短横线命名()。
@update:model-value - 脚本中使用驼峰命名()。
emit("update:modelValue", val)
3. Composables (Reusable Logic)
3. 组合式函数(可复用逻辑)
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);
}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 prefix.
use - Return reactive values (,
ref,computed), never plain primitives.reactive - Accept reactive inputs via /
MaybeRef/toRef().toValue() - Clean up side effects in or watcher
onUnmounted.onCleanup - No module-scope side effects.
- 必须以 前缀开头。
use - 返回响应式值(、
ref、computed),绝不能返回原始类型值。reactive - 通过 /
MaybeRef/toRef()接收响应式输入。toValue() - 在 或监听器的
onUnmounted中清理副作用。onCleanup - 禁止在模块级产生副作用。
vs Mixins
对比 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.
组合式函数完全替代 Vue 2 的 Mixins:
- Mixins:数据流不透明、数据源冲突、命名冲突。
- 组合式函数:显式导入、返回值清晰、可组合且支持 tree-shaking。
4. State Management
4. 状态管理
When to Use What
方案选择指南
| Pattern | Use Case |
|---|---|
| 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 |
| 模式 | 使用场景 |
|---|---|
| 组件本地状态 |
| Props + Emits | 父子组件通信 |
| Provide / Inject | 主题、配置、插件 API |
| Pinia 状态仓库 | 全局共享的复杂状态 |
| 服务端状态组合式函数 | 带缓存的 API 数据(封装 |
Pinia Setup Store (Preferred)
Pinia Setup Store(推荐写法)
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 for grouped updates.
$patch() - Every async action: handle loading + success + error.
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 };
});- 使用 Setup Store 语法(而非 Options Store)。
- 优先使用 actions 处理业务级修改,使用 处理批量更新。
$patch() - 所有异步 action 都要处理加载、成功、失败状态。
5. Vue Router
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 },
},
];ts
const routes = [
{
path: "/users/:id",
name: "user-detail",
component: () => import("@/pages/UserDetail.vue"), // 懒加载
props: true, // 将路由参数作为 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 } };
}
});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));当组件保持挂载但路由参数变化时:
ts
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));6. Template Patterns
6. 模板模式
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" />vue
<!-- v-if/v-else-if/v-else -->
<div v-if="isLoading">加载中...</div>
<div v-else-if="error">错误:{{ error }}</div>
<div v-else>{{ content }}</div>
<!-- 频繁切换使用 v-show -->
<div v-show="isOpen">可切换内容</div>
<!-- v-for 使用稳定 key -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
<!-- 使用计算属性过滤列表(避免同一元素上同时使用 v-if + v-for) -->
<div v-for="item in activeItems" :key="item.id">{{ item.name }}</div>
<!-- 事件处理 -->
<form @submit.prevent="handleSubmit">
<button type="submit">保存</button>
</form>
<!-- v-model 双向绑定 -->
<input v-model="name" />
<CustomInput v-model="value" v-model:title="title" />7. Performance
7. 性能优化
| Technique | When to Use |
|---|---|
| List items that rarely change |
| Content rendered once and static forever |
| Large data structures replaced wholesale |
| Only top-level properties are reactive |
| Frequent visibility toggles |
| Cache toggled views |
| Lazy routes | |
| Async component loading with fallback |
| 技巧 | 使用场景 |
|---|---|
| 极少变化的列表项 |
| 仅渲染一次且永久静态的内容 |
| 整体替换的大型数据结构 |
| 仅需顶层属性响应式的对象 |
优先使用 | 频繁切换可见性的元素 |
| 缓存切换的视图 |
| 路由懒加载 | 非核心路由使用 |
| 异步组件加载时显示 fallback 内容 |
8. Testing
8. 测试方案
Stack
技术栈
- Vitest for unit and component tests
- Vue Test Utils for mounting and interaction
- @pinia/testing for store mocking
- Playwright for E2E
- Vitest:单元测试与组件测试
- Vue Test Utils:组件挂载与交互测试
- @pinia/testing:状态仓库 mocking
- Playwright:端到端测试
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"]);
});ts
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-Specific Patterns
9. Nuxt 专属模式
Auto-Imports
自动导入
Nuxt auto-imports , , , , , etc. Use them directly without importing. For non-Nuxt projects, always import explicitly.
refcomputedwatchuseFetchuseAsyncDataNuxt 会自动导入 、、、、 等 API,可直接使用无需手动导入。非 Nuxt 项目需显式导入。
refcomputedwatchuseFetchuseAsyncDatauseAsyncData / useFetch
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
});ts
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 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
});ts
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, z.object({
id: z.string().uuid(),
}).parse);
// ... 获取数据并返回
});Runtime Config
运行时配置
ts
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// server-only
apiSecret: "",
// public (exposed to client)
public: {
apiBase: "https://api.example.com",
},
},
});ts
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// 仅服务端可见
apiSecret: "",
// 客户端可见(暴露给前端)
public: {
apiBase: "https://api.example.com",
},
},
});10. Vue 3.5+ New APIs
10. Vue 3.5+ 新 API
Reactive Props Destructure
响应式 Props 解构
Vue 3.5 stabilized reactive props destructure — destructured variables from are automatically reactive:
defineProps()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 requiredVue 3.5 稳定了响应式 Props 解构功能——从 解构的变量会自动保持响应式:
defineProps()ts
// Vue 3.5+:解构后的 props 保持响应式(无需 toRefs)
const { count = 0, msg = "hello" } = defineProps<{
count?: number;
msg?: string;
}>();
// 限制:无法直接监听解构后的 prop
watch(() => count, (newVal) => { ... }); // 必须使用 getter 包装useTemplateRef()
useTemplateRef()useTemplateRef()
useTemplateRef()Replace name-matched plain refs with for template references:
useTemplateRef()ts
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef<HTMLInputElement>("input");
// "input" matches the ref="input" attribute in template, not the variable nameSupports dynamic ref IDs: .
useTemplateRef(dynamicRefId)使用 替代名称匹配的普通 ref 来获取模板引用:
useTemplateRef()ts
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef<HTMLInputElement>("input");
// "input" 与模板中的 ref="input" 属性匹配,而非变量名支持动态 ref ID:。
useTemplateRef(dynamicRefId)onWatcherCleanup()
onWatcherCleanup()onWatcherCleanup()
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
});全局可导入的监听器清理 API(Vue 3.5+),必须在监听器回调中同步调用:
ts
import { watch, onWatcherCleanup } from "vue";
watch(userId, async (newId) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort());
// ... 使用 signal 发起请求
});useId()
useId()useId()
useId()SSR-stable unique ID generation for form elements and accessibility:
ts
import { useId } from "vue";
const id = useId();SSR 稳定的唯一 ID 生成工具,适用于表单元素与无障碍场景:
ts
import { useId } from "vue";
const id = useId();defer
Teleport
deferdefer
Teleport
defer<Teleport defer>vue
<Teleport defer to="#container">Content</Teleport>
<div id="container"></div><Teleport defer>vue
<Teleport defer to="#container">内容</Teleport>
<div id="container"></div>Lazy Hydration (SSR)
懒水化(SSR)
defineAsyncComponent()hydratets
import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
loader: () => import("./Comp.vue"),
hydrate: hydrateOnVisible(),
});defineAsyncComponent()hydratets
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 | Captures snapshot, loses reactivity | Access via |
| Compile-time error — destructured props can't be watched directly | Use getter wrapper: |
| Ambiguous execution order | Use computed filtered array |
| Broken state on reorder | Use stable database IDs |
| Mutating props | Violates one-way data flow | Emit events or use |
| 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 |
| Replacement breaks reactivity | Use |
| Watcher without cleanup | Memory leaks, race conditions | Use |
| Options API in new Vue 3 code | Ecosystem move to Composition API | Use |
| Plain ref for template references | No dynamic ref support, name-matching fragile | Use |
| 反模式 | 问题所在 | 修复方案 |
|---|---|---|
Vue < 3.5 中解构 | 仅捕获快照,丢失响应式 | 通过 |
| Vue 3.5+ 中直接监听解构后的 prop | 编译错误——解构后的 prop 无法直接监听 | 使用 getter 包装: |
同一元素上同时使用 | 执行顺序模糊 | 使用计算属性过滤数组 |
| 重排时状态异常 | 使用稳定的数据库 ID |
| 修改 props | 违背单向数据流 | 触发事件或使用 |
对用户内容使用 | XSS 漏洞 | 使用 DOMPurify 进行 sanitize |
| Vue 3 中使用 Mixins | 不透明、易冲突 | 替换为组合式函数 |
| 组合式函数中存在模块级副作用 | 实例间共享副作用 | 限制在 |
对可替换状态使用 | 替换后丢失响应式 | 使用 |
| 监听器未清理 | 内存泄漏、竞态条件 | 使用 |
| 新项目中使用 Options API | 生态已转向 Composition API | 使用 |
| 模板引用使用普通 ref | 不支持动态 ref、名称匹配脆弱 | 使用 |
Related Skills
相关技能
- — ARIA, semantic HTML, focus management
accessibility - — Cross-framework frontend architecture
frontend-patterns - — TypeScript best practices applied to Vue projects
typescript - — General code quality standards
coding-standards
- —— ARIA、语义化 HTML、焦点管理
accessibility - —— 跨框架前端架构
frontend-patterns - —— 应用于 Vue 项目的 TypeScript 最佳实践
typescript - —— 通用代码质量标准
coding-standards