Fermer

juin 3, 2021

Gestion de l'état partagé dans Vue 3 —


À propos de l'auteur

Shawn Wildermuth bricole des ordinateurs et des logiciels depuis qu'il a récupéré un Vic-20 au début des années 80. En tant que MVP Microsoft depuis 2003, il est également impliqué…
En savoir plus sur
Shawn

L'écriture d'applications Vue à grande échelle peut être un défi. L'utilisation de l'état partagé dans vos applications Vue 3 peut être une solution pour réduire cette complexité. Il existe un certain nombre de solutions courantes pour résoudre l'état. Dans cet article, je vais plonger dans les avantages et les inconvénients d'approches telles que les usines, les objets partagés et l'utilisation de Vuex. Je vais également vous montrer ce qui arrive dans Vuex 5 qui pourrait changer la façon dont nous utilisons tous l'état partagé dans Vue 3.

L'état peut être difficile. Lorsque nous démarrons un simple projet Vue, il peut être simple de conserver notre état de fonctionnement sur un composant particulier :

setup() {
  let livres : Travail[] = réactif([]);

  onMounted(async () => {
    // Appel de l'API
    réponse const = wait bookService.getScienceBooks();
    si (réponse.status === 200) {
      books.splice(0, books.length, ...response.data.works);
    }
  });

  revenir {
    livres
  } ;
},

Lorsque votre projet est une seule page d'affichage de données (peut-être pour les trier ou les filtrer), cela peut être convaincant. Mais dans ce cas, ce composant obtiendra des données à chaque requête. Et si vous voulez le garder ? C'est là qu'intervient la gestion étatique. Comme les connexions réseau sont souvent coûteuses et parfois peu fiables, il serait préférable de conserver cet état lorsque vous naviguez dans une application.

Un autre problème est la communication entre les composants. Bien que vous puissiez utiliser des événements et des accessoires pour communiquer avec les parents-enfants directs, la gestion de situations simples telles que la gestion des erreurs et les indicateurs d'occupation peut être difficile lorsque chacune de vos vues/pages est indépendante. Par exemple, imaginez que vous ayez un contrôle de niveau supérieur câblé pour afficher l'erreur et l'animation de chargement :

// App.vue

Sans moyen efficace de gérer cet état, cela peut suggérer un système de publication/abonnement, mais en fait, le partage des données est plus simple dans de nombreux cas. Si vous voulez avoir un état partagé, comment procédez-vous ? Examinons quelques façons courantes de procéder.

Remarque : Vous trouverez le code de cette section dans la branche « principale » du exemple de projet sur GitHub.

État partagé dans Vue 3

Depuis le passage à Vue 3, j'ai complètement migré vers l'utilisation de l'API de composition. Pour l'article, j'utilise également TypeScript bien que ce ne soit pas nécessaire pour les exemples que je vous montre. Bien que vous puissiez partager l'état comme vous le souhaitez, je vais vous montrer plusieurs techniques que je trouve parmi les modèles les plus couramment utilisés. Chacune a ses propres avantages et inconvénients, alors ne prenez rien de ce dont je parle ici comme dogme.

Les techniques incluent :

Note : Vuex 5, au moment de la rédaction de cet article, il est au stade RFC (Request for Comments), donc je veux vous préparer à l'orientation de Vuex, mais pour le moment, il n'y a pas de version fonctionnelle de cette option.

Creuseons…[19659020]Factory

Remarque : Le code de cette section se trouve dans la branche « Factories » de l'exemple de projet sur GitHub.

sur la création d'une instance de l'état qui vous intéresse. Dans ce modèle, vous retournez une fonction qui ressemble beaucoup à la fonction start dans l'API de composition. Vous créeriez une portée et créeriez les composants de ce que vous recherchez. Par exemple :

