Améliorer l'architecture d'une application Vue grâce à la composition API
En tant que développeur full stack, je cherche souvent à appliquer côté frontend, les meilleures pratiques du développement backend (et vice versa).
C’est ce que nous allons voir dans cet article.
Comment la composition API peut améliorer l’architecture d’une application ?
Chez Deepki, nous utilisons Vue.js comme framework frontend.
Initialement nous utilisions Vue 2 basé sur l’options API qui impose une façon de structurer le code de ses composants à partir d’options déclaratives (data
, methods
, mounted
…).
Cette approche est relativement simple et permet de s’affranchir de la compréhension de la réactivité en profondeur.
Par exemple, voici la partie javascript d’un composant en options API :
<script>
export default {
data() {
return {
name: 'Bob',
}
},
methods: {
sayHello() {
console.log(`Hello ${this.name}`)
},
},
mounted() {
this.sayHello()
},
}
</script>
La nouvelle version Vue 3 embarque un nouveau concept : la composition API. Et depuis, nous migrons notre base de code pour respecter cette nouvelle pratique.
Mais, c’est quoi la composition API ?
C’est un ensemble d’APIs qui permet d’importer des fonctions plutôt que déclarer des options comme c’était le cas avec l’options API.
- Reactivity : Créer des états réactifs (
ref
,computed
,watch
) - Lifecycle hooks : Se brancher au cycle de vie du composant (
onMounted
,onUnmounted
) - Dependency injection : Gérer les injections de dépendances de manière réactive (
provide
,inject
)
Pour reprendre l’exemple précédent, voici sa version en composition API en utilisant la syntaxe script setup :
<script setup>
const name = ref('Bob')
const sayHello = () => console.log(`Hello ${name.value}`)
onMounted(() => {
sayHello()
})
</script>
Et… À quoi ça sert ?
Il y a de nombreux avantages à utiliser la composition API plutôt que l’options API. Le principal est la réutilisation de code sous la forme de fonctions composables.
Cela permet également de séparer le code de gros composants pour en augmenter la lisibilité.
Mais plutôt que de se limiter à la théorie, prenons un exemple concret.
Imaginons une fonctionnalité du type « todo list », où l’utilisateur peut ajouter de nouvelles tâches ou bien supprimer des tâches existantes. Les tâches seraient enregistrées et listées grâce à une API côté serveur.
Dans le cadre de cet exemple, nous nous limiterons à la partie script
du composant vue, mais pour vous faire une idée, voici à quoi cela pourrait ressembler :
Et voici le code TypeScript correspondant :
<script lang="ts">
import axios from "axios"
import { defineComponent } from "vue"
export default defineComponent({
mounted() {
this.todoList = this.getTodoList()
},
data() {
return {
todoList: [],
current: { title: "", content: "" },
displayCurrent: false
}
},
computed: {
canAddTodo() {
return this.current.title !== "" && this.current.content !== ""
}
},
methods: {
async getTodoList() {
return await axios.get("/api/todo/list")
},
async createTodo(todo) {
await axios.post("/api/todo/create", todo)
},
async deleteTodo(todo) {
await axios.post("/api/todo/delete", todo)
},
onChange(newValue, type) {
if (type === "title") {
this.current.title = newValue
}
if (type === "content") {
this.current.content = newValue
}
},
onCancelTodo() {
this.toggleDisplayCurrent()
this.current = {
title: "",
content: ""
}
},
addNewTodo() {
this.todoList.push(this.current)
this.createTodo(this.current)
this.current = {
title: "",
content: ""
}
},
removeTodo(index) {
const deletedTodo = this.todoList.splice(index, 1)
this.deleteTodo(deletedTodo[0])
},
toggleDisplayCurrent() {
this.displayCurrent = !this.displayCurrent
}
}
})
</script>
À l’initialisation, le composant appelle le serveur pour lister les tâches existantes et les afficher.
L’objectif est d’améliorer la qualité de ce code.
Grâce à la composition API et au remaniement de code, nous allons pouvoir :
- Remanier pour y extraire des cas d’usages et les découpler au maximum
- Améliorer les noms donnés aux fonctions et aux variables pour gagner en lisibilité
La première étape nous permet de rassembler les méthodes
, computed
et références
qui sont liées aux mêmes fonctionnalités, comme par exemple, regrouper les fonctions d’appel au serveur et les extraire dans un autre fichier client.ts
:
export const useTodoListClient = (baseUrl = "/api/todo/") => {
const url = ref<string>(baseUrl);
const getTodoList = async (): Promise<TodoItem[]> => {
return await axios.get(`${url.value}/list`)
}
const createTodo = (todo: TodoItem): void => {
await axios.post(`${url.value}/create`, todo)
}
const deleteTodo = (todo: TodoItem): void => {
await axios.post(`${url.value}/delete`, todo)
}
return {
getTodoList,
createTodo,
deleteTodo
}
}
Cela permet de séparer le composant du code permettant d’appeler le serveur, et éviter de devoir modifier ce composant dans le cas d’une évolution côté serveur. Ainsi seul le fichier client.ts
sera impacté.
Ensuite nous pouvons extraire la gestion de la todoList
dans un fichier externe todo.ts
.
Ce fichier fournit une interface pour mettre à jour la liste et fait appel à client.ts
pour interagir avec le serveur.
L’interface est composée de 3 fonctions :
fetchTodoList
: Pour récupérer la liste de tâchesaddNewTodo
: Pour ajouter une nouvelle tâcheremoveTodo
: Pour supprimer une tâche existante
import { computed, onMounted, ref } from "vue"
import { useTodoListClient } from "./client"
export interface TodoItem {
title: string
content: string
}
export const useTodo = () => {
const client = useTodoListClient()
const todoList = ref<TodoItem[]>([])
onMounted(async () => {
await fetchTodoList()
})
const fetchTodoList = async (): void => (todoList.value = await client.getTodoList())
const addNewTodo = (todo: TodoItem): void => {
todoList.value.push(todo)
client.createTodo(todo)
}
const removeTodo = (index: number): void => {
const deletedTodo: TodoItem[] = todoList.value.splice(index, 1)
client.deleteTodo(deletedTodo[0])
}
return {
todoList: computed(() => todoList.value),
fetchTodoList,
addNewTodo,
removeTodo
}
}
Pour le composant, la todoList
n’est plus directement modifiable (grâce à l’utilisation de la computed
) mais uniquement via cette nouvelle interface, ainsi on évite tous potentiels futurs bugs liés aux changements sans précaution de cette liste.
Enfin, nous pouvons gagner en lisibilité en :
- créant une fonction
resetNewTodo
pour éviter la duplication de code. - renommant la référence
current
endraftTodo
- renommant la référence
displayCurrent
endisplayTodoForm
- renommant la fonction
toggleDisplayCurrent
entoggleDisplay
- renommant la computed
canAddTodo
enisDraftTodoComplete
Ainsi le code du composant est le suivant :
<script setup lang="ts">
import { computed, ref } from "vue"
import { useTodo, TodoItem } from "./todo"
const { todoList, fetchTodoList, addNewTodo, removeTodo } = useTodo()
const displayTodoForm = ref(false)
const toggleDisplay = (): void => {
displayTodoForm.value = !displayTodoForm.value
}
const draftTodo = ref<TodoItem>({
title: "",
content: ""
})
const onChange = (newValue: string, type: string): void => {
if (type === "title") {
draftTodo.value.title = newValue
}
if (type === "content") {
draftTodo.value.content = newValue
}
}
const isDraftTodoComplete = computed<boolean>(
() => draftTodo.value.title !== "" && draftTodo.value.content !== ""
)
const onValidateNewTodo = (): void => {
addNewTodo(draftTodo.value)
resetDraftTodo()
}
const onCancelNewTodo = (): void => {
toggleDisplay()
resetDraftTodo()
}
const resetDraftTodo = (): void => {
draftTodo.value = {
title: "",
content: ""
}
}
</script>
Bien évidemment ce code n’est pas parfait, et peut-être amélioré. Mais l’idée est de faciliter les évolutions et éviter qu’elles soient coûteuses en termes de temps et d’effort.
⚠️ Ici j’ai uniquement parlé du découpage via la composition, mais il faut garder en tête que le découpage en composants reste l’un des principaux atout des frameworks comme Vue.
Ainsi bien découper son code par fonctionnalité grâce à la composition API ne remplace aucunement le découpage par composant. Au contraire, ces 2 méthodes se complémentent parfaitement et vous permettront de rendre le code de vos applications plus simple et plus lisible.
Conclusion
Depuis que j’ai découvert la flexibilité et la puissance de la composition API offerte par Vue 3, je ne peux pas m’en passer.
Finalement, voici les avantages principaux identifiés :
- Meilleure organisation de la logique interne des composants
- Partage de fonctionnalités transverses entre composants (cela évite la duplication de code et l’utilisation des mixins)
- Découpage de gros composants avec la possibilité d’ajouter des couches d’abstractions pour en améliorer l’architecture
- Meilleur support pour Typescript
Sources
Cette entrée a été publiée dans programmation avec comme mot(s)-clef(s) architecture, frontend, vuejs, composition api
Les articles suivant pourraient également vous intéresser :
- La bonne et la mauvaise review par Sébastien Bizet
- Dark mode vs Light mode : accessibilité et éco-conception par Jean-Baptiste Bergy
- Principes SOLID et comment les appliquer en Python par Mariana ROLDAN VELEZ
- Pydantic, la révolution de python ? par Pablo Abril
- Comment utiliser les fixtures pytest en autouse tout en maîtrisant ses effets de bord ? par Amaury Boin
Postez votre commentaire :
Votre commentaire a bien été envoyé ! Il sera affiché une fois que nous l'aurons validé.
Vous devez activer le javascript ou avoir un navigateur récent pour pouvoir commenter sur ce site.