| name | vue-patterns |
| description | Vue.js 3 Composition API 模式、组件架构、响应式最佳实践、Pinia 状态管理、Vue Router 导航和 Nuxt SSR 模式。在 Vue、Nuxt、Vite 或 Pinia 项目中激活。 |
| origin | ECC |
Vue.js 模式与最佳实践
使用 Composition API(<script setup>)进行 Vue.js 3 开发的综合指南,覆盖组件设计、响应式、状态管理、路由、测试和 SSR 模式。Nuxt 专属指引在与原生 Vue 不同处给出。
何时激活
在以下情况激活:
- 项目使用 Vue.js(任何版本)、Nuxt、Vite + Vue 或 Pinia。
- 用户问 Vue 组件架构、composables、响应式或状态管理。
- 审查 Vue Single-File Components(
.vue 文件)。
- 搭建 Vue Router、Pinia stores 或 Vite/Vitest 配置。
- 讨论 Vue 专属性能、安全或 SSR 模式。
1. 项目结构
推荐布局(Feature-First)
src/
├── api/ # API client 和 endpoint 定义
├── assets/ # 静态资源(图片、字体、图标)
├── components/ # 共享/复用组件
│ ├── base/ # 基础 UI primitives(Button、Input、Modal)
│ └── features/ # Feature 专属共享组件
├── composables/ # 可复用 Composition API 逻辑
├── layouts/ # 页面布局(可选)
├── pages/ # Route 级页面组件
├── router/ # Vue Router 配置
├── stores/ # Pinia stores
├── types/ # TypeScript 类型定义
├── utils/ # 纯工具函数
└── App.vue # 根组件
文件命名
| 约定 | 何时使用 |
|---|
PascalCase.vue | 所有组件(由 vue/multi-word-component-names 强制) |
useCamelCase.ts | Composables |
camelCase.ts | 工具、API clients、types |
kebab-case 目录 | Route segments、feature 文件夹 |
2. 组件架构
Single-File Component 顺序
<script setup lang="ts">
// 1. Imports(vue → ecosystem → 绝对 → 相对)
// 2. Props & Emits & Slots
// 3. Composables
// 4. 本地状态(ref/reactive)
// 5. Computed 属性
// 6. Methods
// 7. Watchers
// 8. 生命周期 hooks
</script>
<template>
<!-- Template 内容 -->
</template>
<style scoped>
/* Scoped 样式 */
</style>
Presentational vs Container
- Container 组件:拥有数据获取、状态和副作用。渲染 presentational 组件。
- Presentational 组件:接收 props、emit 事件。无 API 调用、无 store 访问。纯渲染。
Props 最佳实践
interface Props {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
items: Item[];
}
const props = withDefaults(defineProps<Props>(), {
variant: "primary",
disabled: false,
});
- 始终提供
type,并在合适时提供 required/default。
- Boolean props:
isXxx、hasXxx、canXxx。
- 绝不修改 props——改为 emit 事件。
- 对 v-model 绑定,使用
defineModel()(Vue 3.4+)或 modelValue + update:modelValue。
事件
const emit = defineEmits<{
submit: [];
"update:modelValue": [value: string];
select: [id: string, index: number];
}>();
- 在 templates 中用 kebab-case(
@update:model-value)。
- 在 script 中用 camelCase(
emit("update:modelValue", val))。
3. Composables(可复用逻辑)
结构
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);
}
规则
- 必须以
use 前缀开头。
- 返回响应式值(
ref、computed、reactive),绝不返回普通 primitives。
- 通过
MaybeRef / toRef() / toValue() 接受响应式输入。
- 在
onUnmounted 或 watcher onCleanup 中清理副作用。
- 无模块作用域副作用。
vs Mixins
Composables 完全取代 Vue 2 mixins:
- Mixins:不透明数据流、source-of-truth 冲突、命名冲突。
- Composables:显式 imports、清晰返回值、可组合且 tree-shakable。
4. 状态管理
何时用什么
| 模式 | 用例 |
|---|
ref() / reactive() | 本地组件状态 |
| Props + Emits | 父子通信 |
| Provide / Inject | Theme、config、plugin API |
| Pinia store | 全局、共享、复杂状态 |
| Server state composable | 带 cache 的 API 数据(包装 fetch/TanStack Query) |
Pinia Setup Store(首选)
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)。
- 业务级 mutation 优先用 actions,成组更新用
$patch()。
- 每个异步 action:处理 loading + success + error。
5. Vue Router
Route 定义
const routes = [
{
path: "/users/:id",
name: "user-detail",
component: () => import("@/pages/UserDetail.vue"),
props: true,
meta: { requiresAuth: true },
},
];
Navigation Guards
router.beforeEach((to, from) => {
const { isLoggedIn } = useAuthStore();
if (to.meta.requiresAuth && !isLoggedIn) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
响应式 Route Params
当组件保持挂载但 route params 变化时:
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));
6. 模板模式
模板语法
<!-- 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 用于频繁切换 -->
<div v-show="isOpen">Toggled content</div>
<!-- v-for 带稳定 key -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
<!-- Computed 过滤列表(不是同一元素上的 v-if + v-for) -->
<div v-for="item in activeItems" :key="item.id">{{ item.name }}</div>
<!-- 事件处理 -->
<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. 性能
| 技术 | 何时使用 |
|---|
v-memo | 很少变化的列表项 |
v-once | 只渲染一次且永远静态的内容 |
shallowRef() | 整体替换的大型数据结构 |
shallowReactive() | 只有顶层属性需要响应式 |
v-show over v-if | 频繁的可见性切换 |
<KeepAlive :max="10"> | 缓存切换的视图 |
| 懒加载 routes | () => import(...) 用于非关键 routes |
Suspense | 带 fallback 的异步组件加载 |
8. 测试
技术栈
- Vitest 用于单元和组件测试
- Vue Test Utils 用于挂载和交互
- @pinia/testing 用于 store mocking
- Playwright 用于 E2E
组件测试模式
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 专属模式
Auto-Imports
Nuxt 自动导入 ref、computed、watch、useFetch、useAsyncData 等。直接使用,无需 import。非 Nuxt 项目始终显式 import。
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 Routes
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, z.object({
id: z.string().uuid(),
}).parse);
});
Runtime Config
export default defineNuxtConfig({
runtimeConfig: {
apiSecret: "",
public: {
apiBase: "https://api.example.com",
},
},
});
10. Vue 3.5+ 新 API
Reactive Props Destructure
Vue 3.5 稳定了 reactive props destructure——从 defineProps() 解构的变量会自动保持响应式:
const { count = 0, msg = "hello" } = defineProps<{
count?: number;
msg?: string;
}>();
watch(() => count, (newVal) => { ... });
useTemplateRef()
用 useTemplateRef() 替代名称匹配的普通 ref 来做 template references:
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef<HTMLInputElement>("input");
支持动态 ref IDs:useTemplateRef(dynamicRefId)。
onWatcherCleanup()
全局可 import 的 watcher cleanup API(Vue 3.5+)。必须在 watcher callback 内同步调用:
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();
defer Teleport
<Teleport defer> 允许 teleport 到同一周期内渲染的目标:
<Teleport defer to="#container">Content</Teleport>
<div id="container"></div>
懒加载 Hydration(SSR)
defineAsyncComponent() 现支持 hydrate 策略:
import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
loader: () => import("./Comp.vue"),
hydrate: hydrateOnVisible(),
});
反模式
| 反模式 | 为什么错 | 修复 |
|---|
解构 defineProps()(Vue < 3.5) | 捕获快照,丢失响应式 | 通过 props.xxx 访问或用 toRefs() |
对解构 prop watch()(Vue 3.5+) | 编译期错误——解构 props 不能直接 watch | 用 getter 包装:watch(() => count, ...) |
同一元素上 v-if + v-for | 执行顺序歧义 | 用 computed 过滤数组 |
v-for key = index | 重排时状态错乱 | 用稳定数据库 IDs |
| 修改 props | 违反单向数据流 | emit 事件或用 v-model |
用用户内容 v-html | XSS 漏洞 | 用 DOMPurify 净化 |
| Vue 3 中用 Mixins | 不透明、易冲突 | 替换为 composables |
| Composable 中模块作用域副作用 | 跨实例共享 | 在 onMounted + onUnmounted 中限定 |
reactive() 用于可替换状态 | 替换会破坏响应式 | 改用 ref() |
| 无清理的 watcher | 内存泄漏、竞态 | 用 onCleanup 或 onWatcherCleanup()(Vue 3.5+) |
| 新 Vue 3 代码用 Options API | 生态转向 Composition API | 用 <script setup> |
| 用普通 ref 做 template references | 不支持动态 ref、名称匹配脆弱 | 用 useTemplateRef()(Vue 3.5+) |
相关技能
accessibility — ARIA、语义 HTML、focus 管理
frontend-patterns — 跨框架前端架构
typescript — 应用于 Vue 项目的 TypeScript 最佳实践
coding-standards — 通用代码质量标准