Gestionarea stării partajate în Vue 3

Publicat: 2022-03-10
Rezumat rapid ↬ Scrierea de aplicații Vue la scară largă poate fi o provocare. Utilizarea stării partajate în aplicațiile tale Vue 3 poate fi o soluție pentru reducerea acestei complexități. Există o serie de soluții comune pentru rezolvarea stării. În acest articol, mă voi scufunda în avantajele și dezavantajele abordărilor precum fabricile, obiectele partajate și utilizarea Vuex. De asemenea, vă voi arăta ce urmează în Vuex 5, care ar putea schimba modul în care folosim cu toții starea partajată în Vue 3.

Statul poate fi greu. Când începem un proiect Vue simplu, poate fi simplu să ne păstrăm starea de lucru pe o anumită componentă:

 setup() { let books: Work[] = reactive([]); onMounted(async () => { // Call the API const response = await bookService.getScienceBooks(); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } }); return { books }; },

Atunci când proiectul tău este o singură pagină de afișare a datelor (poate pentru a le sorta sau filtra), acest lucru poate fi convingător. Dar, în acest caz, această componentă va primi date pentru fiecare solicitare. Dacă vrei să-l ții în preajmă? Acolo intervine managementul statului. Deoarece conexiunile la rețea sunt adesea costisitoare și uneori nesigure, ar fi mai bine să păstrați această stare în timp ce navigați printr-o aplicație.

O altă problemă este comunicarea între componente. Deși puteți folosi evenimente și recuzită pentru a comunica cu copiii-părinți direcți, gestionarea situațiilor simple, cum ar fi gestionarea erorilor și semnalizările de ocupat, poate fi dificilă atunci când fiecare dintre vizualizările/paginile dvs. sunt independente. De exemplu, imaginați-vă că ați avut un control de nivel superior a fost conectat pentru a afișa eroarea și animația de încărcare:

 // App.vue <template> <div class="container mx-auto bg-gray-100 p-1"> <router-link to="/"><h1>Bookcase</h1></router-link> <div class="alert" v-if="error">{{ error }}</div> <div class="alert bg-gray-200 text-gray-900" v-if="isBusy"> Loading... </div> <router-view :key="$route.fullPath"></router-view> </div> </template>

Fără o modalitate eficientă de a gestiona această stare, ar putea sugera un sistem de publicare/abonare, dar, de fapt, partajarea datelor este mai simplă în multe cazuri. Dacă doriți să aveți o stare comună, cum procedați? Să ne uităm la câteva modalități comune de a face acest lucru.

Notă : Veți găsi codul pentru această secțiune în ramura „principală” a proiectului exemplu de pe GitHub.

Stare comună în Vue 3

De când m-am mutat la Vue 3, am migrat complet la utilizarea API-ului Composition. Pentru articol, folosesc și TypeScript, deși nu este necesar pentru exemplele pe care vi le arăt. În timp ce puteți împărtăși starea în orice mod doriți, vă voi arăta câteva tehnici pe care le găsesc cele mai frecvent utilizate modele. Fiecare are propriile sale avantaje și dezavantaje, așa că nu lua nimic despre care vorbesc aici drept dogmă.

Tehnicile includ:

  • fabrici,
  • Singletons comune,
  • Vuex 4,
  • Vuex 5.

Notă : Vuex 5, de la scrierea acestui articol, se află în stadiul RFC (Solicitare pentru comentarii), așa că vreau să vă pregătesc pentru unde merge Vuex, dar în acest moment nu există o versiune funcțională a acestei opțiuni.

Să pătrundem…

Fabrici

Notă : Codul pentru această secțiune se află în ramura „Fabrici” a proiectului exemplu de pe GitHub.

Modelul fabricii este doar despre crearea unei instanțe a stării la care vă interesează. În acest model, returnați o funcție care seamănă mult cu funcția de pornire din API-ul Composition. Veți crea un domeniu de aplicare și ați construi componentele a ceea ce căutați. De exemplu:

 export default function () { const books: Work[] = reactive([]); async function loadBooks(val: string) { const response = await bookService.getBooks(val, currentPage.value); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } } return { loadBooks, books }; }

Puteți cere doar părțile obiectelor create din fabrică de care aveți nevoie, astfel:

 // In Home.vue const { books, loadBooks } = BookFactory();

Dacă adăugăm un semnalizator isBusy pentru a arăta când are loc solicitarea rețelei, codul de mai sus nu se schimbă, dar puteți decide unde veți afișa isBusy :

 export default function () { const books: Work[] = reactive([]); const isBusy = ref(false); async function loadBooks(val: string) { isBusy.value = true; const response = await bookService.getBooks(val, currentPage.value); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } } return { loadBooks, books, isBusy }; }

Într-o altă vedere (vue?) ați putea cere doar steagul isBusy fără a fi nevoie să știți cum funcționează restul fabricii:

 // App.vue export default defineComponent({ setup() { const { isBusy } = BookFactory(); return { isBusy } }, })

Dar este posibil să fi observat o problemă; de fiecare dată când sunăm la fabrică, primim o nouă instanță a tuturor obiectelor. Există momente în care doriți ca o fabrică să returneze instanțe noi, dar în cazul nostru vorbim despre partajarea stării, așa că trebuie să mutăm creația în afara fabricii:

 const books: Work[] = reactive([]); const isBusy = ref(false); async function loadBooks(val: string) { isBusy.value = true; const response = await bookService.getBooks(val, currentPage.value); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } } export default function () { return { loadBooks, books, isBusy }; }

