Vzory komponent a composable v Vue
Jozo· 12 min read· 2025/03/18
VueJavaScriptFrontendWeb vývojKomponentyComposables

Vzory komponent a composable v Vue

Vzory komponent a composable v Vue: Průvodce začátečníka

Tento průvodce poskytuje praktický přehled vzorů návrhu komponent a composable v Vue s tipy a příklady, přizpůsobený vývojářům ve vašem startupu. Využívá poznatky z různých zdrojů, které vám pomohou psát čistší, udržitelnější a lépe škálovatelný kód Vue.

Vzory návrhu komponent

1. Vzor komponent

Extrahování znovupoužitelných komponent z existujících komponent zjednodušuje kód a zvyšuje znovupoužitelnost. To podporuje Princip jediné odpovědnosti, čímž je váš kódový základ modulárnější a snadněji udržitelný.

Tip: Identifikujte a extrahujte skryté komponenty v existujícím kódu. Hledejte opakující se prvky UI nebo logiku, která může být zapouzdřena.

Příklad:

<!-- Před: Komplexní formulář -->
<template>
  <div>
    <label for="name">Jméno:</label>
    <input type="text" id="name" v-model="name">
    <label for="email">E-mail:</label>
    <input type="email" id="email" v-model="email">
    <button @click="submitForm">Odeslat</button>
  </div>
</template>
<!-- Po: Použití znovupoužitelných komponent -->
<template>
  <div>
    <InputField label="Jméno" v-model="name" type="text" />
    <InputField label="E-mail" v-model="email" type="email" />
    <SubmitButton @click="submitForm" />
  </div>
</template>

2. Čisté komponenty

Zaměřujte se na komponenty, které nejen fungují, ale fungují dobře, přičemž zohledňujete čitelnost kódu, udržovatelnost a testovatelnost. Čisté komponenty se snadno chápou, upravují a ladí.

Tip: Pište komponenty, které se snadno chápou a údržují. Používejte jasné konvence pojmenování, konzistentní formátování a dobře definované odpovědnosti.

Příklad:

<!-- Špatně: Komponenta se smíšenými starostmi -->
<template>
  <div>
    <button @click="handleClick">{{ buttonText }}</button>
    <div v-if="showDetails">{{ details }}</div>
  </div>
</template>
<script setup>
import { ref } from 'vue';

const buttonText = ref('Zobrazit detaily');
const showDetails = ref(false);
const details = ref('');

async function handleClick() {
  showDetails.value = !showDetails.value;
  if (showDetails.value) {
    details.value = await fetchData();
  }
}
</script>
<!-- Dobře: Komponenta se zaměřenou odpovědností -->
<template>
  <div>
    <ShowDetailsButton @click="toggleDetails" :text="buttonText" />
    <DetailsDisplay v-if="showDetails" :details="details" />
  </div>
</template>
<script setup>
import { ref } from 'vue';
import ShowDetailsButton from './ShowDetailsButton.vue';
import DetailsDisplay from './DetailsDisplay.vue';

const showDetails = ref(false);
const details = ref('');
const buttonText = ref('Zobrazit detaily');

async function toggleDetails() {
  showDetails.value = !showDetails.value;
  if (showDetails.value) {
    details.value = await fetchData();
  }
}
</script>

3. Více komponent v jednom souboru

U malých, nezávislých komponent zvažte jejich uchovávání ve stejném souboru. To může snížit počet souborů v projektu a zlepšit rychlost vývoje, zejména pro komponenty, které jsou těsně provázány.

Tip: Vytvářejte zbytečné soubory pro jednoduché komponenty. Tento přístup používejte pro komponenty, které se používají pouze na jednom místě.

Příklad:

<template>
  <div>
    <MyButton @click="handleClick">Klikni na mě</MyButton>
  </div>
</template>

<script setup>
import MyButton from './MyButton.vue';

function handleClick() {
  alert('Tlačítko bylo kliknuto!');
}
</script>
<template>
  <button @click="$emit('click')">
    <slot></slot>
  </button>
</template>
<script setup>
defineEmits(['click']);
</script>

4. Vzor kontrolovaných props

Tento vzor vám umožňuje přepsat interní stav komponenty z nadřazené komponenty. To je užitečné, když potřebujete vynutit stav komponenty zvenčí, například kontrolu viditelnosti modálu nebo výběru v rozevíracím seznamu.

