Hur man bygger komplexa, storskaliga Vue.js-apparater med Vuex

Det är så lätt att lära sig och använda Vue.js att vem som helst kan bygga en enkel applikation med den ramen. Även nybörjare, med hjälp av Vues dokumentation, kan göra jobbet. Men när komplexiteten träder i spel blir sakerna lite allvarligare. Sanningen är att flera, djupt kapslade komponenter med delat tillstånd snabbt kan göra din ansökan till en oövervinnlig röra.

Huvudproblemet i en komplicerad applikation är hur man hanterar tillståndet mellan komponenter utan att skriva spagetti eller producera biverkningar. I den här handledningen lär du dig hur du löser problemet genom att använda Vuex: ett statshanteringsbibliotek för att bygga komplexa Vue.js-applikationer.

Vad är Vuex?

Vuex är ett statshanteringsbibliotek som är speciellt anpassat för byggnadskomplexa, stora Vue.js-applikationer. Den använder en global centraliserad butik för alla komponenter i en applikation, och utnyttjar sitt reaktivitetssystem för omedelbara uppdateringar.

Vuex-butiken är utformad så att det inte går att ändra sitt tillstånd från någon komponent. Detta säkerställer att staten bara kan muteras på ett förutsägbart sätt. Således blir din butik en enda källa till sanning: varje dataelement lagras bara en gång och är skrivskyddad för att förhindra att programmets komponenter skadar det tillstånd som är tillgängligt för andra komponenter.

Varför behöver du Vuex?

Du kanske frågar: Varför behöver jag Vuex i första hand? Kan jag inte bara sätta delat tillstånd i en vanlig JavaScript-fil och importera den till min Vue.js-applikation?

Du kan naturligtvis, men jämfört med ett vanligt globalt objekt, Vuex-butiken har några stora fördelar och fördelar:

  • Vuex-butiken är reaktiv. När komponenterna hämtar ett tillstånd från det, kommer de reaktivt att uppdatera sina åsikter varje gång staten ändras.
  • Komponenter kan inte direkt muta butikens tillstånd. Det enda sättet att ändra butikens tillstånd är genom att uttryckligen begå mutationer. Detta säkerställer att varje tillståndsändring lämnar en trackable record, vilket gör applikationen lättare att felsöka och testa.
  • Du kan enkelt felsöka din ansökan tack vare Vuex-integrationen med Vue DevTools-förlängning.
  • Vuex-butiken ger dig ett fågelperspektiv av hur allt är kopplat och påverkat i din ansökan.
  • Det är lättare att underhålla och synkronisera tillståndet mellan flera komponenter, även om komponenthierarkin ändras.
  • Vuex möjliggör direkt tvärkomponentkommunikation.
  • Om en komponent förstörs kommer staten i Vuex-butiken att förbli intakt.

Komma igång med Vuex

Innan vi börjar, vill jag göra flera saker tydliga. 

Först att följa denna handledning måste du ha en god förståelse för Vue.js och dess komponentsystem, eller åtminstone minimal erfarenhet av ramverket. 

Även syftet med denna handledning är inte att visa dig hur man bygger en faktisk komplex applikation; Målet är att fokusera mer på Vuex-koncept och hur du kan använda dem för att bygga komplexa applikationer. Av den anledningen kommer jag att använda mycket vanliga och enkla exempel, utan någon överflödig kod. När du väl förstår Vuex-koncepten kommer du att kunna tillämpa dem på vilken nivå som helst av komplexiteten.

Slutligen använder jag ES2015-syntaxen. Om du inte är bekant med det kan du lära dig det här.

Och nu, låt oss börja!

Ställa in ett Vuex-projekt

Det första steget för att komma igång med Vuex är att Vue.js och Vuex är installerade på din maskin. Det finns flera sätt att göra det, men vi använder den enklaste. Skapa bara en HTML-fil och lägg till nödvändiga CDN-länkar:

            