Acum, fabrica ne oferă o instanță comună sau un singleton dacă preferați. În timp ce acest model funcționează, poate fi confuz să returnezi o funcție care nu creează o instanță nouă de fiecare dată.

Deoarece obiectele de bază sunt marcate ca const , nu ar trebui să le puteți înlocui (și rupe natura singleton). Deci, acest cod ar trebui să se plângă:

 // In Home.vue const { books, loadBooks } = BookFactory(); books = []; // Error, books is defined as const

Deci, poate fi important să vă asigurați că starea mutabilă poate fi actualizată (de exemplu, folosind books.splice() în loc să atribuiți cărțile).

O altă modalitate de a gestiona acest lucru este utilizarea instanțelor partajate.

Mai multe după săritură! Continuați să citiți mai jos ↓

Instanțe partajate

Codul pentru această secțiune se află în ramura „SharedState” a proiectului exemplu de pe GitHub.

Dacă aveți de gând să partajați starea, ar putea la fel de bine să fiți clar despre faptul că statul este un singleton. În acest caz, poate fi doar importat ca obiect static. De exemplu, îmi place să creez un obiect care poate fi importat ca obiect reactiv:

 export default reactive({ books: new Array<Work>(), isBusy: false, async loadBooks() { this.isBusy = true; const response = await bookService.getBooks(this.currentTopic, this.currentPage); if (response.status === 200) { this.books.splice(0, this.books.length, ...response.data.works); } this.isBusy = false; } });

În acest caz, importați doar obiectul (pe care îl numesc magazin în acest exemplu):

 // Home.vue import state from "@/state"; export default defineComponent({ setup() { // ... onMounted(async () => { if (state.books.length === 0) state.loadBooks(); }); return { state, bookTopics, }; }, });

Apoi devine ușor să vă legați de stat:

 <!-- Home.vue --> <div class="grid grid-cols-4"> <div v-for="book in state.books" :key="book.key" class="border bg-white border-grey-500 m-1 p-1" > <router-link :to="{ name: 'book', params: { id: book.key } }"> <BookInfo :book="book" /> </router-link> </div>

La fel ca și celelalte modele, obțineți beneficiul că puteți partaja această instanță între vizualizări:

 // App.vue import state from "@/state"; export default defineComponent({ setup() { return { state }; }, })

Apoi, acesta se poate lega de ceea ce este același obiect (fie că este un părinte al Home.vue sau o altă pagină din router):

 <!-- App.vue --> <div class="container mx-auto bg-gray-100 p-1"> <router-link to="/"><h1>Bookcase</h1></router-link> <div class="alert bg-gray-200 text-gray-900" v-if="state.isBusy">Loading...</div> <router-view :key="$route.fullPath"></router-view> </div>

Indiferent dacă utilizați modelul din fabrică sau instanța partajată, ambele au o problemă comună: starea mutabilă. Puteți avea efecte secundare accidentale ale legăturilor sau ale stării de schimbare a codului atunci când nu doriți. Într-un exemplu banal ca cel pe care îl folosesc aici, nu este suficient de complex pentru a vă face griji. Dar, pe măsură ce construiți aplicații din ce în ce mai mari, veți dori să vă gândiți mai atent la mutația stării. Acolo Vuex poate veni în ajutor.

Vuex 4

Codul pentru această secțiune se află în ramura „Vuex4” a proiectului exemplu de pe GitHub.