Tip: Použijte tento vzor, když potřebujete vynutit stav komponenty zvenčí. Předejte props komponentě pro kontrolu jejího interního stavu.

Příklad:

<!-- Modal.vue -->
<template>
  <div v-if="isOpen" class="modal">
    <div class="modal-content">
      <slot></slot>
      <button @click="closeModal">Zavřít</button>
    </div>
  </div>
</template>
<script setup>
defineProps({
  isOpen: {
    type: Boolean,
    default: false
  }
});

const emit = defineEmits(['close']);

function closeModal() {
  emit('close');
}
</script>
<!-- Nadřazená komponenta -->
<template>
  <div>
    <button @click="showModal = true">Otevřít modal</button>
    <Modal :isOpen="showModal" @close="showModal = false">
      <p>Obsah modálu</p>
    </Modal>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';

const showModal = ref(false);
</script>

5. Metadata komponenty

Přidejte metadata komponentám, abyste poskytli další informace jiným komponentám. To lze použít pro konfiguraci komponent, předávání dalších informací nebo usnadnění komunikace mezi komponentami.

Tip: Používejte metadata pro konfiguraci komponent nebo předávání dalších informací. To může být užitečné pro tooling nebo poskytnutí kontextu jiným komponentám.

Příklad:

<!-- Komponenta A -->
<template>
  <div>Komponenta A</div>
</template>
<script>
export default {
  meta: {
    componentType: 'display'
  }
}
</script>
<!-- Komponenta B -->
<template>
  <div>Komponenta B</div>
</template>
<script>
export default {
  meta: {
    componentType: 'formField'
  }
}
</script>

Vzory návrhu composable

1. Vzor objektu možností

Použijte objekt k předávání parametrů do composables. To umožňuje flexibilitu a škálovatelnost. Je to upřednostňovaná metoda pro předávání mnoha možností na composable.

Tip: Tento vzor se používá v VueUse a je vysoce doporučen, když potřebujete konfigurovat chování composable.

Příklad:

// useFetch.js
import { ref, onMounted } from 'vue';

export function useFetch(url, options = {}) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const { method = 'GET', headers = {}, body = null } = options;

  async function fetchData() {
    loading.value = true;
    try {
      const response = await fetch(url, {
        method,
        headers,
        body: body ? JSON.stringify(body) : null
      });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      data.value = await response.json();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  }

  onMounted(() => {
    fetchData();
  });

  return { data, loading, error, fetchData };
}

// V komponentě:
import { useFetch } from './useFetch';
export default {
  setup() {
    const { data, loading, error } = useFetch('/api/data', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: { key: 'value' }
    });
    return { data, loading, error };
  }
}

2. Inline Composables

Vytvářejte composables přímo v souboru komponenty, abyste se vyhnuli vytváření nových souborů. Toto je zvlášť užitečné pro composables, které jsou velmi specifické pro jednu komponentu a nejsou určeny k opětovnému použití jinde. Používejte inline composables pro malou, komponent-specifickou logiku. Tento přístup udržuje související kód dohromady a zjednodušuje strukturu komponenty.

Tip: Používejte inline composables pro malou, komponent-specifickou logiku, která nemusí být použita jinde. Tento přístup udržuje související kód pohromadě a zjednodušuje strukturu vaší komponenty.

Příklad:

Řekněme, že máte toto v komponentě:

<template>
  <div>{{ formattedDate }}</div>
</template>

<script setup>
import { ref, computed } from 'vue'

const useFormattedDate = () => {
  const rawDate = ref(new Date())
  const formattedDate = computed(() => {
    const options = { year: 'numeric', month: 'long', day: 'numeric' }
    return rawDate.value.toLocaleDateString(undefined, options)
  })
  return { formattedDate }
}

const { formattedDate } = useFormattedDate()
</script>

Je v pořádku zatím, ale pokud budete potřebovat toto formátování data kdekoli jinde, jste ve ztrátě. Zde je refaktorovaná verze:

// src/composables/useFormattedDate.ts
import { ref, computed } from 'vue'

export function useFormattedDate() {
  const rawDate = ref(new Date())
  const formattedDate = computed(() => {
    const options = { year: 'numeric', month: 'long', day: 'numeric' }
    return rawDate.value.toLocaleDateString(undefined, options)
  })
  return { formattedDate }
}
<!-- Ve vaší komponentě -->
<template>
  <div>{{ formattedDate }}</div>
