Modèles de conception de composants Vue et de composables
Jozo
Jozo
2025/03/18
12 min read

Modèles de conception de composants Vue et de composables : Un guide de démarrage pour les startups

Ce guide fournit un aperçu pratique des modèles de conception des composants Vue et des composables, avec des conseils et des exemples, adaptés aux développeurs de votre startup. Il tire parti des informations de diverses sources pour vous aider à écrire des applications Vue plus propres, plus faciles à maintenir et plus scalables.

Modèles de conception de composants

1. Modèle de composants

L'extraction de composants réutilisables à partir des composants existants simplifie le code et améliore la réutilisabilité ^1. Cela favorise le principe de responsabilité unique, rendant votre base de code plus modulaire et facile à maintenir.

Conseil : Identifiez et extrayez les composants cachés au sein de votre code existant. Cherchez les éléments d'interface utilisateur répétitifs ou la logique qui peut être encapsulée.

Exemple :

<!-- Avant : Formulaire complexe -->
<template>
  <div>
    <label for="name">Nom:</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">Soumettre</button>
  </div>
</template>
<!-- Après : Utilisation de composants réutilisables -->
<template>
  <div>
    <InputField label="Nom" v-model="name" type="text" />
    <InputField label="Email" v-model="email" type="email" />
    <SubmitButton @click="submitForm" />
  </div>
</template>

2. Composants propres

Visez les composants qui non seulement fonctionnent bien mais qui travaillent bien, en considérant la lisibilité du code, la maintenabilité et la testabilité ^1. Les composants propres sont faciles à comprendre, à modifier et à déboguer.

Conseil : Écrivez des composants faciles à comprendre et à maintenir. Utilisez des conventions de dénomination claires, un formatage cohérent et des responsabilités bien définies.

Exemple :

<!-- Mauvais : Composant avec préoccupations mixtes -->
<template>
  <div>
    <button @click="handleClick">{{ buttonText }}</button>
    <div v-if="showDetails">{{ details }}</div>
  </div>
</template>
<script setup>
import { ref } from 'vue';

const buttonText = ref('Afficher les détails');
const showDetails = ref(false);
const details = ref('');

async function handleClick() {
  showDetails.value = !showDetails.value;
  if (showDetails.value) {
    details.value = await fetchData();
  }
}
</script>
<!-- Bon : Composant avec responsabilité ciblée -->
<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('Afficher les détails');

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

3. Plusieurs composants dans un fichier

Pour les petits composants autonomes, envisagez de les conserver dans le même fichier ^2. Cela peut réduire le nombre de fichiers dans votre projet et améliorer la vitesse de développement, notamment pour les composants étroitement liés.

Conseil : Évitez de créer des fichiers inutiles pour des composants simples. Utilisez cette approche pour les composants qui ne sont utilisés qu'au seul endroit.

Exemple :

<template>
  <div>
    <MyButton @click="handleClick">Cliquez-moi</MyButton>
  </div>
</template>

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

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

4. Modèle de props contrôlés

Ce modèle vous permet de remplacer l'état interne d'un composant à partir du composant parent ^4. Ceci est utile lorsque vous devez forcer l'état d'un composant de l'extérieur, comme contrôler la visibilité d'un modal ou la sélection dans une liste déroulante.

Conseil : Utilisez ce modèle lorsque vous devez forcer l'état d'un composant de l'extérieur. Transmettez des props au composant pour contrôler son état interne.

Exemple :

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

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

function closeModal() {
  emit('close');
}
</script>
<!-- Composant parent -->
<template>
  <div>
    <button @click="showModal = true">Ouvrir Modal</button>
    <Modal :isOpen="showModal" @close="showModal = false">
      <p>Contenu Modal</p>
    </Modal>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';

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

5. Métadonnées de composant

Ajoutez des métadonnées aux composants pour fournir des informations supplémentaires aux autres composants ^2. Ceci peut être utilisé pour la configuration de composant, pour passer des informations supplémentaires ou pour faciliter la communication entre les composants.

Conseil : Utilisez les métadonnées pour la configuration de composant ou pour transmettre des informations supplémentaires. Ceci peut être utile pour les outils ou pour fournir du contexte aux autres composants.

