Vue komponent a composable vzory dizajnu
Jozo
Jozo
2025/03/18
12 min read

Vue komponent a composable vzory dizajnu: Návod na štart

Táto príručka poskytuje praktický prehľad Vue komponent a composable vzorov dizajnu, s tipmi a príkladmi, prispôsobenými vývojárom vo vašom startupe. Využíva poznatky z rôznych zdrojov, aby vám pomohla napísať čistší, ľahšie udržiavateľný a škálovateľný Vue aplikácie.

Vzory dizajnu komponentov

1. Vzor komponentov

Extrahovanie opätovne použiteľných komponentov z existujúcich komponentov zjednodušuje kód a zvyšuje opätovnú použiteľnosť. To podporuje princíp jednej zodpovednosti, vďaka čomu je vaša kódová základňa modulárnejšia a udržiavateľnejšia.

Tip: Identifikujte a extrahovajte skryté komponenty v rámci vášho existujúceho kódu. Hľadajte opakované prvky UI alebo logiku, ktorú je možné zapúzdriť.

Príklad:

<!-- Pred: Komplexný formulár -->
<template>
  <div>
    <label for="name">Meno:</label>
    <input type="text" id="name" v-model="name">
    <label for="email">Email:</label>
    <input type="email" id="email" v-model="email">
    <button @click="submitForm">Poslať</button>
  </div>
</template>
<!-- Po: Využívanie komponentov -->
<template>
  <div>
    <InputField label="Meno" v-model="name" type="text" />
    <InputField label="Email" v-model="email" type="email" />
    <SubmitButton @click="submitForm" />
  </div>
</template>

2. Čisté komponenty

Cieľom sú komponenty, ktoré nielen pracujú, ale aj pracujú dobre, berúc do úvahy čitateľnosť kódu, udržiavateľnosť a testovateľnosť. Čisté komponenty sú ľahké na pochopenie, úpravu a ladenie.

Tip: Napíšte komponenty, ktoré sú ľahké na pochopenie a udržiavanie. Používajte jasné konvencie pomenovania, konzistentné formátovanie a jasne definované zodpovednosti.

Príklad:

<!-- Zlé: Komponent so zmiešanými opatreniami -->
<template>
  <div>
    <button @click="handleClick">{{ buttonText }}</button>
    <div v-if="showDetails">{{ details }}</div>
  </div>
</template>
<script setup>
import { ref } from 'vue';

const buttonText = ref('Zobraziť podrobnosti');
const showDetails = ref(false);
const details = ref('');

async function handleClick() {
  showDetails.value = !showDetails.value;
  if (showDetails.value) {
    details.value = await fetchData();
  }
}
</script>
<!-- Dobrá: Komponent so zameranou zodpovednosťou -->
<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('Zobraziť podrobnosti');

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

3. Viacnásobné komponenty v jednom súbore

Na malé, samostatne uzavreté komponenty zvážte, aby ste ich zips v rovnakom súbore. To môže znížiť počet súborov vo vašom projekte a zlepšiť rýchlosť vývoja, obzvlášť pre komponenty, ktoré sú pevne viazané.

Tip: Vyhnite sa vytváram zbytočných súborov na jednoduché komponenty. Použite tento prístup na komponenty, ktoré sa používajú len na jednom mieste.

Príklad:

<template>
  <div>
    <MyButton @click="handleClick">Klikni ma</MyButton>
  </div>
</template>

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

function handleClick() {
  alert('Tlačidlo bolo kliknuté!');
}
</script>
<template>
  <button @click="$emit('click')">
    <slot></slot>
  </button>
</template>
<script setup>
defineEmits(['click']);
</script>

4. Vzor kontrolovaných rekvizít

Tento vzor vám umožňuje prepísať interný stav komponenty z rodičovského prvku. To je užitočné, keď potrebujete vynútiť stav komponenty zvonka, ako je kontrola viditeľnosti modálny alebo výberu v rozbaľovacom menu.

Tip: Použite tento vzor, keď potrebujete vynútiť stav komponenty zvonka. Presuňte rekvizity komponentu na kontrolu jej interného stavu.

Príklad:

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

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