Jag använde några CSS för att göra komponenterna snyggare, men du behöver inte oroa dig för den CSS-koden. Det hjälper dig bara att få en visuell uppfattning om vad som händer. Bara kopiera och klistra in följande inuti  märka:

Nu, låt oss skapa några komponenter att arbeta med. Inuti > tagg, precis ovanför stängningen  tagg, sätt följande Vue-kod:

Vue.component ('ChildB', template: ' 

Göra:

') Vue.component (' ChildA ', template:'

Göra:

') Vue.component (' Parent ', template:'

Göra:

') ny Vue (el:' #app ')

Här har vi en Vue-förekomst, en föräldrakomponent och två barnkomponenter. Varje komponent har en rubrik "Göra:"där vi kommer att mata ut app staten.

Det sista du behöver göra är att lägga in en omslag

 med id = "app" strax efter öppningen , och placera sedan föräldrakomponenten inuti:

Förberedelsearbetet är nu klart och vi är redo att fortsätta.

Utforska Vuex

Statlig förvaltning

I det verkliga livet hanterar vi komplexitet genom att använda strategier för att organisera och strukturera innehållet vi vill använda. Vi grupperar relaterade saker i olika sektioner, kategorier etc. Det är som ett bokbibliotek där böckerna kategoriseras och läggs i olika sektioner så att vi enkelt kan hitta det vi letar efter. Vuex arrangerar applikationsdata och logik relaterad till tillstånd i fyra grupper eller kategorier: stat, getters, mutationer och åtgärder.

Stat och mutationer är basen för vilken Vuex-butik som helst:

  • stat är ett objekt som håller tillståndet för ansökningsdata.
  • mutationer är också ett objekt som innehåller metoder som påverkar tillståndet.

Getters och handlingar är som logiska prognoser av stat och mutationer:

  • getters innehåller metoder som används för att abstrahera tillgången till staten och att göra vissa förbehandlingsjobb, om det behövs (datakalkylering, filtrering etc.).
  • åtgärder är metoder som används för att utlösa mutationer och utföra asynkron kod.

Låt oss undersöka följande diagram för att göra sakerna lite tydligare:

På vänster sida har vi ett exempel på en Vuex butik, som vi kommer att skapa senare i denna handledning. På höger sida har vi ett Vuex-arbetsflödesdiagram som visar hur de olika Vuex-elementen fungerar tillsammans och kommunicerar med varandra.

För att ändra tillståndet måste en viss Vue-komponent begå mutationer (t.ex.. detta. $ store.commit ('increment', 3)), och sedan förändrar dessa mutationer staten (Göra blir 3). Därefter uppdateras getters automatiskt tack vare Ves reaktiva system, och de gör uppdateringarna i komponentens syn (med detta. $ store.getters.score). 

Mutationer kan inte utföra asynkron kod eftersom det här skulle göra det omöjligt att spela in och spåra ändringarna i felsökningsverktyg som Vue DevTools. För att använda asynkron logik måste du lägga den i handlingar. I det här fallet kommer en komponent först att skicka åtgärder (detta. $ store.dispatch ('incrementScore', 3000)) där den asynkrona koden exekveras, och då kommer dessa åtgärder att begå mutationer som kommer att mutera staten. 

Skapa ett Vuex Store Skeleton

Nu när vi har undersökt hur Vuex fungerar, låt oss skapa skelettet för vår Vuex butik. Lägg följande kod ovanför ChildB komponentregistrering:

const store = nya Vuex.Store (state: , getters: , mutationer: , åtgärder: )

För att ge global tillgång till Vuex-butiken från varje komponent måste vi lägga till Lagra egendom i Vue-förekomsten:

ny Vue (el: '#app', butik // registrera Vuex-butiken globalt)

Nu kan vi komma åt affären från varje komponent med detta. $ butik variabel.

Hittills, om du öppnar projektet med CodePen i webbläsaren, ska du se följande resultat.

Statliga egenskaper

Statobjektet innehåller all delad data i din ansökan. Naturligtvis, om det behövs, kan varje komponent också ha sin egen privata stat.

Tänk dig att du vill bygga en spelapplikation, och du behöver en variabel för att lagra spelets poäng. Så du sätter det i statobjektet:

tillstånd: poäng: 0

Nu kan du nå statens poäng direkt. Låt oss gå tillbaka till komponenterna och återanvända data från affären. För att kunna återanvända reaktiva data från butikens tillstånd bör du använda beräknade egenskaper. Så låt oss skapa en Göra() beräknad egendom i moderkomponenten:

beräknad: score () returnera detta. $ store.state.score

I moderkomponentens mall lägger du Göra uttryck:

Poäng: score

Och gör detsamma för de två barnkomponenterna.

Vuex är så smart att det kommer att göra allt för oss för att reaktivt uppdatera Göra egendom när staten ändras. Försök att ändra poängvärdet och se hur resultatet uppdateras i alla tre komponenterna.

Skapa getters

Det är självklart bra att du kan återanvända detta. $ store.state nyckelord inuti komponenterna, som du såg ovan. Men tänk på följande scenarier:

  1. I en storskalig applikation, där flera komponenter tillgodoser tillståndet i affären genom att använda detta. $ store.state.score, du bestämmer dig för att ändra namnet på Göra. Det betyder att du måste byta namn på variabeln i varje komponent som använder den! 
  2. Du vill använda ett beräknat värde av staten. Låt oss säga att du vill ge spelare en bonus på 10 poäng när poängen når 100 poäng. Så när poängen träffar 100 poäng läggs 10 poäng bonus. Det betyder att varje komponent måste innehålla en funktion som återanvänder poängen och ökar den med 10. Du kommer att ha upprepad kod i varje komponent, vilket inte är bra alls!

Lyckligtvis erbjuder Vuex en arbetslösning för att hantera sådana situationer. Föreställ dig den centraliserade getter som når till butikens tillstånd och ger en getterfunktion till var och en av statens föremål. Om det behövs kan denna getter tillämpa viss beräkning på statens föremål. Och om du behöver byta namn på några av statens egenskaper, ändrar du bara dem på ett ställe, i denna getter. 

Låt oss skapa en Göra() getter:

getters: score (state) return state.score

En getter tar emot stat som det första argumentet, och använder sedan det för att komma åt statens egenskaper.

Obs! Getters får också getters som det andra argumentet. Du kan använda den för att komma åt andra getters i affären.

Ändra i alla komponenter Göra() beräknade egenskaper för att använda Göra() getter istället för statens poäng direkt.

beräknad: score () returnera detta. $ store.getters.score

Nu, om du bestämmer dig för att ändra Göra till resultat, du behöver bara uppdatera den på ett ställe: i Göra() getter. Prova det i denna CodePen!

Skapa mutationer

Mutationer är det enda tillåtna sättet att ändra tillståndet. Utlösande förändringar innebär helt enkelt att begå mutationer i komponentmetoder.

En mutation är ganska mycket en händelsehanteringsfunktion som definieras av namn. Mutationhanteringsfunktionerna mottar a stat som ett första argument. Du kan vidarebefordra ett ytterligare andra argument, vilket kallas nyttolast för mutationen. 

Låt oss skapa en ökning() mutation:

mutationer: increment (state, step) state.score + = steg

Mutationer kan inte ringas direkt! För att utföra en mutation borde du ringa begå() metod med namnet på motsvarande mutation och eventuella ytterligare parametrar. Det kan bara vara en, som steg i vårt fall, eller det kan vara flera som är inslagna i ett objekt.

Låt oss använda ökning() mutation i de två barns komponenterna genom att skapa en metod som heter changeScore ():

metoder: changeScore () this. $ store.commit ('increment', 3); 

Vi begår en mutation istället för att förändras detta. $ store.state.score direkt, eftersom vi vill uttryckligen spåra ändringen som gjorts av mutationen. På så vis gör vi vår applikationslogik mer transparent, spårbar och lätt att motivera. Dessutom gör det möjligt att implementera verktyg, som Vue DevTools eller Vuetron, som kan logga alla mutationer, ta statliga ögonblicksbilder och utföra tidsresor debugging.

Nu, låt oss sätta changeScore () metod i bruk. I varje mall för de två barnkomponenterna skapar du en knapp och lägger till en klickhändelselyttare på den:

När du klickar på knappen, ökas tillståndet med 3, och denna ändring kommer att återspeglas i alla komponenter. Nu har vi effektivt uppnått direktkomponentkommunikation, vilket inte är möjligt med Vue.js inbyggda "props down, events up" -mekanism. Kolla in det i vårt CodePen-exempel.

Skapa åtgärder

En åtgärd är bara en funktion som begår en mutation. Det ändrar staten indirekt, vilket möjliggör utförande av asynkrona operationer. 

Låt oss skapa en incrementScore () verkan:

åtgärder: incrementScore: (commit, delay) => setTimeout (() => commit ("inkrement", 3), fördröjning)

Åtgärder får sammanhang som den första parametern, som innehåller alla metoder och egenskaper från affären. Vanligtvis extraherar vi bara de delar vi behöver genom att använda ES2015-argumentdestrukturering. De begå Metod är en som vi behöver mycket ofta. Åtgärder får också ett andra nyttolastargument, precis som mutationer.

ChildB komponent, ändra changeScore () metod:

metoder: changeScore () this. $ store.dispatch ('incrementScore', 3000); 

För att ringa en åtgärd använder vi avsändande() metod med namnet på motsvarande åtgärd och ytterligare parametrar, precis som med mutationer.

Nu den Ändra poäng knapp från childâ komponenten ökar poängen med 3. Den identiska knappen från ChildB komponenten kommer att göra detsamma, men efter en fördröjning på 3 sekunder. I det första fallet kör vi synkron kod och vi använder en mutation, men i det andra fallet kör vi asynkron kod, och vi behöver istället använda en åtgärd. Se hur allt fungerar i vårt CodePen-exempel.

Vuex Mapping Helpers

Vuex erbjuder några användbara hjälpmedel som kan effektivisera processen för att skapa tillstånd, getters, mutationer och åtgärder. I stället för att skriva dessa funktioner manuellt kan vi berätta för Vuex att skapa dem för oss. Låt oss se hur det fungerar.

Istället för att skriva Göra() beräknad egendom som denna:

beräknad: score () returnera detta. $ store.state.score

Vi använder bara mapState () hjälpar så här:

beräknad: ... Vuex.mapState (['score'])

Och den Göra() egendom skapas automatiskt för oss.

Detsamma gäller för getters, mutationer och handlingar. 

För att skapa Göra() getter, vi använder mapGetters () hjälpare:

beräknad: ... Vuex.mapGetters (['score'])

För att skapa changeScore () metod använder vi mapMutations () hjälpar så här:

metoder: ... Vuex.mapMutations (changeScore: 'increment')

När det används för mutationer och åtgärder med nyttolastargumentet måste vi överföra det argumentet i mallen där vi definierar händelsehanteraren:

Om vi ​​vill changeScore () att använda en åtgärd istället för en mutation som vi använder mapActions () så här:

metoder: ... Vuex.mapActions (changeScore: 'incrementScore')

Återigen måste vi definiera förseningen i händelsehanteraren:

Obs! Alla mapphjälpmedel returnerar ett objekt. Så, om vi vill använda dem i kombination med andra lokala beräknade egenskaper eller metoder, måste vi sammanfoga dem till ett objekt. Lyckligtvis med objektets spridningsoperatör (... ), kan vi göra det utan att använda något verktyg. 

I vårt CodePen kan du se ett exempel på hur alla mapphjälpmedel används i praktiken.

Göra butiken mer modulär

Det verkar som om problemet med komplexitet ständigt hindrar vårt sätt. Vi löst det innan genom att skapa Vuex-butiken, där vi gjorde det lätt att hantera statsledningen och komponentkommunikationen. I den affären har vi allt på ett ställe, lätt att manipulera och lätt att motivera. 

Men när vår applikation växer blir den här lättanvända affärsfilen större och större och som ett resultat hårdare att underhålla. Återigen behöver vi några strategier och tekniker för att förbättra applikationsstrukturen genom att returnera den till sin lättanpassade form. I det här avsnittet undersöker vi flera tekniker som kan hjälpa oss i detta åtagande.

Använda Vuex-moduler

Vuex tillåter oss att dela upp affärsobjektet i separata moduler. Varje modul kan innehålla sin egen stat, mutationer, åtgärder, getters och andra kapslade moduler. När vi har skapat nödvändiga moduler registrerar vi dem i butiken.

Låt oss se det i aktion:

const childB = state: result: 3, getters: resultat (state) return state.result, mutationer: öka (state, step) state.result + = step : commit, delay => setTimeout (() => commit 'increase', 6, fördröjning) const childA = state: score: 0, getters: score state return state.score, mutationer: inkrement (status, steg) state.score + = step, åtgärder: incrementScore: commit, delay) => setTimeout ( commit ('increment', 3), fördröjning) const store = ny Vuex.Store (moduler: scoreBoard: childA, resultBoard: childB)

I ovanstående exempel skapade vi två moduler, en för varje barnkomponent. Modulerna är bara vanliga föremål, som vi registrerar som RESULTATTAVLA och resultBoard i moduler föremål inuti affären. Koden för childâ är samma som i butiken från tidigare exempel. I koden för childB, Vi lägger till några ändringar i värden och namn.

Låt oss nu justera ChildB komponent för att återspegla förändringarna i resultBoard modul. 

Vue.component ('ChildB', template: ' 

Resultat: result

', beräknad: result () returnera detta. $ store.getters.result, metoder: changeResult () this. $ store.dispatch (' increaseResult ', 3000); )

childâ komponent, det enda vi behöver ändra är changeScore () metod:

Vue.component ('ChildA', template: ' 

Poäng: score

', beräknad: score () returnera detta. $ store.getters.score, metoder: changeScore () this. $ store.dispatch (' incrementScore ', 3000); )

Som du kan se splittrar butiken i moduler den mycket mer lätt och underhållbar, men ändå behåller sin stora funktionalitet. Kolla in den uppdaterade CodePen för att se den i åtgärd.

Namespaced Modules

Om du vill ha eller behöver använda ett och samma namn för en viss egendom eller metod i dina moduler, bör du överväga namespacing dem. Annars kan du observera några konstiga biverkningar, till exempel att utföra alla handlingar med samma namn eller att få felaktiga värden. 

För att namnge en Vuex-modul, ställer du bara in med namnområde egendom till Sann.

const childB = namespaced: true, state: poäng: 3, getters: score (state) return state.score, mutationer: inkrement (state, step) state.score + = step, handlingar: incrementScore: commit, delay) => setTimeout (() => commit ('increment', 6), fördröjning) const childA = namespaced: true, state:  0, getters: score (state) return state.score, mutationer: inkrement (stat, steg) state.score + = step, åtgärder: incrementScore: (commit, delay) = > setTimeout (() => commit ('increment', 3), fördröjning)

I ovanstående exempel gjorde vi egenskaps- och metodnamnen samma för de två modulerna. Och nu kan vi använda en egenskap eller metod som är prefixad med namnet på modulen. Till exempel, om vi vill använda Göra() getter från resultBoard modul skriver vi det så här: resultBoard / poäng. Om vi ​​vill ha Göra() getter från RESULTATTAVLA modul, då skriver vi det så här: Resultattavla / poäng

Låt oss nu ändra våra komponenter för att återspegla de förändringar som vi gjort. 

Vue.component ('ChildB', template: ' 

Resultat: result

', beräknad: result () returnera detta. $ store.getters [' resultBoard / score '], metoder: changeResult () this. $ store.dispatch (' resultBoard / incrementScore ', 3000); ) Vue.component ('ChildA', template: '

Poäng: score

', beräknad: score () returnera detta. $ store.getters [' scoreBoard / score '], metoder: changeScore () this. $ store.dispatch (' scoreBoard / incrementScore ', 3000); )

Som du kan se i vårt CodePen-exempel kan vi nu använda den metod eller egendom vi vill ha och få det resultat vi förväntar oss.

Dela upp Vuex Store i separata filer

I föregående avsnitt förbättrade vi applikationsstrukturen i viss utsträckning genom att separera butiken i moduler. Vi gjorde butiken renare och mer organiserad, men all butikskod och dess moduler ligger fortfarande i samma stora fil. 

Så det nästa logiska steget är att dela upp Vuex-butiken i separata filer. Tanken är att ha en enskild fil för butiken själv och en för varje objekt, inklusive modulerna. Detta innebär att ha separata filer för staten, getters, mutationer, åtgärder och för varje enskild modul (store.jsstate.js, getters.js, etc.) Du kan se ett exempel på den här strukturen i slutet av nästa avsnitt.

Använda Vue Single File Components

Vi har gjort Vuex-butiken så modulär som vi kan. Nästa sak vi kan göra är att även tillämpa samma strategi på Vue.js-komponenterna. Vi kan sätta varje komponent i en enda, fristående fil med a .vue förlängning. För att få veta hur det fungerar kan du besöka dokumentationssidan för Vue Single File Components. 

Så, i vårt fall kommer vi att ha tre filer: Parent.vueChildA.vue, och ChildB.vue

Slutligen, om vi kombinerar alla tre teknikerna, kommer vi att sluta med följande eller liknande struktur:

├──index.html └── src ├── main.js ├── App.vue ├── komponenter │ ├── Parent.vue │ ├── ChildA.vue │ ├── ChildB.vue └── butik ├── store.js ├── state.js ├── getters.js ├── mutations.js ├── actions.js └── moduler ├── childA.js └── childB.js

I vår handledning GitHub repo kan du se det färdiga projektet med ovanstående struktur.

Recap

Låt oss sammanfatta några huvudpunkter du behöver komma ihåg om Vuex:

Vuex är ett statsförvaltningsbibliotek som hjälper oss att bygga komplexa och storskaliga applikationer. Den använder en global, centraliserad butik för alla komponenter i en applikation. För att abstrahera staten använder vi getters. Getters är ungefär som beräknade egenskaper och är en idealisk lösning när vi behöver filtrera eller beräkna något på runtime.

Vuex-butiken är reaktiv, och komponenter kan inte direkt muta butikens tillstånd. Det enda sättet att mutera staten är att begå mutationer, som är synkrona transaktioner. Varje mutation ska utföra endast en åtgärd, måste vara så enkel som möjligt och är endast ansvarig för uppdatering av en del av staten.

Asynkron logik bör inkapslas i åtgärder. Varje åtgärd kan begå en eller flera mutationer, och en mutation kan begås av mer än en åtgärd. Åtgärder kan vara komplexa, men de ändrar aldrig staten direkt.

Slutligen är modularitet nyckeln till underhållbarhet. För att hantera komplexiteten och göra vår kodmodul använder vi "divide and conquer" -principen och koduppdelningen.

Slutsats

Det är allt! Du vet redan de viktigaste begreppen bakom Vuex, och du är redo att börja använda dem i praktiken.  

För korthetens och enkelhets skull har jag avsiktligt utelämnat några detaljer och funktioner i Vuex, så du måste läsa hela Vuex-dokumentationen för att lära dig allt om Vuex och dess funktionssats.