</template>

<script setup>
import { useFormattedDate } from './composables/useFormattedDate'

const { formattedDate } = useFormattedDate()
</script>

Nyní můžete znovupoužívat useFormattedDate v libovolné komponentě. Problém vyřešen.

3. Psaní lepších composables

Extrahujte malé kousky logiky do funkcí, které můžete snadno opakovaně používat. To podporuje opětovné použití kódu, snižuje duplikaci a činí váš kód lépe udržitelným.

Tip: Používejte composables k organizaci a opětnému použití obchodní logiky. Uvažujte o composables jako o znovupoužitelných stavebních blocích pro vaši aplikaci.

Příklad:

// useLocalStorage.ts
import { ref, watch } from 'vue';

export function useLocalStorage<T>(key: string, defaultValue: T) {
  const storedValue = localStorage.getItem(key);
  const value = ref<T>(storedValue !== null ? JSON.parse(storedValue) : defaultValue);

  watch(
    value,
    (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue));
    },
    { deep: true }
  );

  return value;
}

// Použití v komponentě:
<script setup>
import { useLocalStorage } from './useLocalStorage';

const theme = useLocalStorage('theme', 'light');
</script>

4. Začněte s rozhraním

Definujte, jak bude composable používán, než ho implementujete. Toto je forma "design-first" vývoje, která vám pomůže objasnit účel, vstupy a výstupy composable, než napíšete jakýkoliv kód.

Tip: Definujte vstupy (props, možnosti) a výstupy (vrácené hodnoty) composable nejdřív. To vám pomůže zaměřit se na API composable.

Příklad:

// Před implementací: useCounter.js
// By mělo přijmout počáteční hodnotu
// Mělo by vrátit počet a metody pro zvýšení a snížení
// Implementace:
import { ref } from 'vue';

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);

  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  return { count, increment, decrement };
}

5. Znovupoužití logiky s scoped slots

Používejte scoped slots k znovupoužití logiky mezi komponentami jedinečným způsobem. Scoped slots umožňují nadřazené komponentě předat data a logiku dceřiné komponentě, čímž poskytují flexibilní způsob, jak sdílet funkcionalitu.

Tip: Scoped slots lze použít k předávání dat a logiky z nadřazené komponenty na podřízenou komponentu. To umožňuje podřízené komponentě renderovat svůj obsah na základě dat a logiky poskytnuté nadřazenou komponentou.

Příklad:

<!-- DataFetcher.vue -->
<template>
  <div>
    <div v-if="loading">Načítání...</div>
    <div v-else-if="error">Chyba: {{ error }}</div>
    <div v-else>
      <slot :data="data" :loading="loading" :error="error"></slot>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue';

const props = defineProps({
  url: {
    type: String,
    required: true
  }
});

const data = ref(null);
const loading = ref(true);
const error = ref(null);

onMounted(async () => {
  try {
    const response = await fetch(props.url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    data.value = await response.json();
  } catch (e) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
});
</script>
<!-- Použití v komponentě -->
<DataFetcher url="/api/items">
  <template v-slot="{ data, loading, error }">
    <div v-if="loading">Načítání položek...</div>
    <div v-else-if="error">Chyba: {{ error }}</div>
    <div v-else>
      <ul>
        <li v-for="item in data" :key="item.id">{{ item.name }}</li>
      </ul>
    </div>
  </template>
</DataFetcher>

Obecné tipy a osvědčené postupy

1. Ref vs. Reactive

Pochopte rozdíly mezi ref a reactive a vyberte si vhodný pro váš případ použití. ref se používá pro primitivní hodnoty, zatímco reactive se používá pro objekty a pole.

Tip: Používejte ref pro primitivní hodnoty a reactive pro objekty a pole. To vám pomůže s reaktivitou Vue.

Příklad:

<script>
import { ref, reactive } from 'vue';

export default {
  setup() {
    const count = ref(0); // ref pro primitivní hodnotu
    const user = reactive({ name: 'Jan', age: 30 }); // reactive pro objekt
    return { count, user };
  }
}
</script>

2. Efektivní správa stavu

Efektivně strukturujte stav ve vašich aplikacích. To je zásadní pro správu toku dat a zajištění, aby vaše aplikace zůstala spravovatelná, když roste. Zvažte vhodné knihovny pro správu stavu pro větší aplikace.

Tip: Zvažte používání Pinia nebo Vuex pro správu stavu ve větších aplikacích. Tyto knihovny poskytují centralizovanou správu stavu a usnadňují manipulaci se složitým stavem aplikace.

Příklad: (Ilustrativní - skutečná implementace závisí na zvolené knihovně správy stavu)

// Příklad Pinia (koncepční)
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    isLoggedIn: false,
    user: null
  }),
  actions: {
    login(userData) {
      this.isLoggedIn = true;
      this.user = userData;
    },
    logout() {
      this.isLoggedIn = false;
      this.user = null;
    }
  }
});

