Vzory komponent a composable v Vue
Jozo
Jozo
2025/03/18
12 min read

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>