Portée de l'État
Lors de la conception d'une application Vue (ou en fait, de toute application basée sur des composants), il existe différents types de données qui dépendent des préoccupations auxquelles nous avons affaire et chacune a ses propres canaux de communication préférés.
-
État global : peut inclure l'utilisateur connecté, le thème actuel, etc.
-
État local : attributs de formulaire, état des boutons désactivés, etc.
Notez qu'une partie de l'état global peut se retrouver dans l'état local à un moment donné, et qu'il peut être transmis aux composants enfants comme n'importe quel autre état local, soit intégralement, soit dilué en fonction du cas d'utilisation.
Canaux de communication
Un canal est un terme général que j'utiliserai pour désigner les implémentations concrètes permettant d'échanger des données dans une application Vue.
Chaque mise en œuvre concerne un canal de communication spécifique, qui comprend :
- État global
- Parent-enfant
- Enfant-parent
- Frères et sœurs
Des préoccupations différentes sont liées à des canaux de communication différents.
Props : Direct Parent-Enfant
Le canal de communication le plus simple de Vue pour la liaison de données à sens unique.
Événements : Direct Enfant-Parent
$emit
et $on
. Le canal de communication le plus simple pour une communication directe entre l'enfant et le parent.
Ajoutée dans Vue 2.2+, et très similaire à l'API contextuelle de React, elle pourrait être utilisée comme un remplacement viable d'un bus d'événements.
À n'importe quel endroit de l'arbre des composants, un composant pourrait fournir certaines données, auxquelles n'importe quel enfant pourrait accéder par l'intermédiaire de l'ordinateur. inject
la propriété du composant.
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
}
})
Cela pourrait être utilisé pour fournir un état global à la racine de l'application, ou un état localisé dans un sous-ensemble de l'arbre.
Magasin centralisé (état global)
Vuex est un modèle de gestion des états + bibliothèque pour les applications Vue.js. Il sert de magasin centralisé pour tous les composants d'une application. l'application, avec des règles garantissant que l'état ne peut être modifié que de de manière prévisible.
Et maintenant vous demandez :
[Dois-je créer un magasin Vuex pour chaque communication mineure ?
Il brille vraiment lorsqu'il s'agit de l'état global, ce qui inclut, mais n'est pas limité à :
- les données reçues d'un backend,
- l'état global de l'interface utilisateur comme un thème,
- toute couche de persistance des données, par exemple la sauvegarde vers un backend ou l'interface avec un stockage local,
- des messages ou des notifications de toast,
- etc.
Ainsi, vos composants peuvent vraiment se concentrer sur ce qu'ils sont censés être, à savoir gérer les interfaces utilisateur, tandis que le magasin global peut gérer/utiliser la logique commerciale générale et offrir une API claire par le biais de getters et actions .
Cela ne veut pas dire que vous ne pouvez pas l'utiliser pour la logique du composant, mais personnellement, j'étendrais cette logique à un espace de nom Module Vuex avec seulement l'état global nécessaire de l'interface utilisateur.
Pour éviter d'avoir à gérer un grand désordre de tout ce qui se trouve dans un état global, voir la fonction Structure de l'application recommandations.
Réf. et les méthodes : Cas limites
Malgré l'existence des props et des événements, il est parfois nécessaire d'accéder directement à un composant enfant en JavaScript. besoin d'accéder directement à un composant enfant en JavaScript.
Il s'agit uniquement d'un trappe d'évacuation pour la manipulation directe des enfants - vous devez éviter d'accéder $refs
à partir de modèles ou de propriétés calculées.
Si vous utilisez souvent des références et des méthodes enfant, il est sans doute temps d'adopter les mesures suivantes relever l'État ou envisagez les autres moyens décrits ici ou dans les autres réponses.
Similaire à $root
le $parent
peut être utilisée pour accéder à l'instance parent à partir d'un enfant. Il peut être tentant d'y recourir comme une alternative paresseuse au passage de données avec une prop.
Dans la plupart des cas, atteindre le parent rend votre application plus difficile à déboguer et à comprendre, en particulier si vous modifiez les données dans le parent. difficile à déboguer et à comprendre, en particulier si vous modifiez des données dans le parent. En regardant ce composant plus tard, il sera très difficile de comprendre d'où vient cette mutation. difficile de comprendre d'où vient cette mutation.
Vous pourriez en fait naviguer dans l'ensemble de l'arborescence en utilisant $parent
, $ref
ou $root
Mais ce serait comme si tout était global et deviendrait probablement un spaghetti impossible à maintenir.
Bus d'événements : État local global/distant
Voir La réponse de @AlexMA pour obtenir des informations actualisées sur le modèle de bus d'événements.
Dans le passé, le modèle consistait à passer des accessoires partout, du haut en bas jusqu'aux composants enfants profondément imbriqués, sans qu'aucun autre composant n'en ait besoin entre les deux. À utiliser avec parcimonie pour des données soigneusement sélectionnées.
Faites attention : La création ultérieure de composants qui se lient au bus d'événements sera liée plus d'une fois, ce qui entraînera le déclenchement de plusieurs gestionnaires et des fuites. Personnellement, je n'ai jamais ressenti le besoin d'un bus d'événements dans toutes les applications à page unique que j'ai conçues dans le passé.
L'exemple suivant démontre comment une simple erreur conduit à une fuite où le Item
se déclenche toujours, même s'il est retiré du DOM.
// A component that binds to a custom 'update' event.
var Item = {
template: `<li>{{text}}</li>`,
props: {
text: Number
},
mounted() {
this.$root.$on('update', () => {
console.log(this.text, 'is still alive');
});
},
};
// Component that emits events
var List = new Vue({
el: '#app',
components: {
Item
},
data: {
items: [1, 2, 3, 4]
},
updated() {
this.$root.$emit('update');
},
methods: {
onRemove() {
console.log('slice');
this.items = this.items.slice(0, -1);
}
}
});
<script src="https://unpkg.com/vue@2.5.17/dist/vue.min.js"></script>
<div id="app">
<button type="button" @click="onRemove">Remove</button>
<ul>
<item v-for="item in items" :key="item" :text="item"></item>
</ul>
</div>
N'oubliez pas de supprimer les écouteurs dans le destroyed
le crochet du cycle de vie.
Types de composants
_Avis de non-responsabilité : les éléments suivants "Composants "conteneurs" ou "présentationnels n'est qu'un moyen parmi d'autres de structurer un projet et il existe désormais de multiples alternatives, comme la nouvelle Composition API qui pourrait effectivement remplacer les "conteneurs d'applications spécifiques" que je décris ci-dessous._
Pour orchestrer toutes ces communications, pour faciliter la réutilisation et les tests, nous pouvons considérer les composants comme deux types différents.
- Conteneurs spécifiques aux applications
- Composants génériques/présentationnels
Encore une fois, cela ne signifie pas qu'un composant générique doit être réutilisé ou qu'un conteneur spécifique à une application ne peut pas être réutilisé, mais ils ont des responsabilités différentes.
Conteneurs spécifiques aux applications
Note : voir le nouveau Composition API comme alternative à ces conteneurs.
Ce sont de simples composants Vue qui enveloppent d'autres composants Vue (conteneurs génériques ou spécifiques à une application). C'est là que la communication avec le magasin Vuex doit avoir lieu et ce conteneur doit communiquer par d'autres moyens plus simples comme les props et les écouteurs d'événements.
Ces conteneurs pourraient même ne comporter aucun élément DOM natif et laisser les composants génériques s'occuper de la mise en page et des interactions avec l'utilisateur.
portée en quelque sorte events
ou stores
visibilité des composants frères et sœurs
C'est là que se fait le cadrage. La plupart des composants ne connaissent pas le magasin et ce composant devrait (principalement) utiliser un module de magasin avec un espace de nom et un ensemble limité de fonctions getters
et actions
appliqué avec les Aides à la liaison Vuex .
Composants génériques/présentationnels
Ils doivent recevoir leurs données des props, effectuer des changements sur leurs propres données locales et émettre des événements simples. La plupart du temps, ils ne devraient même pas savoir qu'un magasin Vuex existe.
On pourrait aussi les appeler des conteneurs, car leur seule responsabilité pourrait être de distribuer des informations à d'autres composants de l'interface utilisateur.
Communication entre frères et sœurs
Donc, après tout cela, comment communiquer entre deux composants frères et sœurs ?
Il est plus facile de comprendre avec un exemple : disons que nous avons un champ de saisie et que ses données doivent être partagées à travers l'application (frères et sœurs à différents endroits dans l'arbre) et persistées avec un backend.
Problèmes de mélange
En commençant par le scénario le plus défavorable notre composant mélangerait présentation et entreprise logique.
// MyInput.vue
<template>
<div class="my-input">
<label>Data</label>
<input type="text"
:value="value"
:input="onChange($event.target.value)">
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
value: "",
};
},
mounted() {
this.$root.$on('sync', data => {
this.value = data.myServerValue;
});
},
methods: {
onChange(value) {
this.value = value;
axios.post('http://example.com/api/update', {
myServerValue: value
});
}
}
}
</script>
Bien qu'elle puisse sembler parfaite pour une application simple, elle présente de nombreux inconvénients :
- Utilise explicitement l'instance globale axios
- API codée en dur dans l'interface utilisateur
- Étroitement couplé au composant racine (modèle de bus d'événements)
- Plus difficile de faire des tests unitaires
Séparation des préoccupations
Pour séparer ces deux préoccupations, nous devons envelopper notre composant dans un conteneur spécifique à l'application et conserver la logique de présentation dans notre composant d'entrée générique.
Avec le modèle suivant, on peut :
- Facilement tester chaque préoccupation avec des tests unitaires
- Modifier l'API sans aucun impact sur les composants
- Configurer les communications HTTP comme vous le souhaitez (axios, fetch, ajout de middlewares, tests, etc.).
- Réutiliser le entrée composant n'importe où (couplage réduit)
- Réagir aux changements d'état à partir de n'importe quel endroit de l'application grâce aux liaisons avec le magasin global.
- etc.
Notre composant d'entrée est maintenant réutilisable et ne connaît pas le backend ni les frères et sœurs.
// MyInput.vue
// the template is the same as above
<script>
export default {
props: {
initial: {
type: String,
default: ""
}
},
data() {
return {
value: this.initial,
};
},
methods: {
onChange(value) {
this.value = value;
this.$emit('change', value);
}
}
}
</script>
Notre conteneur spécifique à l'application peut maintenant servir de pont entre la logique métier et la communication de la présentation.
// MyAppCard.vue
<template>
<div class="container">
<card-body>
<my-input :initial="serverValue" @change="updateState"></my-input>
<my-input :initial="otherValue" @change="updateState"></my-input>
</card-body>
<card-footer>
<my-button :disabled="!serverValue || !otherValue"
@click="saveState"></my-button>
</card-footer>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { NS, ACTIONS, GETTERS } from '@/store/modules/api';
import { MyButton, MyInput } from './components';
export default {
components: {
MyInput,
MyButton,
},
computed: mapGetters(NS, [
GETTERS.serverValue,
GETTERS.otherValue,
]),
methods: mapActions(NS, [
ACTIONS.updateState,
ACTIONS.saveState,
])
}
</script>
Puisque le magasin Vuex actions s'occupe de la communication avec le backend, notre conteneur n'a pas besoin de connaître axios et le backend.
2 votes
$emit
combiné avecv-model
pour émuler.sync
. je pense que vous devriez suivre la voie Vuex.