Exemple :

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

Modèles de conception de composables

1. Modèle d'objet d'options

Utilisez un objet pour transmettre les paramètres dans les composables ^1. Cela permet la flexibilité et la scalabilité. C'est la méthode préférée pour transmettre de nombreuses options à un composable.

Conseil : Ce modèle est utilisé dans VueUse et est hautement recommandé lorsque vous avez besoin de configurer le comportement d'un composable.

Exemple :

// 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(`Erreur HTTP ! statut : ${response.status}`);
      }
      data.value = await response.json();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  }

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

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

// Dans un composant :
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. Composables en ligne

Créez des composables directement dans le fichier de composant pour éviter de créer de nouveaux fichiers ^1. Cela peut réduire le nombre de fichiers dans votre projet et améliorer la vitesse de développement, notamment pour les composables très spécifiques à un seul composant et non destinés à être réutilisés ailleurs. Utilisez les composables en ligne pour la logique petite et spécifique à un composant. Cette approche garde le code associé ensemble et peut simplifier le développement.

Conseil : Utilisez les composables en ligne pour la petite logique spécifique au composant qui n'a pas besoin d'être réutilisée ailleurs. Cette approche garde le code associé ensemble et simplifie votre structure de composant.

Exemple :

Disons que vous avez ceci dans votre composant :

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

C'est correct pour l'instant, mais si vous avez besoin de ce formatage de date n'importe où ailleurs, vous êtes coincé. Voici la version refactorisée :

// 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 }
}
<!-- Dans votre composant -->
<template>
  <div>{{ formattedDate }}</div>
</template>

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

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

Maintenant, vous pouvez réutiliser useFormattedDate dans n'importe quel composant. Problème résolu.

3. Coding Better Composables

Extrayez les petites pièces de logique dans les fonctions que vous pouvez facilement réutiliser à plusieurs reprises ^1. Ceci favorise la réutilisation du code, réduit la duplication et rend votre code plus facile à maintenir.

Conseil : Utilisez les composables pour organiser et réutiliser la logique métier. Pensez aux composables comme des blocs de construction réutilisables pour votre application.

Exemple :

// 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;
}

// Utilisation dans un composant :
<script setup>
import { useLocalStorage } from './useLocalStorage';

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

4. Commencez par l'interface

Définissez comment un composable sera utilisé avant de l'implémenter ^4. C'est une forme de développement "design-first" qui vous aide à clarifier l'objectif du composable, les entrées et les sorties avant d'écrire du code.

Conseil : Définissez les entrées (props, options) et sorties (valeurs retournées) du composable en premier. Cela vous aide à vous concentrer sur l'API du composable.

Exemple :

// Avant l'implémentation : useCounter.js
// Doit accepter une valeur initiale
// Doit retourner un décompte et des méthodes pour l'incrémenter et le décrémenter
// Implémentation :
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. Réutiliser la logique avec des emplacements à portée

Utilisez les emplacements à portée pour réutiliser la logique entre les composants d'une manière unique ^5. Les slots à portée permettent à un composant parent de passer des données et une logique à son enfant, offrant une manière flexible de partager des fonctionnalités.

Conseil : Les slots à portée peuvent être utilisés pour transmettre les données et la logique du composant parent au composant enfant. Cela permet au composant enfant de restituer son contenu en fonction des données et de la logique fournies par le parent.

Exemple :

<!-- DataFetcher.vue -->
<template>
  <div>
    <div v-if="loading">Chargement...</div>
    <div v-else-if="error">Erreur : {{ 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(`Erreur HTTP ! statut : ${response.status}`);
    }
    data.value = await response.json();
  } catch (e) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
});
</script>
<!-- Utilisation dans un composant -->
<DataFetcher url="/api/items">
  <template v-slot="{ data, loading, error }">
    <div v-if="loading">Chargement des éléments...</div>
    <div v-else-if="error">Erreur : {{ error }}</div>
    <div v-else>
      <ul>
        <li v-for="item in data" :key="item.id">{{ item.name }}</li>
      </ul>
    </div>
  </template>
</DataFetcher>

Conseils et bonnes pratiques généraux

1. Ref vs. Reactive

