Utilizarea sloturilor în Vue.js
Publicat: 2022-03-10Odată cu lansarea recentă a Vue 2.6, sintaxa pentru utilizarea sloturilor a fost făcută mai succintă. Această modificare a sloturilor m-a făcut să mă reinteresez să descopăr potențiala putere a sloturilor de a oferi reutilizabilitate, funcții noi și lizibilitate mai clară proiectelor noastre bazate pe Vue. De ce sunt capabile cu adevărat sloturile?
Dacă sunteți nou în Vue sau nu ați văzut modificările din versiunea 2.6, citiți mai departe. Probabil cea mai bună resursă pentru a afla despre sloturi este propria documentație Vue, dar voi încerca să dau o scurtă prezentare aici.
Ce sunt sloturile?
Sloturile sunt un mecanism pentru componentele Vue care vă permite să compuneți componentele într-un alt mod decât relația strictă părinte-copil. Sloturile vă oferă o priză pentru a plasa conținut în locuri noi sau pentru a face componentele mai generice. Cel mai bun mod de a le înțelege este să le vezi în acțiune. Să începem cu un exemplu simplu:
// frame.vue <template> <div class="frame"> <slot></slot> </div> </template>
Această componentă are un wrapper div
. Să presupunem că div
este acolo pentru a crea un cadru stilistic în jurul conținutului său. Această componentă poate fi utilizată generic pentru a înfăşura un cadru în jurul oricărui conţinut dorit. Să vedem cum arată să-l folosești. Componenta frame
aici se referă la componenta pe care tocmai am făcut-o mai sus.
// app.vue <template> <frame><img src="an-image.jpg"></frame> </template>
Conținutul care se află între etichetele frame
de deschidere și de închidere va fi inserat în componenta frame
unde se află slot
, înlocuind etichetele de slot
. Acesta este cel mai elementar mod de a face acest lucru. De asemenea, puteți specifica conținutul implicit pentru a intra într-un slot pur și simplu completându-l:
// frame.vue <template> <div class="frame"> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
Deci acum, dacă îl folosim astfel:
// app.vue <template> <frame /> </template>
Textul implicit „Acesta este conținutul implicit dacă nu se specifică nimic pentru a merge aici” va apărea, dar dacă îl folosim așa cum am făcut înainte, textul implicit va fi înlocuit de eticheta img
.
Sloturi multiple/denumite
Puteți adăuga mai multe sloturi la o componentă, dar dacă o faceți, toate, cu excepția unuia, trebuie să aibă un nume. Dacă există unul fără nume, acesta este slotul implicit. Iată cum creați mai multe sloturi:
// titled-frame.vue <template> <div class="frame"> <header><h2><slot name="header">Title</slot></h2></header> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
Am păstrat același slot implicit, dar de data aceasta am adăugat un slot numit header
în care puteți introduce un titlu. Îl folosești așa:
// app.vue <template> <titled-frame> <template v-slot:header> <!-- The code below goes into the header slot --> My Image's Title </template> <!-- The code below goes into the default slot --> <img src="an-image.jpg"> </titled-frame> </template>
La fel ca înainte, dacă vrem să adăugăm conținut la slotul implicit, puneți-l direct în componenta titled-frame
. Totuși, pentru a adăuga conținut la un slot numit, trebuia să înfășurăm codul într-o etichetă template
cu o directivă v-slot
. Adăugați două puncte ( :
) după v-slot
și apoi scrieți numele slotului la care doriți să fie transmis conținutul. Rețineți că v-slot
este nou pentru Vue 2.6, așa că, dacă utilizați o versiune mai veche, va trebui să citiți documentele despre sintaxa slotului depreciată.
Sloturi cu scop
Încă un lucru pe care trebuie să-l știți este că sloturile pot transmite date/funcții copiilor lor. Pentru a demonstra acest lucru, vom avea nevoie de o componentă de exemplu complet diferită cu sloturi, una care este chiar mai inventată decât cea anterioară: hai să copiem exemplul din documente prin crearea unei componente care furnizează date despre utilizatorul curent în sloturile sale:
// current-user.vue <template> <span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span> </template> <script> export default { data () { return { user: ... } } } </script>
Această componentă are o proprietate numită user
cu detalii despre utilizator. În mod implicit, componenta arată numele de familie al utilizatorului, dar rețineți că folosește v-bind
pentru a lega datele utilizatorului la slot. Cu aceasta, putem folosi această componentă pentru a furniza datele utilizatorului descendenților săi:
// app.vue <template> <current-user> <template v-slot:default="slotProps">{{ slotProps.user.firstName }}</template> </current-user> </template>
Pentru a avea acces la datele transmise slotului, specificăm numele variabilei de scope cu valoarea directivei v-slot
.
Există câteva note de luat aici:
- Am specificat numele
default
, deși nu este nevoie pentru slotul implicit. În schimb, am putea folosi doarv-slot="slotProps"
. - Nu trebuie să utilizați
slotProps
ca nume. Îi poți numi cum vrei. - Dacă utilizați doar un slot implicit, puteți sări peste acea etichetă interioară de
template
și să puneți directivav-slot
direct pe etichetacurrent-user
. - Puteți utiliza destructurarea obiectelor pentru a crea referințe directe la datele intervalului de acoperire, în loc să utilizați un singur nume de variabilă. Cu alte cuvinte, puteți folosi
v-slot="{user}"
în loc dev-slot="slotProps"
și apoi puteți utilizauser
direct în loc deslotProps.user
.
Luând în considerare aceste note, exemplul de mai sus poate fi rescris astfel:
// app.vue <template> <current-user v-slot="{user}"> {{ user.firstName }} </current-user> </template>
Încă câteva lucruri de reținut:
- Puteți lega mai multe valori cu directive
v-bind
. Deci, în exemplu, aș fi putut face mai mult decâtuser
. - Puteți transfera funcții și sloturilor cu scop. Multe biblioteci folosesc acest lucru pentru a oferi componente funcționale reutilizabile, așa cum veți vedea mai târziu.
-
v-slot
are un alias de#
. Deci, în loc să scriețiv-slot:header="data"
, puteți scrie#header="data"
. De asemenea, puteți specifica doar#header
în loc dev-slot:header
atunci când nu utilizați sloturi definite. În ceea ce privește sloturile implicite, va trebui să specificați numeledefault
atunci când utilizați aliasul. Cu alte cuvinte, va trebui să scrieți#default="data"
în loc de#="data"
.
Mai sunt câteva puncte minore despre care puteți afla din documente, dar asta ar trebui să fie suficient pentru a vă ajuta să înțelegeți despre ce vorbim în restul acestui articol.
Ce poți face cu sloturile?
Sloturile nu au fost construite pentru un singur scop, sau cel puțin dacă au fost, au evoluat mult dincolo de această intenție inițială de a fi un instrument puternic pentru a face multe lucruri diferite.
Modele reutilizabile
Componentele au fost întotdeauna concepute pentru a putea fi reutilizate, dar unele modele nu sunt practice pentru a fi aplicate cu o singură componentă „normală”, deoarece numărul de elemente de props
de care veți avea nevoie pentru a o personaliza poate fi excesiv sau ar trebui să treceți secțiuni mari de conținut și eventual alte componente prin props
. Sloturile pot fi folosite pentru a cuprinde partea „exterior” a modelului și a permite altor elemente HTML și/sau componente să fie plasate în interiorul lor pentru a personaliza partea „internă”, permițând componentei cu sloturi să definească modelul și componentele injectate în sloturile să fie unice.
Pentru primul nostru exemplu, să începem cu ceva simplu: un buton. Imaginează-ți că tu și echipa ta folosești Bootstrap*. Cu Bootstrap, butoanele dvs. sunt adesea legate cu clasa de bază `btn` și o clasă care specifică culoarea, cum ar fi `btn-primary`. De asemenea, puteți adăuga o clasă de mărime, cum ar fi „btn-lg”.
* Nu te încurajez și nici nu te descurajez să faci asta, aveam nevoie doar de ceva pentru exemplul meu și este destul de cunoscut.
Să presupunem acum, de dragul simplității, că aplicația/site-ul dvs. folosește întotdeauna btn-primary
și btn-lg
. Nu vrei să fii nevoit să scrii mereu toate cele trei clase pe butoanele tale sau poate nu ai încredere că un începător să-și amintească să le facă pe toate trei. În acest caz, puteți crea o componentă care are automat toate aceste trei clase, dar cum permiteți personalizarea conținutului? O prop
nu este practică, deoarece o etichetă de button
are voie să aibă tot felul de HTML în ea, așa că ar trebui să folosim un slot.
<!-- my-button.vue --> <template> <button class="btn btn-primary btn-lg"> <slot>Click Me!</slot> </button> </template>
Acum îl putem folosi oriunde cu orice conținut doriți:
<!-- somewhere else, using my-button.vue --> <template> <my-button> <img src="/img/awesome-icon.jpg"> SMASH THIS BUTTON TO BECOME AWESOME FOR ONLY $500!!! </my-button> </template>
Desigur, poți merge cu ceva mult mai mare decât un buton. Continuând cu Bootstrap, să ne uităm la un modal, sau cel puțin la partea HTML; Nu voi intra în funcționalitate... încă.
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <slot name="footer"></slot> </div> </div> </div> </div> </template>
Acum, să folosim asta:
<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <template #footer> <em>Now back to your regularly scheduled app usage</em> </template> </my-modal> </template>
Tipul de caz de utilizare de mai sus pentru sloturi este evident foarte util, dar poate face chiar mai mult.
Reutilizarea funcționalității
Componentele Vue nu sunt doar despre HTML și CSS. Sunt construite cu JavaScript, deci sunt și despre funcționalitate. Sloturile pot fi utile pentru a crea funcționalități o singură dată și pentru a le folosi în mai multe locuri. Să revenim la exemplul nostru modal și să adăugăm o funcție care închide modulul:
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <!-- using `v-bind` shorthand to pass the `closeModal` method to the component that will be in this slot --> <slot name="footer" :closeModal="closeModal"></slot> </div> </div> </div> </div> </template> <script> export default { //... methods: { closeModal () { // Do what needs to be done to close the modal... and maybe remove it from the DOM } } } </script>
Acum, când utilizați această componentă, puteți adăuga un buton la subsol care poate închide modalul. În mod normal, în cazul unui modal Bootstrap, puteți doar să adăugați data-dismiss="modal"
la un buton, dar dorim să ascundem anumite lucruri Bootstrap departe de componentele care vor fi introduse în această componentă modală. Așa că le transmitem o funcție pe care o pot apela și nu sunt cu atât mai înțelepți cu privire la implicarea Bootstrap:
<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <!-- pull in `closeModal` and use it in a button's click handler --> <template #footer="{closeModal}"> <button @click="closeModal"> Take me back to the app so I can be awesome </button> </template> </my-modal> </template>
Componente Renderless
Și, în cele din urmă, puteți lua ceea ce știți despre utilizarea sloturilor pentru a trece în jurul funcționalității reutilizabile și puteți elimina practic tot HTML-ul și doar utilizați sloturile. În esență, asta este o componentă fără randare: o componentă care oferă doar funcționalitate fără nici un HTML.
A face componente cu adevărat fără randare poate fi puțin dificil, deoarece va trebui să scrieți funcții de render
, mai degrabă decât să utilizați un șablon, pentru a elimina necesitatea unui element rădăcină, dar s-ar putea să nu fie întotdeauna necesar. Să aruncăm o privire la un exemplu simplu care ne permite să folosim mai întâi un șablon:
<template> <transition name="fade" v-bind="$attrs" v-on="$listeners"> <slot></slot> </transition> </template> <style> .fade-enter-active, .fade-leave-active { transition: opacity 0.3s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
Acesta este un exemplu ciudat de componentă fără randare, deoarece nu are nici măcar JavaScript în ea. Acest lucru se datorează faptului că doar creăm o versiune reutilizabilă preconfigurată a unei funcții fără randare încorporate: transition
.
Da, Vue are componente fără randare încorporate. Acest exemplu particular este preluat dintr-un articol despre tranzițiile reutilizabile al lui Cristi Jora și arată o modalitate simplă de a crea o componentă fără redare care poate standardiza tranzițiile utilizate în aplicația dvs. Articolul lui Cristi intră mult mai în profunzime și arată câteva variante mai avansate de tranziții reutilizabile, așa că recomand să-l verificați.
Pentru celălalt exemplu al nostru, vom crea o componentă care se ocupă de comutarea a ceea ce este afișat în diferitele stări ale unei Promisiune: în așteptare, rezolvată cu succes și eșuată. Este un model obișnuit și, deși nu necesită mult cod, poate încurca multe componente dacă logica nu este scoasă pentru a fi reutilizată.
<!-- promised.vue --> <template> <span> <slot name="rejected" v-if="error" :error="error"></slot> <slot name="resolved" v-else-if="resolved" :data="data"></slot> <slot name="pending" v-else></slot> </span> </template> <script> export default { props: { promise: Promise }, data: () => ({ resolved: false, data: null, error: null }), watch: { promise: { handler (promise) { this.resolved = false this.error = null if (!promise) { this.data = null return } promise.then(data => { this.data = data this.resolved = true }) .catch(err => { this.error = err this.resolved = true }) }, immediate: true } } } </script>
Deci ce se întâmplă aici? În primul rând, rețineți că primim o recuzită numită promise
, care este o Promise
. În secțiunea de urmărire watch
modificările aduse promisiunii și atunci când aceasta se schimbă (sau imediat la crearea componentei datorită proprietății immediate
) ștergem starea și apelăm then
și catch
promisiunea, actualizând starea când fie se termină cu succes, fie eșuează.
Apoi, în șablon, arătăm un slot diferit în funcție de stare. Rețineți că nu am reușit să-l păstrăm cu adevărat fără redare, deoarece aveam nevoie de un element rădăcină pentru a utiliza un șablon. De asemenea, transmitem data
și error
în domeniile de slot relevante.
Și iată un exemplu de utilizare:
<template> <div> <promised :promise="somePromise"> <template #resolved="{ data }"> Resolved: {{ data }} </template> <template #rejected="{ error }"> Rejected: {{ error }} </template> <template #pending> Working on it... </template> </promised> </div> </template> ...
Transmitem somePromise
la componenta fără randare. În timp ce așteptăm să se termine, afișăm „Working on it…” datorită slotului pending
. Dacă reușește, afișăm „Resolved:” și valoarea rezoluției. Dacă eșuează, afișăm „Rejected:” și eroarea care a cauzat respingerea. Acum nu mai trebuie să urmărim starea promisiunii în cadrul acestei componente, deoarece acea parte este extrasă în propria sa componentă reutilizabilă.
Deci, ce putem face în privința acelui span
care se înfășoară în jurul sloturilor din promised.vue
? Pentru a-l elimina, va trebui să eliminăm porțiunea template
și să adăugăm o funcție de render
la componenta noastră:
render () { if (this.error) { return this.$scopedSlots['rejected']({error: this.error}) } if (this.resolved) { return this.$scopedSlots['resolved']({data: this.data}) } return this.$scopedSlots['pending']() }
Nu se întâmplă nimic prea complicat aici. Folosim doar câteva blocuri if
pentru a găsi starea și apoi returnăm slotul corect (prin this.$scopedSlots['SLOTNAME'](...)
) și transmitem datele relevante în domeniul slotului. Când nu utilizați un șablon, puteți sări peste utilizarea extensiei de fișier .vue
trăgând JavaScript din eticheta de script
și introducându-l într-un fișier .js
. Acest lucru ar trebui să vă ofere o ușoară creștere a performanței atunci când compilați acele fișiere Vue.
Acest exemplu este o versiune redusă și ușor modificată a lui vue-promised, pe care aș recomanda-o să utilizeze exemplul de mai sus, deoarece acopera unele posibile capcane. Există o mulțime de alte exemple grozave de componente fără randare. Baleada este o bibliotecă întreagă plină de componente fără randare care oferă funcționalități utile ca aceasta. Există, de asemenea, vue-virtual-scroller pentru a controla redarea elementului din listă pe baza a ceea ce este vizibil pe ecran sau PortalVue pentru a „teleporta” conținut în părți complet diferite ale DOM.
Am iesit
Sloturile Vue duc dezvoltarea bazată pe componente la un nivel cu totul nou și, deși am demonstrat o mulțime de moduri excelente în care sloturile pot fi folosite, există nenumărate altele. La ce idee grozavă vă puteți gândi? În ce mod credeți că sloturile ar putea obține un upgrade? Dacă aveți, asigurați-vă că aduceți ideile dvs. echipei Vue. Dumnezeu să vă binecuvânteze și să vă bucurați de codificare.