3. Používejte uvozovky ke sledování vnořených hodnot

Sledujte vnořené hodnoty přímo pomocí uvozovek. To vám umožňuje sledovat změny konkrétních vlastností v objektu, což spouští aktualizace, když se tyto vlastnosti změní. *Tip: Používejte uvozovky v možnosti watch ke sledování vnořených vlastností objektu. To je efektivnější a specifičtější způsob, jak monitorovat změny.

Příklad:

<script>
import { ref, watch } from 'vue';

export default {
  setup() {
    const data = ref({ user: { name: 'Jan' } });

    watch(() => data.value.user.name, (newName) => {
      console.log('Jméno se změnilo:', newName);
    });

    return { data };
  }
}
</script>

4. Vzor extrahování podmíněného

Rozdělujte komponenty na základě podmíněné logiky. To zlepšuje čitelnost a udržovatelnost oddělením obav. Činí komponenty snadněji srozumitelnými a testovatelný.

Tip: Pokud má komponenta složitou podmíněnou logiku, zvažte její rozdělení do menších komponent. To vytváří více zaměřené a spravovatelné komponenty.

Příklad:

<!-- Před: Komplexní podmínka -->
<template>
  <div>
    <div v-if="isLoading">Načítání...</div>
    <div v-else-if="error">Chyba: {{ error }}</div>
    <div v-else>
      <UserList v-if="users.length > 0" :users="users" />
      <NoUsersMessage v-else />
    </div>
  </div>
</template>
<!-- Po: Použití extrahovaných komponent -->
<template>
  <LoadingIndicator v-if="isLoading" />
  <ErrorMessage v-if="error" :message="error" />
  <UsersDisplay v-else :users="users" />
</template>

5. 6 důvodů pro rozdělení komponent

Rozdělujte komponenty na menší kousky, aby se zlepšila organizace kódu a znovupoužitelnost. To zvyšuje čitelnost, udržovatelnost a testovatelnost vašeho kódu.

Tip: Menší komponenty jsou snadněji srozumitelné, testovatelné a udržitelné. Také podporují opětovné použití, protože je můžete používat v několika částech vaší aplikace.

Příklad:

<!-- Před: Monolitická komponenta -->
<template>
  <div>
    <Header />
    <Sidebar />
    <MainContent />
    <Footer />
  </div>
</template>
<!-- Po: Použití extrahovaných komponent -->
<template>
  <AppLayout>
    <template v-slot:header><Header /></template>
    <template v-slot:sidebar><Sidebar /></template>
    <template v-slot:main><MainContent /></template>
    <template v-slot:footer><Footer /></template>
  </AppLayout>
</template>

6. Nepřepisujte CSS komponenty

Vyvarujte se přímé modifikace CSS komponenty zvnějšku komponenty. To může vést k neočekávanému chování a usnadňuje údržbu stylů vaší aplikace. Inkapsulujte styly v samotné komponentě.

Tip: Používejte props nebo slots k přizpůsobení vzhledu komponenty. To umožňuje kontrolované stylizování a zabraňuje neočekávaným konfliktům stylů.

Příklad:

<!-- Button.vue -->
<template>
  <button :class="['button', variant]" @click="emit('click')">
    <slot></slot>
  </button>
</template>
<script setup>
const props = defineProps({
  variant: {
    type: String,
    default: 'primary'
  }
});

const emit = defineEmits(['click']);
</script>
<style scoped>
.button {
  /* Základní styly tlačítka */
}
.primary {
  /* Primární styly tlačítka */
}
.secondary {
  /* Sekundární styly tlačítka */
}
</style>

Turn the best models into shipped work

Teamday installs AI employees with the right model, harness, MCP servers, workspace files, review path, and recurring mission. Stop comparing tools in isolation and put them to work.