Un refactor interne pour la croissance future de notre blog
Fait partie de la série Créer un Blog Complet avec VitePress et Vue.js de Zéro
Notre blog a été établi, mettant l'accent principalement sur le contenu et un bon fonctionnement.
Mais nous visons à faire plus. Nous avons envisagé d'ajouter un système de commentaires. Une fois que vous avez une API, de nombreuses possibilités s'offrent à vous. De plus, nous avons beaucoup appris depuis le lancement de ce blog, et nous souhaitons appliquer ces connaissances pour le rendre plus maintenable et scalable, surtout pour une croissance future, même si ce refactor interne peut ne pas sembler l'améliorer immédiatement.
Ce refactor se concentre sur deux points principaux :
- Ajouter Tailwind Variants pour faciliter la réutilisation des composants
- Passer d'une structure de dossiers basée sur le type à une structure de dossiers basée sur les fonctionnalités
Laissez-moi expliquer pourquoi nous voulons faire cela.
Pourquoi utiliser Tailwind Variants ?
Commençons par comprendre le problème auquel nous faisons face. Actuellement, notre base de code a deux boutons :
ButtonPrimary.vue
: un bouton principalButtonSecondary.vue
: un bouton secondaire
Le problème est que notre site est simple et n'a que ces deux boutons. Mais que se passe-t-il si nous voulons ajouter un bouton tertiaire, un bouton avec une bordure au lieu d'une couleur de fond, ou un bouton de taille différente ? Pire encore, que se passe-t-il si nous voulons que notre Button
soit un vrai button
au lieu d'une simple balise a
? Mettre à jour chaque composant individuellement serait un cauchemar à maintenir, et des changements mineurs nécessiteraient tellement de temps, entraînant une perte de productivité—souvent la raison pour laquelle les projets échouent.
Pour résoudre cela, nous pouvons créer un unique composant regroupant tous nos boutons et c'est plus facile qu'il n'y paraît. Considérez les composants ButtonPrimary.vue
et ButtonSecondary.vue
:
<script lang="ts" setup>
defineProps<{
href: string
}>()
</script>
<template>
<a
:href
class="border-4 border-black bg-[var(--color-yellow)] px-8 py-4 shadow-[4px_4px_0_black] transition duration-150 ease-linear hover:bg-[#ffdf1b] hover:shadow-[6px_6px_0_black] hover:-translate-x-[0.125rem] hover:-translate-y-[0.125rem]"
>
<slot />
</a>
</template>
<script lang="ts" setup>
defineProps<{
href: string
}>()
</script>
<template>
<a
:href
class="inline-block border-4 border-black bg-[var(--color-purple)] px-8 py-4 text-white shadow-[4px_4px_0_black] transition duration-150 ease-linear hover:bg-[#828cd2] hover:shadow-[6px_6px_0_black] hover:-translate-x-[0.125rem] hover:-translate-y-[0.125rem]"
>
<slot />
</a>
</template>
Ils sont presque identiques ! La seule différence est la couleur : bg-[var(--color-yellow)] hover:bg-[#ffdf1b]
pour le premier, et bg-[var(--color-purple)] hover:bg-[#828cd2]
pour le second. Cela peut être fusionné en un seul composant :
<script lang="ts" setup>
const props = defineProps<{
href: string
color: string
}>()
const color = computed(() => {
switch (props.color) {
case 'primary':
return 'bg-[var(--color-yellow)] hover:bg-[#ffdf1b]'
case 'secondary':
return 'bg-[var(--color-purple)] hover:bg-[#828cd2]'
default:
throw new Error('Couleur inconnue')
}
})
</script>
<template>
<a
:href
class="inline-block border-4 border-black px-8 py-4 text-white shadow-[4px_4px_0_black] transition duration-150 ease-linear hover:shadow-[6px_6px_0_black] hover:-translate-x-[0.125rem] hover:-translate-y-[0.125rem]" :class="[color]"
>
<slot />
</a>
</template>
C'est un bon départ, mais ce n'est pas encore scalable. Notre bouton pourrait varier en taille, en couleur, avoir plusieurs états... Gérer cela dans plusieurs propriétés calculées n'est pas idéal. C'est là que Tailwind Variants entre en jeu.
Une approche puissante est de découpler le thème, pas le style, du composant et du template. Avec Tailwind Variants, nous pouvons définir un thème indépendant pour un composant adaptable à différents contextes de manière déclarative :
import { tv } from 'tailwind-variants'
const button = tv({
base: 'inline-block border-4 border-black px-8 py-4 text-white shadow-[4px_4px_0_black] transition duration-150 ease-linear hover:shadow-[6px_6px_0_black] hover:-translate-x-[0.125rem] hover:-translate-y-[0.125rem]',
variants: {
color: {
primary: 'bg-[var(--color-yellow)] hover:bg-[#ffdf1b]',
secondary: 'bg-[var(--color-purple)] hover:bg-[#828cd2]',
},
},
})
Avec cet exemple, vous devriez commencer à comprendre la puissance de cette approche. Le style de base, partagé entre les variantes, est défini de manière centrale. Ensuite, nous définissons des variantes, comme la couleur. D'autres propriétés, comme la taille, peuvent également être ajoutées ou des variantes composées définies pour des styles spécifiques, comme lorsque le bouton est principal et petit.
Réécrivons notre composant Button.vue
:
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
const button = tv({
base: 'border-4 border-black px-8 py-4 shadow-[4px_4px_0_black] transition duration-150 ease-linear hover:-translate-x-[0.125rem] hover:-translate-y-[0.125rem] hover:shadow-[6px_6px_0_black]',
variants: {
color: {
primary: 'bg-[var(--color-yellow)] hover:bg-[#ffdf1b]',
secondary: 'text-white bg-[var(--color-purple)] hover:bg-[#828cd2]',
},
},
defaultVariants: {
color: 'primary',
},
})
type ButtonVariants = VariantProps<typeof button>
export interface ButtonProps {
href: string
label: string
color?: ButtonVariants['color']
class?: any
}
export interface ButtonEmits {}
export interface ButtonSlots {}
</script>
<script lang="ts" setup>
const props = defineProps<ButtonProps>()
defineEmits<ButtonEmits>()
defineSlots<ButtonSlots>()
const ui = computed(() => button({ class: props.class, color: props.color }))
</script>
<template>
<a :href="props.href" :class="ui">
{{ props.label }}
</a>
</template>
Un aspect intéressant est l'extraction des variantes avec TypeScript et leur application aux props. Le type ButtonVariants
inclut toutes les variantes de Button
en fonction du thème button
. Donc, tout changement de thème se reflète automatiquement dans le composant Button
.
Vous vous demandez probablement : comment Tailwind Variants sait-il quelle variante utiliser ? Regardez ce code :
const ui = computed(() => button({ class: props.class, color: props.color }))
La variable button
est une fonction qui prend un objet avec des variantes comme arguments. Ainsi, les variantes sont calculées à l'exécution en fonction des props et du thème prédéfini.
Nous pouvons utiliser notre composant Button
de cette manière :
<Button href="https://soubiran.dev" label="Bouton Principal" />
<Button href="https://soubiran.dev" label="Bouton Secondaire" color="secondary" />
Automatiquement, le composant Button
applique le style correct en fonction de la prop color
.
Pourquoi passer à une structure de dossiers basée sur les fonctionnalités ?
Actuellement, notre base de code est dans .vitepress/theme
, divisée en dossiers :
- .vitepress/theme
- components
- composables
- pages
- styles
- types
Par Type
Nous pourrions avoir des dossiers supplémentaires comme directives
, utils
, queries
, ou mutations
. Cette structure, basée sur les types de fichiers, place tous les composants dans un dossier, quelle que soit leur utilisation, ce qui n'est pas idéal.
Il est difficile d'identifier les fonctionnalités clés du blog à partir de la structure des dossiers. Avec une seule fonctionnalité, cela reste gérable, mais au fur et à mesure que le blog grandit, localiser des composants ou des pages spécifiques devient un défi. D'où l'avantage d'une structure de dossiers basée sur les fonctionnalités.
Cette approche regroupe plusieurs structures de dossiers par type par fonctionnalité. Il est facile de trouver des composants spécifiques, et lorsque vous travaillez sur une fonctionnalité, tous les fichiers associés sont facilement accessibles. C'est un moyen efficace de gérer et d'évoluer un projet.
Dans notre cas, les principales fonctionnalités sont le blog et les projets. La structure pourrait ressembler à ceci :
- .vitepress/theme
- blog
- components
- composables
- pages
- types
- projects
- components
- composables
- pages
- types
Par Fonctionnalité
De cette façon, les fonctionnalités clés du blog sont claires et les fichiers associés sont facilement trouvables. L'ajout de nouvelles fonctionnalités implique la création d'un nouveau dossier et l'inclusion de fichiers liés.
Un dossier manquant ici est common
ou shared
. Il contient des fichiers partagés entre les fonctionnalités. Par exemple, le composant Button
partagé par le blog et les projets devrait être placé ici.
- .vitepress/theme
- blog
- components
- composables
- pages
- types
- projects
- components
- composables
- pages
- types
- common
- components
- composables
- styles
- types
Par Fonctionnalité avec Commun
Note
Je l'ai nommé common
, mais vous pouvez choisir n'importe quel nom qui transmet la nature partagée des fichiers ne relevant pas d'une fonctionnalité spécifique.
Restructurer la base de code
Outre les deux points principaux, de petits changements de la base de code amélioreront la lisibilité et réduiront la répétition de code, ce qui est crucial pour maintenir la cohérence du style du blog.
Tout d'abord, nous allons consolider tout ce qui est lié à Page
. Chaque composant de page partage la même structure d'enveloppe, de titre et de pied de page. Nous pouvons facilement extraire chaque partie en trois composants :
Page.vue
<script lang="ts">
const page = tv({
base: 'space-y-16',
})
export interface PageProps {
class?: any
}
export interface PageEmits {}
export interface PageSlots {
default: (props?: object) => any
}
</script>
<script lang="ts" setup>
const props = defineProps<PageProps>()
defineEmits<PageEmits>()
defineSlots<PageSlots>()
const ui = computed(() => page({ class: props.class }))
</script>
<template>
<section :class="ui">
<slot />
</section>
</template>
PageTitle.vue
<script lang="ts">
const pageTitle = tv({
base: 'text-6xl font-semibold',
})
export interface PageTitleProps {
title: string
class?: any
}
export interface PageTitleEmits {}
export interface PageTitleSlots {}
</script>
<script lang="ts" setup>
const props = defineProps<PageTitleProps>()
defineEmits<PageTitleEmits>()
defineSlots<PageTitleSlots>()
const ui = computed(() => pageTitle())
</script>
<template>
<h1 :class="ui">
{{ props.title }}
</h1>
</template>
PageFooter.vue
<script lang="ts">
const pageFooter = tv({
base: 'flex justify-center',
})
export interface PageFooterProps {
backTo: { href: string, label: string }
class?: any
}
export interface PageFooterEmits {}
export interface PageFooterSlots {}
</script>
<script lang="ts" setup>
const props = defineProps<PageFooterProps>()
defineEmits<PageFooterEmits>()
defineSlots<PageFooterSlots>()
const ui = computed(() => pageFooter({ class: props.class }))
</script>
<template>
<div :class="ui">
<Button :href="props.backTo.href" :label="props.backTo.label" />
</div>
</template>
Ces composants simples aident à réduire la répétition de code et simplifient la création de nouvelles pages. De plus, nous pouvons regrouper chaque prose en un seul composant Prose.vue
, garantissant un style de prose de blog cohérent.
Ensuite, nous pouvons unifier ProjectCard.vue
et BlogCard.vue
en Card.vue
. Leurs seules différences sont la taille du titre, la description et le remplissage. Tailwind Variants gère efficacement ces différences :
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
const card = tv({
slots: {
base: 'group relative border-4 border-black bg-white shadow-[8px_8px_0_black] transition duration-150 ease-linear hover:-translate-x-1 hover:-translate-y-1 hover:shadow-[12px_12px_0_black]',
title: 'font-medium text-black',
description: 'text-black',
},
variants: {
size: {
sm: {
base: 'space-y-4 p-6',
title: 'text-2xl',
description: 'text-lg',
},
md: {
base: 'space-y-6 p-12',
title: 'text-4xl',
description: 'text-xl',
},
},
},
defaultVariants: {
size: 'md',
},
})
type CardVariants = VariantProps<typeof card>
export interface CardProps {
href: string
title: string
description?: string
size: CardVariants['size']
class?: any
ui?: Partial<typeof card.slots>
}
export interface CardEmits {}
export interface CardSlots {}
</script>
<script lang="ts" setup>
const props = defineProps<CardProps>()
defineEmits<CardEmits>()
defineSlots<CardSlots>()
const ui = computed(() =>
card({
size: props.size,
}),
)
</script>
<template>
<article :class="ui.base({ class: [props.class, props.ui?.base] })">
<h2 :class="ui.title({ class: props.ui?.title })">
<a :href="props.href">
{{ props.title }}
<span class="absolute inset-0" />
</a>
</h2>
<p :class="ui.description({ class: props.ui?.description })">
{{ props.description }}
</p>
</article>
</template>
Enfin, déplacez le fichier .env.example
vers le dossier src
, car VitePress définit la racine du projet dans le dossier src
.
Pour le refactor complet, reportez-vous au commit 26b1dd4 de Garabit car je ne peux pas couvrir tous les changements dans cet article.
J'espère que vous avez trouvé cet article instructif et avez appris quelque chose de nouveau. Les objectifs étaient de démontrer la puissance de Tailwind Variants avec une structure de dossier basée sur les fonctionnalités et de montrer que vous pouvez commencer simplement et grandir étape par étape au besoin. Dans notre cas, ce refactor prépare le terrain pour ajouter le système de commentaires.
Note
Tous mes projets, même les plus petits, sont basés sur à la fois Tailwind Variants et une structure de dossiers basée sur les fonctionnalités. Cette configuration améliore ma productivité et, grâce à des composants et à une structure de dossiers cohérents, me permet de passer d'un projet à l'autre sans perdre de temps à comprendre l'architecture du projet. Maintenant, créer un composant semble plus être un réflexe qu'une tâche.
Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web.
Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.
Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.
J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous—cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !
Discussions
Ajouter un commentaire
Vous devez être connecté pour accéder à cette fonctionnalité.