exporter la fonction par défaut () {

  livres const : Travail[] = réactif([]);

  fonction asynchrone loadBooks(val: string) {
      réponse const = wait bookService.getBooks(val, currentPage.value);
      si (réponse.status === 200) {
        books.splice(0, books.length, ...response.data.works);
      }
  }

  revenir {
    chargerLivres,
    livres
  } ;
}

Vous pouvez demander uniquement les parties des objets créés en usine dont vous avez besoin comme suit :

// Dans Home.vue
  const { livres, loadBooks } = BookFactory();

Si nous ajoutons un indicateur isBusy pour afficher quand la demande réseau se produit, le code ci-dessus ne change pas, mais vous pouvez décider où vous allez afficher le isBusy :

exporter la fonction par défaut () {

  livres const : Travail[] = réactif([]);
  const isBusy = ref(false);

  fonction asynchrone loadBooks(val: string) {
    isBusy.value = true;
    réponse const = wait bookService.getBooks(val, currentPage.value);
    si (réponse.status === 200) {
      books.splice(0, books.length, ...response.data.works);
    }
  }

  revenir {
    chargerLivres,
    livres,
    est occupé
  } ;
}

Dans une autre vue (vue ?), vous pouvez simplement demander le drapeau isBusy sans avoir à savoir comment le reste de l'usine fonctionne :

 // App.vue
exporter par défaut defineComponent({
  mettre en place() {
    const { est Occupé } = BookFactory();
    revenir {
      est occupé
    }
  },
})

Mais vous avez peut-être remarqué un problème ; chaque fois que nous appelons l'usine, nous obtenons une nouvelle instance de tous les objets. Il y a des moments où vous voulez qu'une usine renvoie de nouvelles instances, mais dans notre cas, nous parlons de partager l'état, nous devons donc déplacer la création en dehors de l'usine :

const books : Work[] = reactive ([]);
const isBusy = ref(false);

fonction asynchrone loadBooks(val: string) {
  isBusy.value = true;
  réponse const = wait bookService.getBooks(val, currentPage.value);
  si (réponse.status === 200) {
    books.splice(0, books.length, ...response.data.works);
  }
}

exporter la fonction par défaut () {
 revenir {
    chargerLivres,
    livres,
    est occupé
  } ;
}

Maintenant, l'usine nous donne une instance partagée, ou un singleton si vous préférez. Bien que ce modèle fonctionne, il peut être déroutant de renvoyer une fonction qui ne crée pas une nouvelle instance à chaque fois.

Parce que les objets sous-jacents sont marqués comme constvous ne devriez pas pouvoir les remplacer (et briser la nature singleton). Donc ce code devrait se plaindre :

// Dans Home.vue
  const { livres, loadBooks } = BookFactory();

  livres = []; // Erreur, les livres sont définis comme const

Il peut donc être important de s'assurer que l'état mutable peut être mis à jour (par exemple, en utilisant books.splice() au lieu d'affecter les livres).

Une autre façon de gérer cela consiste à utiliser des instances partagées. .

Shared Instances

Le code de cette section est dans le « SharedState » branche de l'exemple de projet sur GitHub.

Si vous allez partager l'état, autant être clair sur le fait que l'état est un singleton. Dans ce cas, il peut simplement être importé en tant qu'objet statique. Par exemple, j'aime créer un objet qui peut être importé en tant qu'objet réactif :

export default reactive({

  livres : new Array(),
  isBusy : faux,

  loadBooks asynchrone () {
    this.isBusy = true;
    réponse const = wait bookService.getBooks(this.currentTopic, this.currentPage);
    si (réponse.status === 200) {
      this.books.splice(0, this.books.length, ...response.data.works);
    }
    this.isBusy = false;
  }
});

Dans ce cas, vous importez simplement l'objet (que j'appelle un magasin dans cet exemple):

// Home.vue
importer l'état de "@/state" ;

exporter par défaut defineComponent({
  mettre en place() {

    // ...

    onMounted(async () => {
      if (state.books.length === 0) state.loadBooks();
    });

    revenir {
      Etat,
      livreSujets,
    } ;
  },
});