Comprenez les différences entre ref et reactive et choisissez celui qui convient à votre cas d'usage ^1. ref est utilisé pour les valeurs primitives, tandis que reactive est utilisé pour les objets et les tableaux.

Conseil : Utilisez ref pour les valeurs primitives et reactive pour les objets et les tableaux. Cela aide au système de réactivité de Vue.

Exemple :

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

export default {
  setup() {
    const count = ref(0); // ref pour une valeur primitive
    const user = reactive({ name: 'John', age: 30 }); // reactive pour un objet
    return { count, user };
  }
}
</script>

2. Gestion d'état efficace

Structurez l'état dans vos applications efficacement ^1. C'est crucial pour gérer le flux de données et assurer que votre application reste gérable à mesure qu'elle grandit. Envisagez les bibliothèques de gestion d'état appropriées pour les applications plus grandes.

Conseil : Envisagez d'utiliser Pinia ou Vuex pour la gestion d'état dans les plus grandes applications. Ces bibliothèques fournissent une gestion d'état centralisée et facilitent la gestion de l'état complexe de l'application.

Exemple : (Illustratif - l'implémentation réelle dépend de la bibliothèque de gestion d'état choisie)

// Exemple Pinia (Conceptuel)
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. Utilisez des guillemets pour regarder les valeurs imbriquées

Regardez les valeurs imbriquées directement en utilisant des guillemets ^2. Cela vous permet d'observer les modifications apportées à des propriétés spécifiques dans un objet, déclencher des mises à jour lorsque ces propriétés changent. *Conseil : Utilisez les guillemets dans l'option watch pour regarder les propriétés imbriquées d'un objet. C'est une manière plus efficace et spécifique de surveiller les changements.

Exemple :

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

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

    watch(() => data.value.user.name, (newName) => {
      console.log('Nom changé:', newName);
    });

    return { data };
  }
}
</script>

4. Le modèle de condition d'extraction

Divisez les composants en fonction de la logique conditionnelle ^3. Cela améliore la lisibilité et la maintenabilité en séparant les préoccupations. Cela rend les composants plus faciles à comprendre et à tester.

Conseil : Si un composant a une logique conditionnelle complexe, envisagez de la diviser en composants plus petits. Cela crée des composants plus ciblés et gérables.

Exemple :

<!-- Avant : Logique conditionnelle complexe -->
<template>
  <div>
    <div v-if="isLoading">Chargement...</div>
    <div v-else-if="error">Erreur : {{ error }}</div>
    <div v-else>
      <UserList v-if="users.length > 0" :users="users" />
      <NoUsersMessage v-else />
    </div>
  </div>
</template>
<!-- Après : Utilisation de composants extraits -->
<template>
  <LoadingIndicator v-if="isLoading" />
  <ErrorMessage v-if="error" :message="error" />
  <UsersDisplay v-else :users="users" />
</template>

5. 6 raisons de diviser les composants

Divisez les composants en pièces plus petites pour améliorer l'organisation du code et la réutilisabilité ^3. Cela augmente la lisibilité, la maintenabilité et la testabilité de votre code.

Conseil : Les petits composants sont plus faciles à comprendre, à tester et à maintenir. Ils favorisent également la réutilisabilité, car vous pouvez les utiliser dans de nombreuses parties de votre application.

Exemple :

<!-- Avant : Composant monolithique -->
<template>
  <div>
    <Header />
    <Sidebar />
    <MainContent />
    <Footer />
  </div>
</template>
<!-- Après : Utilisation de composants extraits -->
<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. Ne pas remplacer le CSS du composant

Évitez de modifier directement le CSS d'un composant de l'extérieur du composant ^2. Cela peut entraîner un comportement inattendu et rendre difficile la maintenance des styles de votre application. Encapsulez les styles dans le composant lui-même.

Conseil : Utilisez les props ou les slots pour personnaliser l'apparence d'un composant. Cela permet un style contrôlé et évite les conflits de style inattendus.

Exemple :

<!-- 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 {
  /* Styles de bouton de base */
}
.primary {
  /* Styles de bouton primaire */
}
.secondary {
  /* Styles de bouton secondaire */
}
</style>