Création du plugin Vuex Undo / Redo pour VueJS

image


La centralisation de l'état de votre application dans la boutique Vuex présente de nombreux avantages. Un avantage est que toutes les transactions sont enregistrées. Cela vous permet d'utiliser des fonctions pratiques, telles que le débogage d'exécution , où vous pouvez basculer entre les états précédents pour séparer les tâches de l'exécution.


Dans cet article, je vais montrer comment créer davantage la fonction Annuler / Rétablir Rollback / Return en utilisant Vuex, qui fonctionne de manière similaire au débogage pendant le débogage. Cette fonctionnalité peut être utilisée dans une variété de scénarios, des formulaires complexes aux jeux sur navigateur.


Vous pouvez consulter le code fini ici sur Github et essayer la démo dans ce Codepen . J'ai également créé un plugin en tant que module NPM appelé vuex-undo-redo si vous souhaitez l'utiliser dans un projet.


Remarque: Cet article a été initialement publié ici sur le blog du développeur Vue.js 2017/11/13.

Configuration du plugin


Pour rendre cette fonctionnalité réutilisable, nous allons la créer en tant que plugin Vue. Cette fonction nous oblige à ajouter des méthodes et des données à l'instance de Vue, donc nous structurons le plugin comme un mixin.


module.exports = { install(Vue) { Vue.mixin({ // Code goes here }); } }; 

Pour l'utiliser dans un projet, nous pouvons simplement importer le plugin et le connecter:


 import VuexUndoRedo from './plugin.js'; Vue.use(VuexUndoRedo); 

Idée


La fonctionnalité fonctionnera en annulant la dernière mutation si l'utilisateur veut l'annuler, puis en la réappliquant s'il veut la répéter. Comment fait-on cela?


Approche n ° 1


La première approche possible consiste à prendre des «instantanés» de l'état du référentiel après chaque mutation et à placer l'instantané dans un tableau. Pour annuler / rétablir, nous pouvons obtenir l'instantané correct et le remplacer par l'état de stockage.


Le problème avec cette approche est que l'état du référentiel est un objet JavaScript. Lorsque vous placez un objet JavaScript dans un tableau, vous mettez simplement une référence à l'objet. Une implémentation naïve, comme la suivante, ne fonctionnera pas:


 var state = { ... }; var snapshot = []; // Push the first state snapshot.push(state); // Push the second state state.val = "new val"; snapshot.push(state); // Both snapshots are simply a reference to state console.log(snapshot[0] === snapshot[1]); // true 

L'approche par instantané nécessitera que vous fassiez d'abord un clone d'état avant de pousser. Étant donné que l'état de Vue devient réactif en ajoutant automatiquement les fonctions get et set, cela ne fonctionne pas très bien avec le clonage.


Approche n ° 2


Une autre approche possible consiste à enregistrer chaque mutation fixe. Pour annuler, nous réinitialisons le magasin à son état initial, puis réexécutons les mutations; tous sauf le dernier. Rembourser un concept similaire.


Compte tenu des principes de Flux, le redémarrage des mutations à partir du même état initial devrait idéalement recréer l'état. Comme il s'agit d'une approche plus propre que la première, continuons.


Enregistrement des mutations


Vuex propose une méthode API pour s'abonner aux mutations, que nous pouvons utiliser pour les enregistrer. Nous allons définir cela sur le crochet created . Dans le rappel, nous plaçons simplement la mutation dans un tableau qui peut ensuite être réexécuté.


 Vue.mixin({ data() { return { done: [] } }, created() { this.$store.subscribe(mutation => { this.done.push(mutation); } } }); 

Méthode de restauration


Pour annuler la mutation, nous effaçons le référentiel, puis réexécutons toutes les mutations à l'exception de la dernière. Voici comment fonctionne le code:


  1. Utilisez la méthode du tableau pop pour supprimer la dernière mutation.
  2. Effacer l'état du magasin à l'aide de la mutation spéciale EMPTY_STATE (expliqué ci-dessous)
  3. Répétez chaque mutation restante, en la fixant à nouveau dans le nouveau magasin. Veuillez noter que la méthode d'abonnement est toujours active pendant ce processus, c'est-à-dire que chaque mutation sera à nouveau ajoutée. Supprimez-le immédiatement avec pop .

 const EMPTY_STATE = 'emptyState'; Vue.mixin({ data() { ... }, created() { ... }, methods() { undo() { this.done.pop(); this.$store.commit(EMPTY_STATE); this.done.forEach(mutation => { this.$store.commit(`${mutation.type}`, mutation.payload); this.done.pop(); }); } } }); 

Magasin de nettoyage


Chaque fois que ce plugin est utilisé, le développeur doit implémenter une mutation dans son référentiel appelée emptyState. Le défi consiste à ramener le magasin à son état d'origine afin qu'il soit prêt pour la récupération à partir de zéro.


Le développeur doit le faire lui-même, car le plugin que nous créons n'a pas accès au magasin, uniquement à l'instance de Vue. Voici un exemple d'implémentation:


 new Vuex.Store({ state: { myVal: null }, mutations: { emptyState() { this.replaceState({ myval: null }); } } }); 

En revenant à notre plugin, la mutation emptyState ne doit pas être ajoutée à notre liste done , car nous ne voulons pas le réengager pendant le processus de restauration. Empêchez cela avec la logique suivante:


 Vue.mixin({ data() { ... }, created() { this.$store.subscribe(mutation => { if (mutation.type !== EMPTY_STATE) { this.done.push(mutation); } }); }, methods() { ... } }); 

Méthode de retour


Créons une nouvelle propriété de données, undone qui sera un tableau. Lorsque nous supprimons la dernière mutation de done pendant le processus de restauration, nous la mettons dans ce tableau:


 Vue.mixin({ data() { return { done: [], undone: [] } }, methods: { undo() { this.undone.push(this.done.pop()); ... } } }); 

Nous pouvons maintenant créer une méthode de redo qui prendra simplement la dernière mutation undone ajoutée et la réengagera.


 methods: { undo() { ... }, redo() { let commit = this.undone.pop(); this.$store.commit(`${commit.type}`, commit.payload); } } 

Aucun retour possible


Si l'utilisateur initie l'annulation une ou plusieurs fois puis effectue un nouveau nouveau commit, le contenu de undone sera invalidé. Si cela se produit, nous devons vider l' undone .


Nous pouvons découvrir de nouvelles validations à partir de notre rappel d'abonnement lors de l'ajout d'une validation. Cependant, la logique est délicate, car le rappel n'a aucun moyen évident de savoir ce qu'est un nouveau commit et ce qu'est un undo / redo.


L'approche la plus simple consiste à définir l'indicateur newMutation. Ce sera vrai par défaut, mais les méthodes de retour en arrière et de retour le définiront temporairement sur faux. Si la mutation est définie sur true, le rappel d' subscribe effacera le tableau undone .


 module.exports = { install(Vue) { Vue.mixin({ data() { return { done: [], undone: [], newMutation: true }; }, created() { this.$store.subscribe(mutation => { if (mutation.type !== EMPTY_STATE) { this.done.push(mutation); } if (this.newMutation) { this.undone = []; } }); }, methods: { redo() { let commit = this.undone.pop(); this.newMutation = false; this.$store.commit(`${commit.type}`, commit.payload); this.newMutation = true; }, undo() { this.undone.push(this.done.pop()); this.newMutation = false; this.$store.commit(EMPTY_STATE); this.done.forEach(mutation => { this.$store.commit(`${mutation.type}`, mutation.payload); this.done.pop(); }); this.newMutation = true; } } }); }, } 

La fonctionnalité principale est maintenant terminée! Ajoutez le plugin à votre propre projet ou à ma démo pour le tester.


API publique


Dans ma démo, vous remarquerez que les boutons d'annulation et de retour sont désactivés si leur fonctionnalité n'est pas possible actuellement. Par exemple, s'il n'y a pas encore eu de commit, vous ne pouvez évidemment pas annuler ou refaire. Un développeur utilisant ce plugin peut vouloir implémenter des fonctionnalités similaires.


Pour l'activer, le plugin peut fournir deux propriétés calculées, canUndo et canRedo dans le cadre de l'API publique. C'est trivial à implémenter:


 module.exports = { install(Vue) { Vue.mixin({ data() { ... }, created() { ... }, methods: { ... }, computed: {}, computed: { canRedo() { return this.undone.length; }, canUndo() { return this.done.length; } }, }); }, } 

Source: https://habr.com/ru/post/fr433986/


All Articles