选项
首页首页 Skill 网页开发 vue-patterns

vue-patterns

affaan-m/ECC affaan-m/ECC

为 Vue、Nuxt、Vite 或 Pinia 项目提供 Vue.js 3 组合式 API 模式、组件架构、响应式编程最佳实践、Pinia 状态管理、Vue Router 导航以及 Nuxt 服务器端渲染(SSR)模式。

...展开全部
0
更新时间 2026-10-02

Vue.js 设计模式与最佳实践

使用 Composition API 进行 Vue.js 3 开发的综合指南(

呈现组件与容器组件

  • 容器组件:负责数据获取、状态管理及副作用。渲染表现层组件。
  • 展示型组件:接收 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 迁移 使用

所有文件

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

复制 复制
快速设置: 将技能文件夹复制到 .claude/skills/ Claude 将自动检测并使用该技能
仓库 affaan-m/ECC

相关技能

github-code-search
更新时间 2026-06-29
drizzle-orm
更新时间 2026-06-29
clickhouse-io
更新时间 2026-06-29
prisma-client-api
更新时间 2026-06-29