選項
首頁首頁 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 呼叫,不存取 store。純粹進行渲染。

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];
}>();
  • 在模板中使用 kebab-case 命名法 (@update:model-value)。
  • 在腳本中使用駱駝式命名法 (emit("update:modelValue", val))。

3. Composables(可重用邏輯)

結構

// 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 中清理副作用。
  • 不得產生模組範圍內的副作用。

vs 混入(Mixins)

可組合元件完全取代了 Vue 2 的混合函式:

  • Mixins:不透明的資料流、權威來源衝突、名稱衝突。
  • 可組合元件:明確的導入、清晰的回傳值、可組合且支援樹狀震動。

4. 狀態管理

何時該使用什麼

模式 使用情境
ref()/reactive() 本機元件狀態
Props + Emits 父子通訊
提供 / 注入 主題、設定、外掛程式 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)。
  • 建議使用 action 處理業務層級的變動,並使用$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用於 Store 模擬
  • 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

反應式屬性解構

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()

將名稱匹配的普通 ref 替換為useTemplateRef(),用以處理模板參照:

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)下穩定的唯一識別碼生成,並兼顧無障礙支援:

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 使用 getter 封裝器:watch(() => count, ...)
在同一元素上使用v-if+v-for 執行順序不確定 使用經過篩選的計算結果陣列
v-for的 key 參數設定為 index 重新排序時狀態失效 使用穩定的資料庫 ID
修改 props 違反單向資料流原則 發送事件或使用v-model
含有使用者內容的v-html XSS 漏洞 使用 DOMPurify 進行資料淨化
Vue 3 中的 Mixins 不透明且易發生衝突 以可組合元件取代
可組合函式中的模組作用域副作用 在不同實例間共享 onMounted與onUnmounted中的作用域
reactive()適用於可替換的狀態 替換會破壞反應性 請改用ref()
未進行清理的 Watcher 記憶體洩漏、競態條件 請使用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