function closeModal() {
  emit('close');
}
</script>
<!-- Komponent rodiča -->
<template>
  <div>
    <button @click="showModal = true">Otvoriť modálny</button>
    <Modal :isOpen="showModal" @close="showModal = false">
      <p>Obsah modálnych</p>
    </Modal>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';

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

5. Metadáta komponentov

Pridajte metadáta komponentom na poskytnúť ďalšie informácie ostatným komponentom. To je možné použiť na konfiguráciu komponentov, na sto ďalších informácií alebo na uľahčenie komunikácie medzi komponentami.

Tip: Používajte metadáta na konfiguráciu komponentov alebo na sto ďalších informácií. To môže byť užitočné na nástroje alebo na poskytnúť kontext ostatným komponentom.

Príklad:

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

Composable vzory dizajnu

1. Vzor objektu možnosti

Na prenos parametrov do composables používajte objekt. To umožňuje flexibilitu a škálovateľnosť. Je to preferovaný spôsob na prenos mnohých možností do composable.

Tip: Tento vzor sa používa v VueUse a je vrúcne odporúčaný, keď potrebujete nakonfigurujte správanie composable.

Prí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 chyba! 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 komponente:
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. Vrátené composables

Vytvárali composables priamo v súbore komponentu, aby ste sa vyhli vytváraní nových súborov. To je obzvlášť užitočné pre composables, ktoré sú veľmi špecifické pre jeden komponent a nie sú určené na opätovné použitie.

Používajte vrátené composables na malú, špecifickú logiku komponentov. To udržiava súvisiaci kód spolu a môže zjednodušiť štruktúru komponentov.

Tip: Používajte vrátené composables na malú, špecifickú logiku komponentov, ktorá nemá byť opakovane použitá. Tento prístup je skladá spolu s komponentom a zjednodušuje štruktúru komponentov.

Príklad:

Povedzme, že máte toto v svojom komponente:

<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 to v poriadku po teraz, ale ak budete potrebovať formátovanie dátumov kdekoľvek iinde, ste stratení. Tu je refaktorovaná verzia:

// 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 }
}
<!-- Vo vašom komponente -->
<template>
  <div>{{ formattedDate }}</div>
</template>

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

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

Teraz môžete opätovne používať useFormattedDate v ľubovoľnom komponente. Problém vyriešený.

3. Lepšie kódovanie Composables

Extrahovať malé kúsky logiky do funkcií, ktoré môžete ľahko opakovane používať. To podporuje opätovné používanie kódu, znižuje duplikácie a robí váš kód udržiavateľnejším.

Tip: Používajte composables na organizáciu a opätovné používanie obchodnej logiky. Myslite na composables ako na opätovne použiteľné stavebné bloky pre vašu aplikáciu.

Prí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žitie v komponente:
<script setup>
import { useLocalStorage } from './useLocalStorage';

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

4. Začnite s rozhraním

Pred implementáciou definujte, ako sa composable bude používať. To je forma vývoja "dizajn-prvý", ktorá vám pomôže ujasniť účel composable, vstupy a výstupy pred napísaním akéhokoľvek kódu.

Tip: Definujte vstupy (rekvizity, možnosti) a výstupy (vrátené hodnoty) composable prvý. To vám pomôže zamerať sa na API composable.

Príklad:

// Pred implementáciou: useCounter.js
// Mal by akceptovať počiatočnú hodnotu
// Mal by vrátiť počet a metódy na prírasty a pokles
// Implementácia:
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. Opätovné použitie logiky s oblasťou sloty

Používajte oblasť sloty na opätovné používanie logiky medzi komponentami jedinečným spôsobom. Oblasť sloty umožňujú rodičovskému komponentu presunúť údaje a logiku do svojho dieťaťa, čo poskytuje flexibilný spôsob zdieľania funkcií.

Tip: Oblasť sloty je možné použiť na prenos údajov a logiky z rodičovského komponentu na dieťa. To umožňuje dieťaťu vykresliť svoj obsah na základe údajov a logiky poskytnutej rodiča.

Príklad:

