Composable — Encapsulation de logique réutilisable

Dans ce chapitre, vous apprendrez la bonne pratique de Vue3 — utiliser des fonctions composables pour encapsuler la logique réutilisable, rendant le code plus propre et plus facile à maintenir.


Qu'est-ce qu'un Composable ?

À mesure que les fonctionnalités augmentent, la logique JS à l'intérieur des composants s'allonge. La page d'accueil et la page de détail doivent toutes deux charger les données des articles, donc le code est dupliqué deux fois.

Composable (fonction composable)Elle extrait la logique réutilisable dans des fonctions JS indépendantes, et tout composant qui en a besoin peut l'appeler à cet endroit.

Son essence estune fonction ordinaire qui renvoie des données réactives et des méthodes.。


Pourquoi avez-vous besoin de Composable ?

Comparez la structure du code avant et après le changement :

Sans Composable (avant)Avec Composable (après)
Chaque composant écrit sa propre logique de récupérationUn seul usePosts() est réutilisé partout
Le chargement des données et la logique d'interface utilisateur sont mélangésLa logique de données est indépendante, les composants ne s'occupent que de l'UI
Modifier une partie de la logique nécessite de modifier plusieurs fichiersModifiez un seul endroit, tous les composants se mettent à jour de manière synchrone
Le code des composants gonfle rapidement à mesure que l'activité se développeLe code du composant a une responsabilité unique, et sa taille est contrôlable

Si vous avez utilisé les Mixins de Vue2, un Composable peut être compris comme une alternative plus claire et plus sûre — il n'a aucun conflit de nommage, et la source des données est transparente.


Conventions de nommage et règles

RègleDescriptionExemple
withusePréfixeSavoir au premier coup d'œil qu'il s'agit d'un ComposableusePosts, useDarkMode
Placer danscomposables/répertoireFacile à trouver et à maintenirsrc/composables/usePosts.js
Retourner des données réactivesRetourner ref ou reactive, garder l'appelant réactifreturn { articles, isLoading }
Une fonction ne fait qu'une seule choseResponsabilité unique, facile à composerusePosts ne gère que les données, useDarkMode ne gère que les thèmes

usePosts — Encapsulation de la logique des données des articles

Ce composable est responsable de :Chargement des données + filtrage par catégorie + filtrage par recherche。

Exemple

// Chemin du fichier : src/composables/usePosts.js
import { ref, computed, onMounted } from 'vue'

export function usePosts() {
  const articles = ref([])           // Toutes les données des articles
  const isLoading = ref(true)        // État de chargement
  const error = ref(null)            // Message d'erreur
  const activeCategory = ref('All') // Catégorie actuellement sélectionnée
  const keyword = ref('')            // Mot-clé de recherche

  // Extraire toutes les catégories (dédoublonner)
  const categories = computed(() => {
    const cats = articles.value.map(a => a.category)
    return ['Tous', ...new Set(cats)]
  })

  // Articles filtrés : d'abord par catégorie, puis par mot-clé
  const filteredArticles = computed(() => {
    let result = articles.value

    // Filtrer par catégorie
    if (activeCategory.value !== 'All') {
      result = result.filter(a => a.category === activeCategory.value)
    }

    // Filtrer par mot-clé (correspondance avec le titre ou le résumé)
    if (keyword.value.trim()) {
      const kw = keyword.value.trim().toLowerCase()
      result = result.filter(a =>
        a.title.toLowerCase().includes(kw) ||
        a.summary.toLowerCase().includes(kw)
      )
    }

    return result
  })

  // Trouver un article unique par ID
  function getArticleById(id) {
    return articles.value.find(a => a.id === id)
  }

  // Changer de catégorie
  function setCategory(cat) {
    activeCategory.value = cat
  }

  // Charger les données
  async function fetchPosts() {
    isLoading.value = true
    error.value = null
    try {
      const res = await fetch('/posts.json')
      if (!res.ok) throw new Erreur(`HTTP ${res.status}`)
      articles.value = await res.json()
    } catch (err) {
      error.value = err.message
    } finally {
      isLoading.value = false
    }
  }

  // Charger automatiquement lorsque le composant est monté
  onMounted(() => {
    fetchPosts()
  })

  // Renvoyer les données et méthodes que les composants peuvent utiliser
  return {
    articles,
    isLoading,
    error,
    activeCategory,
    keyword,
    categories,
    filteredArticles,
    getArticleById,
    setCategory,
    fetchPosts
  }
}

Maintenant, le composant de la page d'accueil devient très concis, il suffit d'appeler usePosts :

Exemple

<!-- Chemin de fichier : src/views/HomeView.vue -->
<script setup>
import { usePosts } from '../composables/usePosts.js'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'

// Obtenez toutes les données et méthodes liées aux articles avec une seule ligne de code
const {
  isLoading,
  error,
  activeCategory,
  keyword,
  categories,
  filteredArticles,
  setCategory,
  fetchPosts
} = usePosts()
</script>

<template>
  <div class="home">
    <div class="search-bar">
      <input
       v-model="keyword"
       type="text"
       placeholder="Rechercher un titre ou un résumé d'article..."
     />
    </div>

    <CategoryFilter
     :categories="categories"
     :active-category="activeCategory"
     @update-category="setCategory"
   />

    <p v-if="isLoading">Chargement...</p>
    <p v-else-if="error">Échec du chargement : {{ error }}
      <button @click="fetchPosts">Réessayer</button>
    </p>
    <p v-else-if="filteredArticles.length === 0">Aucun article correspondant</p>
    <div v-else class="article-grid">
      <BlogCard
       v-for="article in filteredArticles"
       :key="article.id"
       :id="article.id"
       :title="article.title"
       :summary="article.summary"
       :date="article.date"
       :category="article.category"
     />
    </div>
  </div>