Vuex este manager de stat pentru Vue. A fost construit de echipa de bază, deși este gestionat ca un proiect separat. Scopul Vuex este de a separa statul de acțiunile pe care doriți să le faceți statului. Toate schimbările de stare trebuie să treacă prin Vuex, ceea ce înseamnă că este mai complex, dar aveți protecție împotriva schimbărilor accidentale de stare.

Ideea Vuex este de a oferi un flux previzibil de management al statului. Vizualizările circulă către Acțiuni care, la rândul lor, folosesc mutații pentru a schimba starea care, la rândul său, actualizează vizualizarea. Limitând fluxul de schimbare a stării, ar trebui să aveți mai puține efecte secundare care modifică starea aplicațiilor dvs.; prin urmare, să fie mai ușor să construiți aplicații mai mari. Vuex are o curbă de învățare, dar cu această complexitate obțineți predictibilitate.

În plus, Vuex acceptă instrumente pentru timpul de dezvoltare (prin Instrumentele Vue) pentru a lucra cu gestionarea statului, inclusiv o caracteristică numită călătorie în timp. Acest lucru vă permite să vizualizați un istoric al stării și să vă deplasați înapoi și înainte pentru a vedea cum afectează aplicația.

Există momente, de asemenea, când Vuex este și el important.

Pentru a-l adăuga la proiectul dvs. Vue 3, puteți fie să adăugați pachetul la proiect:

 > npm i vuex

Sau, alternativ, îl puteți adăuga utilizând Vue CLI:

 > vue add vuex

Folosind CLI, va crea un punct de plecare pentru magazinul dvs. Vuex, altfel va trebui să-l conectați manual la proiect. Să vedem cum funcționează asta.

Mai întâi, veți avea nevoie de un obiect de stare care este creat cu funcția createStore a Vuex:

 import { createStore } from 'vuex' export default createStore({ state: {}, mutations: {}, actions: {}, getters: {} });

După cum puteți vedea, magazinul necesită definirea mai multor proprietăți. Starea este doar o listă a datelor la care doriți să acordați acces aplicației dvs.:

 import { createStore } from 'vuex' export default createStore({ state: { books: [], isBusy: false }, mutations: {}, actions: {} });

Rețineți că statul nu ar trebui să folosească ambalaje ref sau reactive . Aceste date sunt același tip de date de partajare pe care le-am folosit cu Instanțe partajate sau Fabrici. Acest magazin va fi un singleton în aplicația dvs., prin urmare datele în stare vor fi, de asemenea, partajate.