Ensuite, il devient facile de se lier à l'État :


Comme les autres modèles, vous avez l'avantage de pouvoir partager cette instance entre les vues :

// App.vue
importer l'état de "@/state" ;

exporter par défaut defineComponent({
  mettre en place() {
    revenir {
      Etat
    } ;
  },
})

Ensuite, cela peut se lier à ce qui est le même objet (qu'il s'agisse d'un parent de Home.vue ou d'une autre page du routeur) :


  

Bibliothèque

Chargement...

Que vous utilisiez le modèle d'usine ou l'instance partagée, ils ont tous deux un problème commun : l'état mutable. Vous pouvez avoir des effets secondaires accidentels de liaisons ou de changement d'état du code lorsque vous ne le souhaitez pas. Dans un exemple trivial comme celui que j'utilise ici, ce n'est pas assez complexe pour s'en inquiéter. Mais au fur et à mesure que vous créez des applications de plus en plus grandes, vous voudrez réfléchir plus attentivement à la mutation d'état. C'est là que Vuex peut venir à la rescousse.

Vuex 4

Le code de cette section se trouve dans la branche « Vuex4 » de l'exemple de projet sur GitHub.

Vuex est gestionnaire d'état pour Vue. Il a été construit par l'équipe de base bien qu'il soit géré comme un projet distinct. Le but de Vuex est de séparer l'état des actions que vous souhaitez faire à l'état. Tous les changements d'état doivent passer par Vuex, ce qui signifie que c'est plus complexe, mais vous bénéficiez d'une protection contre les changements d'état accidentels.

L'idée de Vuex est de fournir un flux prévisible de gestion d'état. Les vues affluent vers des actions qui, à leur tour, utilisent des mutations pour changer d'état qui, à leur tour, met à jour la vue. En limitant le flux de changement d'état, vous devriez avoir moins d'effets secondaires qui modifient l'état de vos applications ; donc être plus facile de construire des applications plus grandes. Vuex a une courbe d'apprentissage, mais avec cette complexité, vous obtenez une prévisibilité.

De plus, Vuex prend en charge les outils de développement (via les outils Vue) pour fonctionner avec la gestion de l'état, y compris une fonctionnalité appelée voyage dans le temps. Cela vous permet d'afficher un historique de l'état et d'aller en arrière pour voir comment cela affecte l'application.

Il y a aussi des moments où Vuex est également important.

Pour l'ajouter à votre projet Vue 3, vous pouvez soit ajouter le package au projet :

> npm i vuex

Ou, vous pouvez également l'ajouter en utilisant Vue CLI :

> vue add vuex

En utilisant la CLI, cela créera un point de départ pour votre magasin Vuex, sinon vous devrez le connecter manuellement au projet. Voyons comment cela fonctionne.

Tout d'abord, vous aurez besoin d'un objet d'état créé avec la fonction createStore de Vuex :

import { createStore } depuis 'vuex'

exporter par défaut createStore({
  Etat: {},
  mutation : {},
  Actions: {},
  getters : {}
});

Comme vous pouvez le voir, le magasin nécessite la définition de plusieurs propriétés. L'état n'est qu'une liste des données auxquelles vous souhaitez donner accès à votre application :

import { createStore } depuis 'vuex'

exporter par défaut createStore({
  Etat: {
    livres : [],
    isBusy : faux
  },
  mutation : {},
  Actions: {}
});

Notez que l'état ne doit pas utiliser les wrappers ref ou reactive. Ces données sont du même type de données de partage que nous avons utilisées avec les instances ou les usines partagées. Ce magasin sera un singleton dans votre application, donc les données en état seront également partagées.