</template>

La page de détail peut également réutiliser usePosts sans réécrire fetch. Notez que vous devez l'importer séparément depuis Vuecomputed:

Exemple

<!-- Chemin du fichier : src/views/PostView.vue -->
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { usePosts } from '../composables/usePosts.js'

const route = useRoute()
const id = Number(route.params.id)

const { isLoading, error, getArticleById } = usePosts()

// articles est une donnée réactive, computed recalculera automatiquement une fois le fetch terminé
const article = computed(() => getArticleById(id))
</script>

<template>
  <div>
    <p v-if="isLoading">Chargement...</p>
    <p v-else-if="error">Échec du chargement : {{ error }}</p>
    <div v-else-if="!article">L'article n'existe pas</div>
    <article v-else>
      <h1>{{ article.title }}</h1>
      <div v-html="article.content"></div>
    </article>
  </div>
</template>

Remarque : Si l'utilisateur accède directement à l'URL de la page de détail (par exemple, en actualisant la page), usePosts récupérera à nouveau les données. Parce que la page d'accueil et la page de détail sont des instances de composant différentes, chaque appel à usePosts exécute la récupération dans onMounted, et les données ne sont pas partagées. Si vous avez besoin de partager les mêmes données entre les pages, vous pouvez remonter l'état vers un store Pinia, ou l'appeler une fois dans un composant de niveau supérieur et le transmettre via provide / inject.


useDarkMode — Basculer le mode sombre avec une seule ligne de code

L'idée centrale du mode sombre : sur lehtmlbalise, ajoutez une classe, puis utilisez des variables CSS pour contrôler les couleurs.

Le choix de l'utilisateur est stocké dans localStorage et automatiquement restauré lors de la prochaine visite.

Exemple

// Chemin du fichier: src/composables/useDarkMode.js
import { ref, watchEffect } from 'vue'

export function useDarkMode() {
  // Lire le réglage précédent de l'utilisateur depuis localStorage (par défaut clair si aucun)
  const saved = localStorage.getItem('blog-theme')
  const isDark = ref(saved === 'dark')

  // Appliquer le thème au DOM
  function applyTheme(dark) {
    if (dark) {
      document.documentElement.classList.add('dark')
      localStorage.setItem('blog-theme', 'dark')
    } else {
      document.documentElement.classList.remove('dark')
      localStorage.setItem('blog-theme', 'light')
    }
  }

  // watchEffect s'exécute une fois immédiatement pour appliquer le thème initial
  // Ensuite, chaque fois que isDark change, synchroniser automatiquement avec le DOM
  watchEffect(() => {
    applyTheme(isDark.value)
  })

  // Basculer le mode sombre
  function toggleDark() {
    isDark.value = !isDark.value
  }

  return { isDark, toggleDark }
}

Pour l'utiliser dans NavBar, il suffit de deux lignes :

Exemple

<!-- Chemin du fichier : src/components/NavBar.vue -->
<script setup>
import { useDarkMode } from '../composables/useDarkMode.js'

const { isDark, toggleDark } = useDarkMode()
</script>

<template>
  <header class="navbar">
    <a href="/" class="logo">EXAMPLE Blog</a>
    <nav>
      <a href="/">Accueil</a>
      <button class="theme-btn" @click="toggleDark">
        {{ isDark ? '&#x2600;`Light' : '☾ Dark' }}`
      </button>
    </nav>
  </header>
</template>

Combinez avec les variables CSS globales pour implémenter le changement de thème

Exemple

/* Chemin du fichier : src/assets/main.css */
/* Mode clair (par défaut) */
:root {
  --bg-primary:    #f5f5f5;
  --bg-card:       #ffffff;
  --text-primary:  #333333;
  --text-secondary:#666666;
  --border-color:  #eeeeee;
}

/* Mode sombre (s'applique lorsque la balise html a la classe .dark) */
html.dark {
  --bg-primary:    #1a1a2e;
  --bg-card:       #16213e;
  --text-primary:  #e0e0e0;
  --text-secondary:#a0a0a0;
  --border-color:  #2a2a4a;
}

/* Référencez ces variables dans les composants, pas besoin de modifier le code d’un composant lors du changement de thème */
body {
  background: var(--bg-primary);
  color: var(--text-primary);
}

.card {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
}

À ce stade, cliquer sur le bouton bascule changera instantanément les couleurs de tout le site, et votre choix sera mémorisé après actualisation.


Comparaison avec le Mixin Vue2

FonctionnalitéMixin(Vue2)Composable(Vue3)
Source de donnéesOpaque, vous ne savez pas d’où viennent les propriétésObtention explicite par déstructuration depuis le return, la source est claire au premier coup d’œil
Conflit de nommageLes propriétés de même nom issues de plusieurs Mixins s’écrasent mutuellementVous décidez des noms de variables, aucun conflit n’existe
Inférence de typePrise en charge limitée de TypeScriptPrise en charge native de TypeScript, inférence de type automatique
Organisation de la logiqueCatégorisé par options (data/methods/computed)Catégorisé par fonctionnalité, un composable par fichier
Réutilisation de la logiqueLes mixins sont difficiles à combiner et ont tendance à créer des dépendances implicitesLes composables peuvent s'appeler mutuellement, composition flexible
Autres extensions