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ération | Un seul usePosts() est réutilisé partout |
| Le chargement des données et la logique d'interface utilisateur sont mélangés | La 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 fichiers | Modifiez 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éveloppe | Le 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ègle | Description | Exemple |
|---|---|---|
withusePréfixe | Savoir au premier coup d'œil qu'il s'agit d'un Composable | usePosts, useDarkMode |
Placer danscomposables/répertoire | Facile à trouver et à maintenir | src/composables/usePosts.js |
| Retourner des données réactives | Retourner ref ou reactive, garder l'appelant réactif | return { articles, isLoading } |
| Une fonction ne fait qu'une seule chose | Responsabilité unique, facile à composer | usePosts 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
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
<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
<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
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
<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 ? '☀`Light' : '☾ Dark' }}`
</button>
</nav>
</header>
</template>
Combinez avec les variables CSS globales pour implémenter le changement de thème
Exemple
/* 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ées | Opaque, vous ne savez pas d’où viennent les propriétés | Obtention explicite par déstructuration depuis le return, la source est claire au premier coup d’œil |
| Conflit de nommage | Les propriétés de même nom issues de plusieurs Mixins s’écrasent mutuellement | Vous décidez des noms de variables, aucun conflit n’existe |
| Inférence de type | Prise en charge limitée de TypeScript | Prise en charge native de TypeScript, inférence de type automatique |
| Organisation de la logique | Catégorisé par options (data/methods/computed) | Catégorisé par fonctionnalité, un composable par fichier |
| Réutilisation de la logique | Les mixins sont difficiles à combiner et ont tendance à créer des dépendances implicites | Les composables peuvent s'appeler mutuellement, composition flexible |