Ensuite, examinons les actions. Les actions sont des opérations que vous souhaitez activer et qui impliquent l'état. Par exemple :

 actions : {
    charge asynchroneLivres (magasin) {
      réponse const = wait bookService.getBooks(store.state.currentTopic,
      si (réponse.status === 200) {
        // ...
      }
    }
  },

Les actions reçoivent une instance du magasin afin que vous puissiez accéder à l'état et à d'autres opérations. Normalement, nous déstructurerions uniquement les parties dont nous avons besoin :

 actions : {
    loadBooks asynchrone ({ état }) {
      réponse const = wait bookService.getBooks(state.currentTopic,
      si (réponse.status === 200) {
        // ...
      }
    }
  },

La ​​dernière pièce de ceci est Mutations. Les mutations sont des fonctions qui peuvent muter l'état. Seules les mutations peuvent affecter l'état. Donc, pour cet exemple, nous avons besoin de mutations qui changent l'état :

 mutations : {
    setBusy : (état) => état.isBusy = vrai,
    clearBusy : (état) => état.isBusy = faux,
    setBooks(état, livres) {
      state.books.splice(0, state.books.length, ...livres);
    }
 },

Les fonctions de mutation transmettent toujours l'objet d'état afin que vous puissiez muter cet état. Dans les deux premiers exemples, vous pouvez voir que nous définissons explicitement l'état. Mais dans le troisième exemple, on passe à l'état set. Les mutations prennent toujours deux paramètres : l'état et l'argument lors de l'appel de la mutation.

Pour appeler une mutation, vous devez utiliser la fonction commit sur le magasin. Dans notre cas, je vais juste l'ajouter aux actions de déstructuration :

 : {
    loadBooks asynchrone ({ état, validation }) {
      commit("setBusy");
      réponse const = wait bookService.getBooks(state.currentTopic,
      si (réponse.status === 200) {
        commit("setBooks", response.data);
      }
      commit("clearBusy");
    }
  },

Ce que vous verrez ici, c'est comment commit requiert le nom de l'action. Il existe des astuces pour que cela n'utilise pas seulement des chaînes magiques, mais je vais sauter cela pour l'instant. Cette utilisation de chaînes magiques est l'une des limitations de l'utilisation de Vuex.

Bien que l'utilisation de commit puisse sembler être un wrapper inutile, rappelez-vous que Vuex ne vous laissera pas muter l'état sauf à l'intérieur de la mutation, donc n'appelle que commit le fera.

Vous pouvez également voir que l'appel à setBooks prend un deuxième argument. C'est le deuxième argument qui appelle la mutation. Si vous aviez besoin de plus d'informations, vous auriez besoin de les regrouper dans un seul argument (une autre limitation de Vuex actuellement). En supposant que vous deviez insérer un livre dans la liste des livres, vous pouvez l'appeler comme ceci :

commit("insertBook", { book, place: 4 }); // objet, tuple, etc.

Ensuite, vous pouvez simplement déstructurer les éléments dont vous avez besoin :

mutations : {
  insertBook(état, { livre, lieu }) => // ...
}

Est-ce élégant? Pas vraiment, mais cela fonctionne.

Maintenant que notre action fonctionne avec les mutations, nous devons pouvoir utiliser le magasin Vuex dans notre code. Il y a vraiment deux façons d'arriver au magasin. Tout d'abord, en enregistrant le magasin avec l'application (par exemple, main.ts/js), vous aurez accès à un magasin centralisé auquel vous avez accès partout dans votre application :

// main.ts
importer le magasin de './store'

createApp(App)
  .utiliser (magasin)
  .use(routeur)
  .mount('#app')

Notez qu'il ne s'agit pas d'ajouter Vuex, mais votre magasin réel que vous créez. Une fois que cela est ajouté, vous pouvez simplement appeler useStore pour obtenir l'objet store :

import { useStore } de "vuex" ;

exporter par défaut defineComponent({
  Composants: {
    RéserverInfo,
  },
  mettre en place() {
    const store = useStore();
    const livres = calculé(() => store.state.books);
    // ...
  

Cela fonctionne bien, mais je préfère simplement importer le magasin directement :

importer le magasin depuis "@/store" ;

exporter par défaut defineComponent({
  Composants: {
    RéserverInfo,
  },
  mettre en place() {
    const livres = calculé(() => store.state.books);
    // ...
  

Maintenant que vous avez accès à l'objet store, comment l'utilisez-vous ? Pour l'état, vous devrez les envelopper avec des fonctions calculées afin que les modifications soient propagées à vos liaisons :

export default defineComponent({
  mettre en place() {

    const livres = calculé(() => store.state.books);

    revenir {
      livres
    } ;
  },
});

Pour appeler des actions, vous devrez appeler la méthode dispatch :

export default defineComponent({
  mettre en place() {

    const livres = calculé(() => store.state.books);

    onMounted(async () => wait store.dispatch("loadBooks"));

    revenir {
      livres
    } ;
  },
});

Les actions peuvent avoir des paramètres que vous ajoutez après le nom de la méthode. Enfin, pour changer d'état, vous devrez appeler commit comme nous l'avons fait dans les actions. Par exemple, j'ai une propriété de pagination dans le magasin, puis je peux changer l'état avec commit:

const incrementPage = () =>
  store.commit("setPage", store.state.currentPage + 1);
const decrementPage = () =>
  store.commit("setPage", store.state.currentPage - 1);

Notez que l'appeler comme ceci générerait une erreur (car vous ne pouvez pas changer l'état manuellement):

const incrementPage = () => store.state.currentPage++;
  const decrementPage = () => store.state.currentPage--;

C'est le vrai pouvoir ici, nous voudrions contrôler où l'état est modifié et ne pas avoir d'effets secondaires qui produisent des erreurs plus tard dans le développement.

Vous pouvez être submergé par le nombre de pièces en mouvement dans Vuex, mais cela peut vraiment aider à gérer l'état dans des projets plus importants et plus complexes. Je ne dirais pas que vous en avez besoin dans tous les cas, mais il y aura de grands projets où cela vous aidera dans l'ensemble.

Le gros problème avec Vuex 4 est que travailler avec dans un projet TypeScript laisse beaucoup à désirer. Vous pouvez certainement créer des types TypeScript pour aider au développement et aux constructions, mais cela nécessite beaucoup de pièces mobiles.

C'est là que Vuex 5 est destiné à simplifier le fonctionnement de Vuex dans TypeScript (et dans les projets JavaScript en général). Voyons comment cela fonctionnera lors de sa prochaine publication.

Vuex 5

Note : Le code de cette section se trouve dans la branche « Vuex5 » de l'exemple de projet sur GitHub.

Au moment de cet article, Vuex 5 n'est pas réel. C'est un RFC (Request for Comments). C'est un plan. C'est un point de départ pour la discussion. Donc, beaucoup de ce que je peux expliquer ici changera probablement quelque peu. Mais pour vous préparer au changement de Vuex, je voulais vous donner une vue d'où ça va. Pour cette raison, le code associé à cet exemple ne se construit pas.

Les concepts de base du fonctionnement de Vuex sont restés quelque peu inchangés depuis sa création. Avec l'introduction de Vue 3, Vuex 4 a été créé pour permettre principalement à Vuex de travailler dans de nouveaux projets. Mais l'équipe essaie d'examiner les véritables problèmes avec Vuex et de les résoudre. À cette fin, ils prévoient des changements importants :

  • Plus de mutations : les actions peuvent muter l'état (et peut-être n'importe qui).
  • Meilleure prise en charge de TypeScript.
  • Meilleure fonctionnalité multi-magasins.

Alors, comment cela se passerait-il. travail? Commençons par créer le magasin :

exporter par défaut createStore({
  clé : 'librairie',
  état : () => ({
    isBusy : faux,
    livres : nouveau tableau()
  }),
  Actions: {
    loadBooks asynchrone () {
      essayer {
        this.isBusy = true;
        réponse const = wait bookService.getBooks();
        si (réponse.status === 200) {
          this.books = response.data.works;
        }
      } finalement {
        this.isBusy = false;
      }
    }
  },
  getters : {
    findBook (clé : chaîne) : Travail | indéfini {
      renvoie this.books.find(b => b.key === clé);
    }
  }
});

Le premier changement à voir est que chaque magasin a maintenant besoin de sa propre clé. Cela vous permet de récupérer plusieurs magasins. Ensuite, vous remarquerez que l'objet d'état est maintenant une usine (par exemple, les retours d'une fonction, non créés lors de l'analyse). Et il n'y a plus de section sur les mutations. Enfin, à l'intérieur des actions, vous pouvez voir que nous accédons à l'état en tant que propriétés sur le pointeur this. Plus besoin de passer en état et de s'engager dans des actions. Cela aide non seulement à simplifier le développement, mais facilite également la déduction des types pour TypeScript.

Pour enregistrer Vuex dans votre application, vous enregistrerez Vuex au lieu de votre magasin global :

import { createVuex } depuis 'vuex '

createApp(App)
  .use(createVuex())
  .use(routeur)
  .mount('#app')

Enfin, pour utiliser le magasin, vous importerez le magasin puis en créerez une instance :

importez bookStore depuis "@/store" ;

exporter par défaut defineComponent({
  Composants: {
    RéserverInfo,
  },
  mettre en place() {
    magasin const = librairie(); // Générer le wrapper
    // ...
  

Notez que ce qui est renvoyé du magasin est un objet d'usine qui renvoie cette instance du magasin, quel que soit le nombre de fois que vous appelez l'usine. L'objet renvoyé est juste un objet avec les actions, l'état et les getters en tant que citoyens de première classe (avec les informations de type):

onMounted(async () => wait store.loadBooks());

const incrementPage = () => store.currentPage++;
const decrementPage = () => store.currentPage--;

Ce que vous verrez ici, c'est que l'état (par exemple, currentPage) ne sont que de simples propriétés. Et les actions (par exemple loadBooks) ne sont que des fonctions. Le fait que vous utilisiez un magasin ici est un effet secondaire. Vous pouvez traiter l'objet Vuex comme un simple objet et continuer votre travail. Il s'agit d'une amélioration significative de l'API.

Un autre changement important à souligner est que vous pouvez également générer votre magasin à l'aide d'une syntaxe de type API de composition :

export default defineStore("another", () => {

  // État
  const isBusy = ref(false);
  const books = reactive(new Array≷Work>());

  // Actions
  fonction asynchrone loadBooks() {
    essayer {
      this.isBusy = true;
      réponse const = wait bookService.getBooks(this.currentTopic, this.currentPage);
      si (réponse.status === 200) {
        this.books = response.data.works;
      }
    } finalement {
      this.isBusy = false;
    }
  }

  findBook (clé : chaîne) : Travail | indéfini {
    renvoie this.books.find(b => b.key === clé);
  }

  // Getters
  const bookCount = computed(() => this.books.length);

  revenir {
    est occupé,
    livres,
    chargerLivres,
    trouverLivre,
    nombre de livres
  }
});

Cela vous permet de créer votre objet Vuex comme vous le feriez pour vos vues avec l'API de composition et c'est sans doute plus simple.

Un inconvénient principal de cette nouvelle conception est que vous perdez la non-mutabilité de l'état. Il y a des discussions autour de la possibilité d'activer cela (pour le développement uniquement, tout comme Vuex 4), mais il n'y a pas de consensus sur l'importance de cela. Personnellement, je pense que c'est un avantage clé pour Vuex, mais nous devrons voir comment cela se passe. Avoir un plan de match sur la façon dont vous voulez vous y prendre dans Vue est une étape importante dans la conception de votre solution. Dans cet article, je vous ai montré plusieurs modèles de gestion de l'état partagé, y compris ce qui s'en vient pour Vuex 5. J'espère que vous aurez maintenant les connaissances nécessaires pour prendre la bonne décision pour vos propres projets.

Smashing Editorial" width="35 " height="46" loading="lazy" decoding="async(vf, yk, il)




Source link