În continuare, să ne uităm la acțiuni. Acțiunile sunt operațiuni pe care doriți să le activați și care implică statul. De exemplu:

 actions: { async loadBooks(store) { const response = await bookService.getBooks(store.state.currentTopic, if (response.status === 200) { // ... } } },

Acțiunile sunt transmise unei instanțe a magazinului, astfel încât să puteți ajunge la stare și alte operațiuni. În mod normal, am destructura doar părțile de care avem nevoie:

 actions: { async loadBooks({ state }) { const response = await bookService.getBooks(state.currentTopic, if (response.status === 200) { // ... } } },

Ultima bucată din asta sunt Mutații. Mutațiile sunt funcții care pot muta stare. Numai mutațiile pot afecta starea. Deci, pentru acest exemplu, avem nevoie de mutații care schimbă starea:

 mutations: { setBusy: (state) => state.isBusy = true, clearBusy: (state) => state.isBusy = false, setBooks(state, books) { state.books.splice(0, state.books.length, ...books); } },

Funcțiile de mutație trec întotdeauna în obiectul de stare, astfel încât să puteți muta starea respectivă. În primele două exemple, puteți vedea că setăm în mod explicit starea. Dar în cel de-al treilea exemplu, trecem în stat pentru a stabili. Mutațiile iau întotdeauna doi parametri: starea și argumentul la apelarea mutației.

Pentru a apela o mutație, ați folosi funcția commit din magazin. În cazul nostru, o voi adăuga doar la destructurare:

 actions: { async loadBooks({ state, commit }) { commit("setBusy"); const response = await bookService.getBooks(state.currentTopic, if (response.status === 200) { commit("setBooks", response.data); } commit("clearBusy"); } },

Ceea ce veți vedea aici este modul în care commit necesită numele acțiunii. Există trucuri pentru ca acest lucru să nu folosească doar șiruri magice, dar o să omit deocamdată. Această utilizare a șirurilor magice este una dintre limitările utilizării Vuex.

În timp ce utilizarea commit poate părea un wrapper inutil, amintiți-vă că Vuex nu vă va permite să mutați starea decât în ​​interiorul mutației, prin urmare doar apelurile prin commit will.

De asemenea, puteți vedea că apelul la setBooks are un al doilea argument. Acesta este al doilea argument care numește mutația. Dacă ai avea nevoie de mai multe informații, ar trebui să le împachetezi într-un singur argument (o altă limitare a Vuex în prezent). Presupunând că trebuie să inserați o carte în lista de cărți, o puteți numi astfel:

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

Atunci ai putea să te desstructurezi în bucățile de care ai nevoie:

 mutations: { insertBook(state, { book, place }) => // ... }

Este elegant? Nu chiar, dar funcționează.

Acum că acțiunea noastră lucrează cu mutații, trebuie să putem folosi magazinul Vuex în codul nostru. Există într-adevăr două moduri de a ajunge la magazin. În primul rând, înregistrând magazinul cu aplicația (de exemplu, main.ts/js), veți avea acces la un magazin centralizat la care aveți acces peste tot în aplicația dvs.:

 // main.ts import store from './store' createApp(App) .use(store) .use(router) .mount('#app')

Rețineți că aceasta nu adaugă Vuex, ci magazinul dvs. real pe care îl creați. Odată ce acesta este adăugat, puteți doar să apelați useStore pentru a obține obiectul magazin:

 import { useStore } from "vuex"; export default defineComponent({ components: { BookInfo, }, setup() { const store = useStore(); const books = computed(() => store.state.books); // ...

Funcționează bine, dar prefer să importe direct magazinul:

 import store from "@/store"; export default defineComponent({ components: { BookInfo, }, setup() { const books = computed(() => store.state.books); // ...

Acum că aveți acces la obiectul magazin, cum îl folosiți? Pentru stare, va trebui să le împachetați cu funcții calculate, astfel încât modificările să fie propagate la legările dvs.:

 export default defineComponent({ setup() { const books = computed(() => store.state.books); return { books }; }, });

Pentru a apela acțiuni, va trebui să apelați metoda de expediere :

 export default defineComponent({ setup() { const books = computed(() => store.state.books); onMounted(async () => await store.dispatch("loadBooks")); return { books }; }, });

Acțiunile pot avea parametri pe care îi adăugați după numele metodei. În cele din urmă, pentru a schimba starea, va trebui să apelați commit exact așa cum am făcut în cadrul Acțiunilor. De exemplu, am o proprietate de paginare în magazin și apoi pot schimba starea cu commit :

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

Rețineți că o numire astfel ar genera o eroare (pentru că nu puteți schimba starea manual):

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

Aceasta este puterea reală aici, am dori controlul în cazul în care starea este schimbată și să nu aibă efecte secundare care să producă erori mai departe în curs de dezvoltare.

Este posibil să fiți copleșit de numărul de piese în mișcare în Vuex, dar poate ajuta cu adevărat la gestionarea stării în proiecte mai mari și mai complexe. Nu aș spune că ai nevoie de el în toate cazurile, dar vor exista proiecte mari în care te ajută în general.

Marea problemă cu Vuex 4 este că lucrul cu acesta într-un proiect TypeScript lasă mult de dorit. Cu siguranță puteți crea tipuri TypeScript pentru a ajuta dezvoltarea și construirea, dar necesită o mulțime de piese în mișcare.

Acesta este locul în care Vuex 5 este menit să simplifice modul în care funcționează Vuex în TypeScript (și în proiectele JavaScript în general). Să vedem cum va funcționa odată ce va fi lansat.

Vuex 5

Notă : Codul pentru această secțiune se află în ramura „Vuex5” a proiectului exemplu de pe GitHub.

La momentul acestui articol, Vuex 5 nu este real. Este un RFC (Solicitare de comentarii). Este un plan. Este un punct de plecare pentru discuții. Deci, multe din ceea ce pot explica aici probabil se vor schimba oarecum. Dar, pentru a vă pregăti pentru schimbarea în Vuex, am vrut să vă ofer o imagine despre unde se duce. Din această cauză, codul asociat cu acest exemplu nu se construiește.

Conceptele de bază ale modului în care funcționează Vuex au rămas oarecum neschimbate de la începuturile sale. Odată cu introducerea Vue 3, Vuex 4 a fost creat pentru a permite în mare parte Vuex să lucreze în proiecte noi. Dar echipa încearcă să se uite la problemele reale cu Vuex și să le rezolve. În acest scop, ei plănuiesc câteva schimbări importante:

  • Gata cu mutațiile: acțiunile pot modifica starea (și posibil pe oricine).
  • Suport mai bun pentru TypeScript.
  • O mai bună funcționalitate multi-magazin.

Deci cum ar funcționa asta? Să începem cu crearea magazinului:

 export default createStore({ key: 'bookStore', state: () => ({ isBusy: false, books: new Array<Work>() }), actions: { async loadBooks() { try { this.isBusy = true; const response = await bookService.getBooks(); if (response.status === 200) { this.books = response.data.works; } } finally { this.isBusy = false; } } }, getters: { findBook(key: string): Work | undefined { return this.books.find(b => b.key === key); } } });

Prima schimbare de văzut este că fiecare magazin are acum nevoie de propria cheie. Acest lucru este pentru a vă permite să preluați mai multe magazine. În continuare, veți observa că obiectul de stare este acum o fabrică (de exemplu, se întoarce dintr-o funcție, nu este creată prin parsare). Și nu mai există secțiunea de mutații. În cele din urmă, în interiorul acțiunilor, puteți vedea că accesăm starea ca doar proprietăți pe this pointer. Nu mai trebuie să treacă în stat și să se angajeze la acțiuni. Acest lucru ajută nu numai la simplificarea dezvoltării, dar facilitează și deducerea tipurilor pentru TypeScript.

Pentru a înregistra Vuex în aplicația dvs., veți înregistra Vuex în locul magazinului dvs. global:

 import { createVuex } from 'vuex' createApp(App) .use(createVuex()) .use(router) .mount('#app')

În cele din urmă, pentru a utiliza magazinul, vei importa magazinul, apoi vei crea o instanță a acestuia:

 import bookStore from "@/store"; export default defineComponent({ components: { BookInfo, }, setup() { const store = bookStore(); // Generate the wrapper // ...

Observați că ceea ce este returnat din magazin este un obiect din fabrică care returnează această instanță a magazinului, indiferent de câte ori apelați la fabrică. Obiectul returnat este doar un obiect cu acțiunile, starea și getters ca cetățeni de primă clasă (cu informații de tip):

 onMounted(async () => await store.loadBooks()); const incrementPage = () => store.currentPage++; const decrementPage = () => store.currentPage--;

Ceea ce veți vedea aici este că starea (de exemplu currentPage ) sunt doar proprietăți simple. Iar acțiunile (de exemplu loadBooks ) sunt doar funcții. Faptul că folosești un magazin aici este un efect secundar. Puteți trata obiectul Vuex doar ca pe un obiect și vă puteți desfășura munca. Aceasta este o îmbunătățire semnificativă a API-ului.

O altă modificare pe care este important de subliniat este că vă puteți genera și magazinul folosind o sintaxă asemănătoare API-ului Composition:

 export default defineStore("another", () => { // State const isBusy = ref(false); const books = reactive(new Array≷Work>()); // Actions async function loadBooks() { try { this.isBusy = true; const response = await bookService.getBooks(this.currentTopic, this.currentPage); if (response.status === 200) { this.books = response.data.works; } } finally { this.isBusy = false; } } findBook(key: string): Work | undefined { return this.books.find(b => b.key === key); } // Getters const bookCount = computed(() => this.books.length); return { isBusy, books, loadBooks, findBook, bookCount } });

Acest lucru vă permite să vă construiți obiectul Vuex la fel cum ați face cu vederile dvs. cu API-ul Composition și, probabil, este mai simplu.

Un dezavantaj principal al acestui nou design este că pierzi nemutabilitatea stării. Au loc discuții despre posibilitatea de a activa acest lucru (doar pentru dezvoltare, la fel ca Vuex 4), dar nu există un consens cât de important este acest lucru. Personal cred că este un beneficiu cheie pentru Vuex, dar va trebui să vedem cum se desfășoară asta.

Unde suntem?

Gestionarea stării partajate în aplicațiile cu o singură pagină este o parte crucială a dezvoltării pentru majoritatea aplicațiilor. A avea un plan de joc despre cum doriți să procedați în Vue este un pas important în proiectarea soluției dvs. În acest articol, v-am arătat mai multe modele pentru gestionarea stării partajate, inclusiv ceea ce urmează pentru Vuex 5. Sperăm că acum veți avea cunoștințele necesare pentru a lua decizia corectă pentru propriile proiecte.