<!-- DataFetcher.vue -->
<template>
  <div>
    <div v-if="loading">Načítavame...</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 chyba! status: ${response.status}`);
    }
    data.value = await response.json();
  } catch (e) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
});
</script>
<!-- Použitie v komponente -->
<DataFetcher url="/api/items">
  <template v-slot="{ data, loading, error }">
    <div v-if="loading">Načítavame položky...</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>

Všeobecný tips a najlepšie postupy

1. Ref vs. Reaktívna

Pochopte rozdiele medzi ref a reactive a vyberte si vhodný pre váš prípad použitia. ref sa používa na primárne hodnoty, zatiaľ čo reactive sa používa na objekty a polia.

Tip: Používajte ref na primárne hodnoty a reactive na objekty a polia. To pomáha systému reaktivity Vue.

Príklad:

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

export default {
  setup() {
    const count = ref(0); // ref na primárnu hodnotu
    const user = reactive({ name: 'John', age: 30 }); // reaktívny na objekt
    return { count, user };
  }
}
</script>

2. Efektívna správa stavu

Štruktúra stavu v aplikáciách efektívne. To je rozhodujúce na spravovanie toku údajov a zabezpečenie, aby vaša aplikácia zostala spravovateľná s rastúcou. Zvážte vhodné knižnice na správu stavu pre väčšie aplikácie.

Tip: Na správu stavu v väčšom aplikáciách zvážte Pinia alebo Vuex. Tieto knižnice poskytujú centralizovanú správu stavu a uľahčujú spravovanie zložitého stavu aplikácie.

Príklad: (Ilustratívne - skutočná implementácia závisí od zvolenej knižnice na správu stavu)

// Príklad Pinia (Konceptuálne)
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žívajte úvodné slová na sledovanie vnorených hodnôt

Sledujte vnorené hodnoty priamo pomocou úvodných slov. To umožňuje sledovanie zmien v špecifických vlastnostiach objektu, pričom sa spúšťajú aktualizácie pri zmene týchto vlastností.

Tip: Používajte úvodné slová v možnosti watch na sledovanie vnorených vlastností objektu. To je efektívnejší a špecifickejší spôsob na sledovanie zmien.

Príklad:

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

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

    watch(() => data.value.user.name, (newName) => {
      console.log('Meno sa zmenilo:', newName);
    });

    return { data };
  }
}
</script>

4. Vzor extrakcie podmieneného textu

Rozdelite komponenty podľa podmienenej logiky. To zlepšuje čitateľnosť a udržiavateľnosť tým, že oddelí opatrenia. Robí komponenty zrozumiteľnejšími a testovateľnými.

Tip: Ak má komponent zložitú podmienenú logiku, zvážte jej rozdelenie do menších komponentov. To vytvára zameranejšie a spravovateľnejšie komponenty.

Príklad:

<!-- Pred: Zložitá podmienená logika -->
<template>
  <div>
    <div v-if="isLoading">Načítavame...</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žitie extrahu komponentov -->
<template>
  <LoadingIndicator v-if="isLoading" />
  <ErrorMessage v-if="error" :message="error" />
  <UsersDisplay v-else :users="users" />
</template>

5. 6 dôvodov na rozdelenie komponentov

Rozdeľte komponenty na menšie časti na zlepšenie organizácie kódu a opätovného použitia. To zvyšuje čitateľnosť, udržiavateľnosť a testovateľnosť vášho kódu.

Tip: Menšie komponenty sú ľahšie na pochopenie, testovanie a údržbu. Podporujú aj opätovné použitie, keďže ich môžete používať v rôznych častiach aplikácie.

Príklad:

<!-- Pred: Monolitický komponent -->
<template>
  <div>
    <Header />
    <Sidebar />
    <MainContent />
    <Footer />
  </div>
</template>
<!-- Po: Použitie extrahu komponentov -->
<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. Neprepíšte komponenty CSS

Vyhnite sa priamej úprave CSS komponenty zvonka. To môže viesť k neočakávanému správaniu a sťažuje údržbu štýlov vašej aplikácie. Zapúzdrite štýly v samotnom komponente.

Tip: Používajte rekvizity alebo sloty na úpravu vzhľadu komponentu. To umožňuje kontrolované štýlovanie a zabezpečuje, že sa zabráni neočakávaným konfliktom štýlu.

Prí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é štýly tlačidla */
}
.primary {
  /* Primárne štýly tlačidla */
}
.secondary {
  /* Sekundárne štýly tlačidla */
}
</style>